網絡游戲逆向分析-9-自動更新基址


網絡游戲逆向分析-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內核就可以輕松解決掉。所以后面准備着手這些方向,謝謝大家的觀看。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM