原標題:FPS遊戲反作弊系統設計:API調用回溯

0×01 前言

目前自己在製作csgo遊戲的反作弊系統。國內外有名的遊戲反作弊系統有TP/NP/BE和EAC,但國內外幾乎沒有關於反作弊系統方面的資料(其實是完全沒有…),一來是因爲搞二進制安全的人特別少,二來這方面在某些公司屬於”商業機密”。於是打算在freebuf上開個坑,把我的一些遊戲反作弊思路展現給大家,本文儘量以白話文的方式簡單易懂的把一些關鍵知識寫出來.涉及代碼量很少。希望如果後人有相關需求的可以參考一下本文。由於本文是一邊製作一邊寫的,所以會分爲幾個部分來寫,有些地方可能沒有考慮周全,多多見諒。

0×02 遊戲外掛常見注入方式

目前大部分遊戲外掛不再是以前那種createremotethread + loadlibary注入方式了,因爲大部分反作弊有自己的minifilter文件過濾驅動與imageloadcallback鏡像加載回調做判斷,大部分反作弊軟件在這種過濾鉤子中做這種操作:

if(!CheckFileCertificateByR3(FilePatch)){ //把文件路徑傳回r3,r3判斷文件數字簽名是否在白名單數字簽名裏面(比如微軟數字簽名),如果是白名單文件,就放行,如果不是白名單文件,就攔截 //不是白名單文件...攔截 block; } //放行 pass;

所以,外掛是特別難通過dll直接注入到遊戲裏面.因此大部分外掛通過一種 無文件落地注入方式 所謂無文件落地注入方式,就是直接在遊戲進程裏面開闢一個內存空間,把外掛的dll的shellcode寫入,之後手動修復輸入表,然後解析pe文件頭拿到dllmain,再通過createremotethread,apc或者hook方式讓遊戲執行這塊內存地址,這樣子外掛就注入了

具體代碼如下(抄自google):

//以下代碼來自與谷歌搜索 void InjectorDLLByManualMap(const char* filepath, HANDLE hProcess) { LPVOID lpBuffer; HANDLE hFile; DWORD dwLength; DWORD dwBytesRead; DWORD dwThreadId; ULONG_PTR lpReflectiveLoader; LPVOID lpRemoteDllBuffer; //打開文件 hFile = CreateFileA(filepath, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); //得到文件大小 dwLength = GetFileSize(hFile, NULL); lpBuffer = HeapAlloc(GetProcessHeap, 0, dwLength); //讀入文件 ReadFile(hFile, lpBuffer, dwLength, &dwBytesRead, NULL); //修復導入表 dwReflectiveLoaderOffset = GetReflectiveLoaderOffset(lpBuffer); //給遊戲進程分配一段內存空間 lpRemoteDllBuffer = VirtualAllocEx(hProcess, NULL, dwLength, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE); //寫入文件shellcode到分配的內存空間 WriteProcessMemory(hProcess, lpRemoteDllBuffer, lpBuffer, dwLength, NULL) lpReflectiveLoader = (ULONG_PTR)lpRemoteDllBuffer + dwReflectiveLoaderOffset; //啓動進程 CreateRemoteThread(hProcess, NULL, 1024*1024, (LPTHREAD_START_ROUTINE)lpReflectiveLoader, NULL, NULL, &dwThreadId) }

其特點是:內存標誌爲PAGE_EXECUTE_READWRITE,MEM_PRIVATE,無文件,無模塊,不會觸發minifilter和imageloadcallbacks,無法通過正常方式枚舉到外掛模塊,隱蔽性非常高.

0×03 檢測內存加載外掛

之前的方法看起來非常的”無敵”實際上也是可以對抗的,因爲其特徵也非常明顯:

內存屬性爲MEM_PRIVATE,內存標誌爲PAGE_EXECUTE_READWRITE.大小會很大.

所以檢測方法也有幾個:

1.暴力搜索PE頭,大部分這種內存加載的dll都有pe頭.一個內存屬性爲mem_private居然還有pe頭,就說明是外掛了.目前大部分反作弊都有這個機制

外掛反制: 抹掉pe頭.不止pe頭,還可以抹掉一切pe特徵.

2.createthreadcallbacks得到線程地址,判斷線程地址是否在一個內存屬性的mem_private的內存裏面.如果是,說明就是外掛了.

外掛反制:不創建線程,使用hook方啓動外掛.

3.api調用回溯.顧名思義,外掛總要調用一些api地址的,我們可以通過回溯是誰調用了api地址,然後判斷這個調用地方內存屬性是不是mem_private.有兩種方法,一個是hook所有關鍵api,在hook部位用_returnaddres得到調用地址(其實是讀ESP/RSP寄存器)第二種通過int3斷點觸發異常,使用異常處理函數處理這個異常,判斷調用者.

外掛反制: 第一種內聯hook方式,直接寫跳轉跳過hook,比如你hook的時候:

jmp 你的hook地址

push ebp

push eax

call xxxx;

外掛可以直接從push ebp調用,不再調用你jmp ,就可以繞過

第二種外掛反制目前沒有特別的能反制的地方.除非外掛自己構造api函數調用更底層的api.當然我們可以混淆原底層api的地址(無限套娃),具體以後在說.

0×04 實現調用回溯

爲了實現調用回溯,我們需要實現如下步驟:

1. 設置異常處理程序去捕獲異常,代碼如下:

AddVectoredExceptionHandler

2. 拷貝原API地址到自己的內存區域,然後填充原API地址爲int,代碼如下:

LPVOID pHOOKAdress; pHOOKAdress = Megrez_GetProAdress(pszModuleName, pszProcName); vecInt3HookedAdress.push_back((DWORD)pHOOKAdress); //用於檢測 if (pHOOKAdress == 0) { return 0; } DWORD dProSize = 0; LPBYTE pTemp = (LPBYTE)pHOOKAdress; BYTE bTemp = 0; for (dProSize = 0; ; ) { bTemp = *pTemp++; dProSize++; if (bTemp == 0xcc) { break; } } DWORD dFileSize = dProSize - 1; PVOID pNewAddr = VirtualAlloc(NULL, dFileSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE); if (pNewAddr == NULL) { return 0; } Megrez_SetMemoryAttr(pHOOKAdress, dProSize); memcpy(pNewAddr, pHOOKAdress, dProSize - 1); memset(pHOOKAdress, 0xcc, 1); memset((PBYTE)pHOOKAdress + 1, 0xc3, 1); memset((PBYTE)pHOOKAdress + 2, 0x90, dProSize - 1 -2); memset((PBYTE)pHOOKAdress + 2 + dProSize - 1 - 2 - 1, 0xcc, 1); //memset((PBYTE)pHOOKAdress + 2 + dProSize - 3 - 2 , 0xcc, 2); mapAdress.insert(pair((DWORD)pHOOKAdress, (DWORD)pNewAddr)); Megrez_SetMemoryAttr(pHOOKAdress, dProSize); Megrez_SetMemoryAttr(pNewAddr, dFileSize);

這樣子原api函數就會變成int3 當調用時候就回觸發int3異常 然後被我們的異常處理捕獲

3. 查詢異常位置內存信息,如果是meme_private者調用的代碼,則報告給服務端,代碼如下(記住,x32位下保存調用者地址的是esp,x64位下保存調用者地址的是rsp,):

size_t sizeQuery = VirtualQuery((PVOID)caller_function, lpBuffer, sizeof(MEMORY_BASIC_INFORMATION)); bool non_commit = lpBuffer->State != MEM_COMMIT; bool foreign_image = lpBuffer->Type != MEM_IMAGE && lpBuffer->RegionSize > 0x2000; bool spoof = *(PWORD)caller_function == 0x23FF; // jmp qword ptr [rbx],這是爲了防止被欺騙 return sizeQuery || non_commit || foreign_image || spoof; //返回

處理完異常後,我們要跳到原來的保存的api內存裏面正常調用(設置eip保存的內存地址)

ExceptionInfo->ContextRecord->Eip = mapAdress[(DWORD)ExceptionInfo->ExceptionRecord->ExceptionAddress]; #ifdef DEBUG WCHAR _buf[256] = { 0 }; swprintf_s(_buf, 256, L"eIP:0x%08X\n", ExceptionInfo->ContextRecord->Eip); OutputDebugStringW(_buf); #endif //已經處理了異常要再調用下一個異常處理來處理此異常 return EXCEPTION_CONTINUE_EXECUTION; } //調用下一個處理器 return EXCEPTION_CONTINUE_SEARCH;

可以看到,這樣子就得到了api調用者的信息,從而做出判斷.

(部分代碼參考了BE)

這樣,一個能檢測出絕大部分內存加載外掛的東西就做好了(誰調用誰就會被檢測)

*本文作者:huoji120,轉載請註明來自FreeBuf.COM

相關文章