網路遊戲逆向分析-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內核就可以輕鬆解決掉。所以後面準備著手這些方向,謝謝大家的觀看。