外掛Anti-warden的方法之一是給warden mod做數字簽名,在檢測到不安全或者未知的warden mod時,停止工作。對於以注入到遊戲進程的方式運行的外掛來說,應該在warden開始執行檢測之前自動卸載。這一篇介紹外掛自動卸載技術,也就是說如何在檢測到不安全的.mod時把自己卸載掉。可能有人會想,這有什麼難的,不就是調用FreeLibrary(假設加載用LoadLibrary)嗎。調用FreeLibrary是對的,不過需要一點兒小技巧。如下面代碼所示,檢測、卸載流程大致如下:
{
if (!CheckWardenMod())
{
// unsafe warden .mod!
FreeLibrary(g_hInstDLL);
}
}
InitWardenInterfacePatch的旁路點(detour patch)安裝在.mod解壓加載完成後、執行檢測代碼之前,如果檢查失敗則調用FreeLibrary卸載自身。代碼邏輯看上去很合理,但仔細想想,你會發現有點兒問題,就是在FreeLibrary之後,DLL的代碼和數據所佔用的內存已經被釋放了,可是FreeLibrary調用的返回地址是在DLL中,顯然這會產生內存訪問違例異常致使遊戲崩潰!如何解決這個問題呢?一種想法可能是(用VirtualAlloc)動態分配一塊內存,把調用FreeLibray的代碼拷貝到這塊內存裏執行,但這又會導致內存泄漏。另外一種想法是在棧裏預留一塊空間,把這塊代碼拷貝到預留的棧空間,然後用jmp指令跳到棧中運行。你可以看到,這個問題是有那麼點兒麻煩的。
這裏介紹另外一種簡潔、優雅的做法。有彙編基礎的朋友都知道,在x86上調用一個函數用指令call,返回函數則用ret。你可能想不到的是,其實用ret也能調用函數。ret指令的動作,是從棧指針esp指向的內存地址取出一個32位數做爲返回地址,然後跳到該地址處繼續執行。如果我們把FreeLibrary入棧再執行ret指令,就可以跳到FreeLibrary函數去:
ret; // call FreeLibrary to unload myself
比較起來,這種方法和用call指令調用函數的不同之處在於,call指令在跳轉到目標函數執行之前還有一個動作,就是把call指令的下一條指令地址入棧。在這個例子中就是把FreeLibrary調用的下一條地址-這是我們不希望的。用ret指令可以省去這一副作用。用ret指令調用函數的關鍵是構造棧上的數據,把合適的參數傳遞給目標函數,並讓目標函數返回時返回到合適的地址繼續執行:
{
__asm {
call CheckWardenMod;
test eax, eax;
jz unloadmyself;
ret;
unloadmyself:
pop eax; // return address of FreeLibrary
push hInstDLL; // parameter of FreeLibrary
push eax; // return address of FreeLibrary
push FreeLibrary; // function to call
ret; // call FreeLibrary to unload myself
}
}
上述代碼中,ret指令調用前棧的佈局如下:
| return address of FreeLibrary |
| address of FreeLibrary | <-- ESP
這只是一個簡單的示例,在實際的應用中,棧上數據如何構造還應該結合具體的需要(跟detour patch有關)進行調整。最早提出利用ret調用函數的可能是Gary Nebbett(《Windows NT Native API Reference》的作者),用於程序的自我刪除。