d2hackmap有一個完整性檢查的功能(Integrity Scan),用來檢查遊戲進程的代碼有沒有被改過。這個功能在d2hackmap的“安全開地圖”中有所應用。所謂的“安全開地圖”,其原理大致是在遊戲 進程分配一塊空間,把“開地圖”的相關代碼(不是一個完整的DLL模組)注入這塊空間,這段代碼會在遊戲的主線程context下運行,調用遊戲的內部函 數實現“開地圖”邏輯,完事兒後再釋放分配的空間。這個過程時間很短,也不需要修改遊戲進程的代碼,因此安全性比較高,不容易被warden抓到。是 “安全開地圖”代碼運行前的警告:
“開地圖”(Reveal Map)的代碼邏輯大致如下,紅色程式碼調用了遊戲內建函式:
void __stdcall RemoteRevealAutomapAct(RevealMapContext *pctx)
{
AutomapLayer2 *pLayer;
UnitAny* unit = *pctx->p_D2CLIENT_PlayerUnit;
if (!unit || !unit->pPos->pRoom1) return;
DWORD currlvl = unit->pPos->pRoom1->pRoom2->pDrlgLevel->nLevelNo;
DWORD act = 0;
BYTE actlvls[] = {1, 40, 75, 103, 109, 133, 134, 135, 136, 137};
do {} while (currlvl >= actlvls[++act]);
DWORD lvl = currlvl;
for (lvl = actlvls[act-1]; lvl < actlvls[act]; lvl++) {
DrlgLevel *pDrlgLevel = pctx->GetDrlgLevel((*pctx->p_D2CLIENT_pDrlgAct)->pDrlgMisc, lvl);
if (!pDrlgLevel)
pDrlgLevel = pctx->D2COMMON_GetDrlgLevel((*pctx->p_D2CLIENT_pDrlgAct)->pDrlgMisc, lvl);
if (!pDrlgLevel->pRoom2First) {
pctx->D2COMMON_InitDrlgLevel(pDrlgLevel);
}
pLayer = pctx->D2COMMON_GetDrlgLayer(lvl);
pctx->InitAutomapLayer(pLayer->nLayerNo, (DWORD)pctx->D2CLIENT_InitAutomapLayer_I);
pctx->RevealAutomapLevel(pctx, pDrlgLevel);
}
pLayer = pctx->D2COMMON_GetDrlgLayer(currlvl);
pctx->InitAutomapLayer(pLayer->nLayerNo, (DWORD)pctx->D2CLIENT_InitAutomapLayer_I);
}
由於“開地圖”需要調用到遊戲的內建函式,這給warden檢測留下了一點可乘之機:如果warden截獲了這幾個內建函式中的一個,在調用發生時 檢查調用者的身份(通過分析函數返回地址得到調用模組資訊),就可抓住外掛。在d2hackmap中,為了對付warden的這種檢測,“安全開地圖”代 碼在執行前,d2hackmap會對遊戲進程做完整性檢查,也就是檢查遊戲進程的代碼有沒有被改過。這篇文章講講“完整性檢查”的實現。
首先要明 白的是這裡說的“完整性檢查”主要指的是檢查代碼的完整性。一個可執行程式的構成,大約可分為檔案頭、程式碼片段和資料區段幾部分。程式的代碼在運行時不會改 變,一般裝載在唯讀記憶體頁面,資料區段又可分為唯讀資料和可讀寫資料兩部分。可讀寫資料裝載在讀寫記憶體頁面,從通用的角度來說,這部分資料是沒法做完整性檢 查的。d2hackmap的完整性檢查功能查的是可執行模組(exe、dll)的唯讀記憶體頁面,包括程式碼片段和唯讀資料區段。
一個windows的進 程載入幾十個DLL是很常見的,加上EXE主程式模組,完整性檢查需要檢測的資料大小一般在幾兆到幾十MB之間。對於這樣的資料量,一個好的檢測演算法是 很必要的。d2hackmap使用的策略是,對於每一個待掃描的模組,構建出相應的“乾淨”模組,然後拿兩個模組逐位元組比較。在 x86下,記憶體比較有專用、高效的彙編指令cmpsd和cmpsb。
DWORD _declspec(naked) __fastcall mymemcmpd(DWORD nSize, void* pleft, void* pright)
{
__asm
{
push esi;
push edi;
shr ecx, 2;
mov eax, edx;
mov esi, edx; // pleft
mov edi, [esp+0x0c]; // pright
rep cmpsd;
sub eax, esi;
neg eax;
pop edi;
pop esi;
ret 4;
}
}
DWORD _declspec(naked) __fastcall mymemcmpb(DWORD nSize, LPBYTE pleft, LPBYTE pright)
{
__asm
{
push esi;
push edi;
mov eax, edx;
mov esi, edx;
mov edi, [esp+0x0c];
rep cmpsb;
test ecx, ecx;
jz notfound;
sub eax, esi;
not eax;
pop edi;
pop esi;
ret 4;
notfound:
xor eax, eax;
pop edi;
pop esi;
ret 4;
}
}
現在問題的關鍵是如何構建一個“乾淨”的模組,這跟駭客的反擊中一文中提到的“模組重建”是非常相似的,唯一的區別在於“模組重建”的代碼運行在遊戲進程中,和目標模組在同一個記憶體空間。
構建一個“乾淨”模組的演算法步驟和手工載入DLL的步驟是比較類似的,描述如下:
1,把目標模組的資料完整複製一份到本地進程空間(ReadProcessMemory),以下稱為“髒”模組;
2,分配一塊空間以存放“乾淨”模組。
3,把目標模組的磁碟檔案對應到本地進程空間(CreateFile/CreateFileMapping/MapViewOfFile),以下稱為磁碟檔案映象;
4,把“髒”模組資料再複製到“乾淨”模組空間(memcpy)-這樣保證了可寫資料區段是相同的;
5,把磁碟檔案映象的可執行檔頭(PE header)複製到“乾淨”模組(memcpy)-pe header需要檢測;
6,分析pe header,把磁碟檔案映象中的唯讀section逐一複製到“乾淨”模組-唯讀section需要檢測;
7,接下來對“乾淨”模組做進一步的修正(fix-up),包括匯入表(IAT)和重定位表(relocation table);
8,IAT的修正稍微有點兒繁瑣,也和普通的載入DLL不同,主要的問題是同一個DLL,在本地載入和在遊戲進程載入的基地執有可能是不一樣的。對於IAT中連結到的DLL,修正時應該以該DLL在目標遊戲進程中載入的基地址為基準;
9,重定位表的修正也類似,應該使用“髒”模組的重定位元據-這和普通的載入DLL也不同。
經 過這幾步以後,“乾淨”模組就構建好了。接下來的完整性檢查用前面給出的mymemcmpd和mymemcmpb函數就行了。使用這種方法,完整性檢查的 效率還是比較高的,一般情況下掃描一個進程的時間在幾秒鐘(<5秒)以內。是d2hackmap外掛程式(d2hackmap.dll)注入後對遊 戲進程的完整性檢查的結果,可以看到d2hackmap.dll修改了很多處,視圖中的每一項列出了被修改的dll名稱(入d2win.dll),修改地 址,修改長度,修改後的指令,如是跳轉指令,還給出了跳轉模組的名稱(中都是d2hackmap.dll,根據這點我們就可以判斷出該處是被 d2hackmap.dll修改的)。
完 整性檢查還可以有很多其他用途,不僅僅限於遊戲外掛方面。比如說有些流氓軟體可能會在一些敏感進程中截獲某些API來監控使用者的行為,完整性檢查可以把它 檢測出來。另外,完整性檢查還可以用來分析那些依賴於代碼截獲技術的程式,比如說你想分析D2JSP.DLL的實現技術,那麼通過觀測它的截獲點,以截獲 點為起點進行逆向分析是一種很有效方法。是D2JSP載入後的完整性檢查結果: