网络游戏逆向分析-9-自动更新基址

基址在每次更新之后都会修改,这个比较麻烦,不然每次都得重新找,非常消耗体力和时间。

 

自动更新基址原理

搜索游戏进程的内存,然后把硬编码依次和内存里面数据进行匹配,匹配到了之后就返回地址,地址附近就是基址了,通过加减来得到基址。

这里要扯到一些关于硬编码和机器指令的问题了,从整个计算机来看实际上要跑的东西在CPU上,只能识别0和1,但是为了后面的多种多样功能,通过对0和1的组合来实现了机器指令,CPU可以直接通过这个0和1的指令来进行不同的操作,这个指令就叫做机器指令也可以说是硬编码(也就是硬件上的编码)根据CPU的不同而不同,而我们常用的汇编指令,是唯一一个可以和机器指令一一对应的东西,因为如果直接用机器指令对于开发来说非常非常麻烦。所以我们常用的是汇编语言,而我们通过一些Ollydbg,xdbg这些东西,都是通过CPU里用的机器指令翻译成的汇编指令,这个流程的工具叫做反汇编引擎,比如说:

 

 

这个ollydbg里面的1是内存地址,2是机器码的内容,3是汇编指令的内容。实际上运行的是内存地址里面存放的机器码,只不过这个调试器帮我们翻译成了汇编指令,然后我们修改汇编指令的时候也帮我们修改了机器码这样子。

自动更新基址思路

所以这里我们可以参考机器码,我们把整个内存的机器码读出来,然后通过机器码比对得到对应的有关地址的机器码指令,然后转成字符串,读取得到基址。

开始

这里我们随便用一段东西把:

 

 

需要注意的是,这里的机器码尽量多弄一下,这样来达到机器码是唯一的别的地方不能会重复。

逻辑都在代码里面了:

#include"UpdateAddr.h"

BOOL ByteToChar(BYTE* ByteArray, char* CharArray, int ByteLen)
{
//ByteArray是字节数组
//CharArray是字符数组
//ByteLen 是字节数组长度
for (int i = 0; i < ByteLen; i++)
{
wsprintfA(&CharArray[i * 2], "%02X", ByteArray[i]);
}


return TRUE;
}
BOOL CmpMachineStr(char* TempReadMachineCodeStr,char* MachineCodeStr, int MachineCodeStrLen)
{
// TempReadMachineCodeStr 读取的机器码字符串
// MachineCodeStr 特征机器码字符串
//MachineCodeStrLen特征机器码字符串长度
for (int i = 0; i < MachineCodeStrLen; i++)
{
if (TempReadMachineCodeStr[i] != MachineCodeStr[i])
return FALSE;
}
return TRUE;
}
BOOL ScanProcess(HANDLE HandleProcess, DWORD BeginAddr, DWORD EndAddr, char* MachineCodeStr, int MachineCodeStrLen)
{
//HandleProcess是进程的句柄,BeginAddr是起始内存地址,EndAddr是结束内存地址,MachineCode是机器码的字符串表达形式
//MachineCodeLen是机器码字符串长度。
int Flag = 0;

//每次读取0x1000个机器码的内容进行比较。
BYTE TempReadMachineCode[0x1000] = { 0 };
for (DWORD TempBeginAddr = BeginAddr; TempBeginAddr < EndAddr - 0x1000; TempBeginAddr += (0x1000 - MachineCodeStrLen))
{
//将机器码缓冲区用0填充
memset(TempReadMachineCode, 0x0, 0x1000);
//读0x1000个机器码到byte缓冲数组里。
BOOL RetReadProcessMemory = ReadProcessMemory(HandleProcess,(LPVOID)TempBeginAddr,TempReadMachineCode, 0x1000, NULL);
if (RetReadProcessMemory == 0)
continue;

//把byte字节数组转换成字符串
char TempReadMachineCodeStr[0x2001]={ 0 };
ByteToChar(TempReadMachineCode, TempReadMachineCodeStr, 0x1000);

//开始比较
for (int i = 0; i < 0x2001 - MachineCodeStrLen;i++)
{
BOOL ret = CmpMachineStr(TempReadMachineCodeStr + i,MachineCodeStr,MachineCodeStrLen);
if (ret == TRUE)
{
cout << "找到了地址为";
printf("%X\n", TempBeginAddr + i / 2);
Flag = 1;
}
}
}
if (Flag == 0)
cout << "未找到" << endl;

return TRUE;
}
int main()
{
char MachineCodeStr[] = "E8955DECFFE8D85EECFF3DB70000000F85870000008B15884754008BC7E8A4C2FFFF0FB7068945E8DB45E883C4F8DF3C249BE8EBCDFFFF";
HANDLE HandleProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 1144);
ScanProcess(HandleProcess,0x00000000,0x7FFFFFFF,MachineCodeStr,strlen(MachineCodeStr));

return 0;
}
#pragma once
#include<Windows.h>
#include<iostream>
#include<string>
using namespace std;
//扫描进行内存判断机器码
BOOL ScanProcess(HANDLE HandleProcess,DWORD BeginAddr,DWORD EndAddr,char *MachineCode,int MachineCodeLen);

//将字节数组转换为字符串
BOOL ByteToChar(BYTE* ByteArray, char* CharArray, int CharArrayLen);

//比较两个变成字符串的机器码是否相等
BOOL CmpMachineStr(char* TempReadMachineCodeStr, char* MachineCodeStr, int MachineCodeStrLen);

完善1:

前面那个不够,那个只能得到首地址,还得自己去判断,而且在硬编码(机器码)里面,一些jmp,以及call还有赋值,会因为值的改变而改变,所以这种不百分之百的确定的值就不能用了。因为如果游戏更新了肯定会有一些小的调整,很有可能会改变,所以这里得用到模糊匹配的方式来匹配了。

原理上来说不麻烦,只需要把会变化的硬编码用一个符号来代替,然后当匹配到这个符号的时候直接跳过。

比如说这里:

 

 

硬编码为:

8B15884754008BC7E8A4C2FFFF0FB706

这里我们用 ?来代替就成了这样:

8B15????????8BC7E8????????0FB706

然后在比对字符串的时候,遇到?就直接跳过就好了:

BOOL CmpMachineStr(char* TempReadMachineCodeStr,char* MachineCodeStr, int MachineCodeStrLen)
{
// TempReadMachineCodeStr 读取的机器码字符串
// MachineCodeStr 特征机器码字符串
//MachineCodeStrLen特征机器码字符串长度
for (int i = 0; i < MachineCodeStrLen; i++)
{
if (MachineCodeStr[i] == '?')
continue;
if (TempReadMachineCodeStr[i] != MachineCodeStr[i])
return FALSE;
}
return TRUE;
}

完善2

添加一个文件来方便读取:

BOOL ReadCodeFile()
{
FILE* fp;
errno_t  errnoFile = fopen_s(&fp, "HardCode.txt", "r");
if (errnoFile != 0)
return FALSE;
char* CodeBuff = new char[0x100]{ 0 };
fgets(CodeBuff, 0x100, fp);


fclose(fp);
delete[]CodeBuff;
return TRUE;
}

完善3:

封装一个偏移值,因为如果找到了特征码但是得到的是特征码的基址,我们还要知道怎么从这个基址偏移得到我们想要的内容,所以这里就在字符串里面添加一些特征码:

比如这里:

 

 

要往下偏移,也就是+地址,+8个byte才得到我们想要的地址,那么我们就可以把字符串写成这样:

83C4F8DF3C249BE8????????,8,+    //把逗号作为一种分割
void split(vector<string> &vc,string CodeStr,const char Flag=',')
{
istringstream is(CodeStr);//把string变成istringstream的输入流
string temp;
while (getline(is, temp, Flag))
{
vc.push_back(temp);
}

}

完善4:

一个文件里面肯定有很多内容,需要把整个文件的字符串提出来,然后分割,然后把特征码拿去匹配,匹配到之后通过偏移得到具体的基址的位置。

BOOL ReadStrFile(vector<string> &AllFileStr)
{
FILE* fp;
errno_t  errnoFile = fopen_s(&fp, "HardCode.txt", "r");
if (errnoFile != 0)
return FALSE;//判断文件是否打开成功


char* TempStrBuff = new char[0x100]{ 0 };
while (!feof(fp))
{
memset(TempStrBuff, 0, 0x100);
fgets(TempStrBuff, 0x100, fp);
for (int i = 0; i < 0x100; i++)
{
if (TempStrBuff[i] == '\n')
TempStrBuff[i] = '\0';
}
string TempCodeStr = TempStrBuff;
AllFileStr.push_back(TempCodeStr);
}



fclose(fp);
delete[]TempStrBuff;
return TRUE;
}

完善5:

编写Main函数将内容通过地址+偏移,然后读取:

int main()
{
HANDLE HandleProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 1144);
if (!HandleProcess)
{
cout << "打开进程失败" << endl;
return 0;
}

vector<string> AllFileStr;
//读取文件中的字符串
ReadStrFile(AllFileStr);

//分割文件中的字符串
vector<vector<string>> AllFileStrToPartition;
split(AllFileStrToPartition, AllFileStr);
for (int i = 0; i < AllFileStrToPartition.size(); i++)
{
DWORD BaseAddr = 0;
DWORD DataBaseAddr = 0;
DWORD Context = 0;
DWORD ReadData = 0;
string TempCodeAddr = AllFileStrToPartition[i][0];
int Num = atoi(AllFileStrToPartition[i][1].c_str());
string Symbol = AllFileStrToPartition[i][2];
ScanProcess(HandleProcess, 0x00000000, 0x7FFFFFFF, (char*)TempCodeAddr.c_str(), strlen(TempCodeAddr.c_str()), BaseAddr);
//printf("%X:",BaseAddr);
//cout << Symbol << " " << Num << endl;
if (Symbol == "+")
{
DataBaseAddr = BaseAddr + Num;
}
else
{
DataBaseAddr = BaseAddr - Num;
}
//读取内容
BOOL retReadRealData= ReadProcessMemory(HandleProcess,(LPVOID)DataBaseAddr, &ReadData, 4, NULL);
if (retReadRealData == FALSE)
{
cout << "读取实际内容失败" << endl;
return 0;
}
printf("%X\n", ReadData);

}

return 0;
}

 

 

读取到了这个值了。这里我采用的是通过WORD来读取,那么还可以添加控制码来选择读取的字节数,因为有的可能是byte,或者WORD。这个功能我就不实现了,后面要用可以自己DIY一下

最终代码:

最后我加了一个文件来保存得到的基址。

总结

整个项目的代码已经打包上传github:skrandy/AutoUpdateAddr: 通过匹配特征码自动更新基址 (github.com)

通过匹配特征码,这里机器码特征码硬编码不区分。然后通过匹配到的特征码(因为特征码必须来多一点,不然很容易有相同的),特征码里面有一些值是会变的就采用模糊匹配来实现,然后得到特征码匹配上了的首地址,再通过字符串里面的首地址偏移,得到了要的数据的起始地址,然后把起始地址再拿来读取就是我们要的内容了,再把内容保存到另外一个文件里,然后自己写的外挂可以通过保存了的基址的文件进行读取拿来进行基址的更新。

 

顺带说一句,网游逆向无限期延迟更新了,因为这个游戏比较老了研究起来也没有价值,还有个原因是技术就这么写,对发包函数的处理,以及搜索数据的处理,然后往上找基址。这个只能在应用层玩,一些更高难度的比如反调试,加壳脱壳,Windows内核就可以轻松解决掉。所以后面准备着手这些方向,谢谢大家的观看。