BattlEye內核驅動檢測模塊深入分析
BattlEye概述
BattlEye總共分為以下4個部分:
BEService - 與BattlEye服務器通信的服務。
BEDaisy - 內核驅動,執行各種內核層的檢測,并與BEClient通信。
BEClient - 一個DLL,運行在游戲進程中,負責執行各種應用層的BE shellcode,并與內核驅動進行通信。
BEServer - BattlEye服務器,收集上傳的信息,并判定作弊行為。
本次分析的是BEDaisy,也就是BE內核驅動中的各種檢測。
BattlEye內核驅動檢測模塊深入分析
BE內核驅動中包含著很多種檢測,在發現檢測到異常情況時首先會記錄到一個內部的鏈表中,然后當BEClient對BE內核驅動發起特定長度的讀請求時,BE內核驅動會將鏈表內的數據發送給BEClient,BEClient再將其發送給BEServer。
半個月前寫過一個簡易的繞過BE內核驅動的程序,原理就是阻斷這一過程,具體原理請看我的上一篇文章:https://bbs.pediy.com/thread-273334-1.htm
下面的內容主要分為三個部分,第一個是上傳部分,主要是講解BEClient給BE內核驅動發送的各種檢測相關的數據;第二個是檢測部分,重點講述BE內核驅動中的各種檢測;第三個部分是對于這些檢測的總結。BE內核驅動中還包含一些其他的內容,比如數據包加解密算法、設備UID算法等,這些內容都不涉及“檢測”,因此在本文中不進行分析。三
上傳部分
BEClient通過對BE驅動調用Write方法,也就是對應驅動的IRP_MJ_WRITE方式進行上傳。上傳的內容主要是一些黑名單特征,這些特征應該是從服務器下發的,因此可以在不重新編譯驅動的情況下,動態調整檢測的特征。
[未知]黑名單特征(upload type 0)
以類似數組的形式緊密排列,由于檢測數據不定長,因此每個數據包是動態長度的,依靠包頭記錄的數據包長度確定下一個數據包的位置。
檢測分為兩類:
給定偏移量的特征,BE在檢測時只會在特定的偏移量上進行匹配。
沒有給定偏移量的特征,BE在檢測時會按子串匹配的方式嘗試所有位置進行匹配。
如果report list 0中存在數據包,則會挨個檢測是否有匹配的特征,如果存在則直接原封上報異常數據。(由于并沒有人寫入report list 0,因此懷疑該檢測暫未開啟)
report list 0 數據結構如下:
struct AbnormalListItem { // because nobody writes to report list 0, so some parts of the structure is unknown BYTE Unknown[10]; BYTE Content[64];};
通過IrpWrite上傳的數據包如下:
struct UploadPatternBlackListItemType0 { // -1 means no specified match offset, it will try every possible offset // not -1 means a specified offset, it will just try the offset BYTE MatchOffset; // if the length <= 32, it will be copied to the g_PatternBlackList BYTE PatternLength; // length depends on PatternLength BYTE Content[0];};
g_PatternBlackList是存儲著32個PatternBlackListItem的數組,具體數量記錄在g_PatternBlackListSize中。
struct PatternBlackListItemType0 { // pattern in black list up to 32 bytes BYTE Pattern[32]; // length up to 32 ULONG Length;};
在檢測線程啟動時,會向g_PatternBlackList添加一個9字節長度的硬編碼的特征(看起來像是有關ROP的一些特征?不太清楚。)
48 81 C4 80 01 00 00 5F C3
對應amd64匯編
add rsp, 180hpop rdiret
回調黑名單特征(upload type 1)
該檢測針對的是進程、線程的前置、后置回調,注冊表回調,映像加載回調,對這些函數的頭部64個字節進行特征檢測。
struct UploadPatternBlackListItemType1or2 { // -1 means universal pattern, this check will be applied to each callback // not -1 means this check only works on a specific callback BYTE FunctionType; // -1 means no specified match offset, it will try every possible offset // not -1 means a specified offset, it will just try the offset BYTE MatchOffset; // length of the pattern BYTE PatternLength; // length depends on PatternLength BYTE Content[0];};
系統調用黑名單特征(upload type 2)
數據包格式同上一個特征,檢測的對象為系統調用函數的頭部64字節。
BE驅動完整性檢測特征(upload type 3)
上傳的內容為一個給定偏移量的字節序列,在后續步驟中(見report type 18)會使用上傳的特征對BE驅動自身的重點代碼進行檢查,檢查BE驅動是否被篡改。
struct UploadSelfIntegrityCheck { // if it is true, it means use stored driver memory range // if it is false, it means use driver memory range read from driver object BOOLEAN UseStoredDriverInfo; // offset to the driver module ULONG Offset; // unknown, has an impact on the reporting policy // if the flag is true, then normal means upload, abnormal means don't upload // maybe use to detect some kind of attack? BOOLEAN FlipReportPolicy; // compare size, up to 64 bytes ULONG CompareSize; // content of normal data, length depends on CompareSize BYTE Content[0];};
Dxgkrnl某內部未導出函數特征(upload type 4)
該特征用于定位Dxgkrnl某個內部的未導出函數,在后續步驟(見report type 22),BE將會Hook該函數,并對該函數的地址范圍進行檢查。
struct UploadDxgkrnlInternalFunctionRangeCheck { // length = upload packet length - 1 BYTE Pattern[0]; // how far is the function address from the pattern matching address BYTE Offset;}
InfinityHook檢測(upload type 5)
該類型的數據包僅是為了觸發InfinityHook檢測(見report type 23),不傳輸數據。
檢測部分
該部分內容較多,總共有30多種檢測。大多數的檢測都具有標號,只有當檢測結果異常時才會記錄,并傳輸給處在應用層的BEClient,然后再由其發送給BE的服務器。所有具有標號的檢測如下,除此之外還有少量處在IRP_MJ_READ的handler中的沒有標號的檢測(例如:獲取設備UID,虛擬機檢測等)。
- 派遣函數完整性檢測
- 系統線程啟動地址檢測
- 進程、線程回調功能性檢測
- 游戲進程線程創建檢測
- PsLookupThreadByThreadId hook檢測
- [未知]
- 進程、線程、注冊表回調hook檢測
- 進程、線程、注冊表回調地址模塊范圍檢測
- PhysicalMemory引用檢測
- 系統調用完整性檢測
- [未知]
- 模塊異常指令檢測
- DxgCoreInterface 地址范圍檢測
- DxgCoreInterface hook檢測
- 系統線程堆棧檢測
- 隱藏驅動檢測
- [未知]
- 回調函數信息上報
- BE驅動完整性檢測
- 模塊IAT hook檢測
- gDxgkInterface 地址范圍檢測
- gDxgkInterface hook檢測
- Dxgkrnl某內部未導出函數范圍檢測(disabled)
- infinity hook 檢測
- gDxgkWin32kEngInterface 地址范圍檢測
- gDxgkWin32kEngInterface hook檢測
- PCI設備檢測
- HalDispatchTable 地址范圍檢測
- HalDispatchTable hook檢測
- HalPrivateDispatchTable 地址范圍檢測
- HalPrivateDispatchTable hook檢測
- FltMgrMsg對象callback模塊范圍檢測
- FltMgrMsg對象callback hook檢測
- ext_ms_win_core_win32k_full_export_l1 地址范圍檢測
- ...
BE驅動完整性檢測(report type 18)
在接收到應用程序上傳的數據后會開始檢測。BE會對自身的驅動的關鍵部位進行檢查,檢查是否被篡改。如果出現異常則會上報。
數據包結構:
struct PacketSelfIntegrityCheck { // 18 is self integrity check BYTE PacketType; // if it is true, it means use stored driver memory range // if it is false, it means use driver memory range read from driver object BOOLEAN UseStoredDriverInfo; // offset to the driver module ULONG Offset; // content of checked address, 64 bytes BYTE Content[64];};
系統調用完整性檢測(report type 9)
BE會對通過MmGetSystemRoutineAddress獲得的系統函數進行完整性檢測,會檢測此時調用MmGetSystemRoutineAddress獲得的地址與以前獲得的地址是否相同,會檢測系統函數頭部是否存在hook,如果存在hook則會追蹤連續的無條件跳轉,直到最終的hook函數,并上報該hook函數的特征上報。
總共分為4類異常:
- 函數指針修改
- 函數地址不在模塊范圍內(手動映射的驅動的hook)
- 追蹤跳轉后,函數地址不在模塊范圍內(類似上一個異常情況)
- 存在int 3斷點,說明系統正在被調試
除此之外,如果判定正常,仍會將信息臨時記錄在report list 2中,方便在后續過程中檢查是否存在黑名單特征。
數據包結構:
struct PacketSyscallIntegrityCheck { // 9 is syscall integrity check BYTE PacketType; // each syscall function has an index BYTE FuncIndex; // -1: fine // 0: function pointer modification // 1: address out of module range // 2: after jump, address out of range // 3: int3 trap, may be under debugging BYTE ErrorType; // after useless jump instructions, the function body's address PVOID Address; // dump 64 bytes BYTE Content[64];};
系統線程啟動地址檢測(report type 1)
會試圖通過多種手段遍歷系統線程(通過SystemProcessInformation獲得線程信息、通過枚舉TID嘗試得到線程對象),如果遍歷過程中檢測到隱藏進程/線程(找不到系統進程或系統進程的SystemProcessInformation中找不到當前線程),則會在全局變量中進行記錄。
如果檢測到啟動地址不在加載模塊地址范圍內的系統線程(模塊地址范圍會在LoadImageNotify中以鏈表的形式記錄),則會上報異常數據。猜測是用來檢測kdmapper等工具加載的模塊。
數據包結構:
struct PacketSystemThreadStartAddressCheck { // 1 is system thread start address check BYTE PacketType; // start address read from SYSTEM_PROCESS_INFORMATION structure PVOID StartAddress; // dump 64 bytes from start address BYTE Content[64]; // thread running time // from thread creation to now LARGE_INTEGER RunningTime; // CountdonwId = SystemProcessInformation->NumberOfThreads - AbnormalThreadIndex - 1 // counting thread indexes from back to front // making the ID generic USHORT CountdownId; // thread create time // between process creation and thread creation LARGE_INTEGER CreateTime;};
系統線程堆棧檢測(report type 14)
向所有系統線程插入APC,調用RtlWalkFrameChain獲得調用者列表,依次檢查各個內核空間調用者的地址是否在模塊范圍內,是否存在黑名單中的特征,是否存在多次跳轉(>=5)、int3、nop等異常情況,如果存在則直接上報異常數據,如果判斷正常則會添加到report list 0,待進一步進行黑名單檢查。
數據包結構:
struct PacketSystemThreadStartAddressCheck { // 14 is system thread stack check BYTE PacketType; // bad caller index in the RtlWalkFrameChain result BYTE CallerIndex; // bad caller's return address PVOID Address; // 64 bytes of caller's content BYTE Content[64]; // notice: only 32 bits // which thread has the bad caller ULONG ThreadId; // image name length BYTE ImageNameLength; // image name buffer // length depends on the ImageNameLength BYTE ImageName[0]; // low 32 bits of StartAddress, always upload ULONG LowStartAddress; // may be null if the StartAddress is invalid PVOID StartAddress; // may be null if the StartAddress is invalid HANDLE ProcessId; // thread running time // from thread creation to now LARGE_INTEGER RunningTime; // CountdonwId = SystemProcessInformation->NumberOfThreads - AbnormalThreadIndex - 1 // counting thread indexes from back to front // making the ID generic USHORT CountdownId; // thread create time // between process creation and thread creation LARGE_INTEGER CreateTime; // track the E9 jumps after the return address up to 60 bytes, // record up to 10 addresses BYTE FollowAddressCount; // size depends on the FollowAddressCount PVOID FollowAddressArr[0];};
進程、線程、注冊表回調檢測
回調Hook檢測(report type 6)
會檢測進程、線程的前置、后置回調,注冊表回調,映像加載回調,檢測是否存在一下幾種hook,最多檢測頭部64字節:
- FF 25 XX XX XX XX: jmp [addr]
- 48 B8 XX XX XX XX XX XX XX XX: mov rax, imm
- FF E0: jmp rax
(注:不會多次追蹤跳轉,只會追蹤1次,感覺設計不太合理)
當檢測到hook時才會上報異常數據,結構如下:
struct PacketCallbackHookCheck { // 6 is callback hook check BYTE PacketType; // function type: // 0: process callback // 1: thread callback // 2: register callback // 3: image notify callback BYTE FunctionType; // hooked offset to the callback function begin BYTE HookOffset; // absolute hooked address PVOID HookAddress; // dump 16 bytes of callback head BTYE CallbackHeadContent[16]; // where to jump PVOID JumpAddress; // content of address after the jump BYTE HookContent[64]; // up to 260 bytes, no terminator CHAR ModulePath[0];};
回調地址模塊范圍檢測(report type 7)
檢測回調地址是否在某個內核模塊的范圍內,如果不在任何一個模塊的地址范圍內,則上報異常。
數據包結構:
struct PacketCallbackRangeCheck { // 7 is callback range check BYTE PacketType; // function type: // 0: process callback // 1: thread callback // 2: register callback // 3: image notify callback BYTE FunctionType; // address of the function PVOID Address; // 64 bytes content of the callback BYTE Content[64];};
回調函數信息上報(report type 17)
所有進程、線程的前置、后置回調,注冊表回調,映像加載回調都會記錄到report list 1,待進一步檢測黑名單特征。
數據包結構:
struct PacketCallbackCheck { // 17 is callback check BYTE PacketType; // function type: // 0: process callback // 1: thread callback // 2: register callback // 3: image notify callback BYTE FunctionType; // address of the callback PVOID Address; // 64 bytes content of the callback BYTE Content[64]; // module path if exists, no terminator CHAR ModulePath[0];};
PhysicalMemory引用檢測(report type 8)
檢測是否有應用程序引用"\\device\\PhysicalMemory"對象,如果存在則上報異常數據。
檢測的邏輯如下:
首先遍歷所有進程,使用MmUnmapViewOfSection解除掉"\\device\\PhysicalMemory"的映射,然后再查看"\\device\\PhysicalMemory" Section對象內部的ControlArea中的NumberOfUserReferences是否為0,如果非0則說明仍存在應用程序對物理內存的引用,因此判定異常,上報異常數據。(用于檢測某種手動創建的"\\device\\PhysicalMemory"對象?)
數據包結構:
struct PacketPhysicalMemoryReferenceCheck { // 8 is physical memory reference check BYTE PacketType; // fields in struct _CONTROL_AREA ULONG64 NumberOfSectionReferences; ULONG64 NumberOfPfnReferences; ULONG64 NumberOfMappedViews; ULONG64 NumberOfUserReferences;};
進程、線程回調功能性檢測(report type 2)
首先置一個標志位為0,然后嘗試獲得游戲進程句柄,如果回調工作正常,則會將標志位置為1,否則標志位仍為0,從而達到檢測回調是否被通過某些手段摘除,無法正常工作。
如果回調無法正常工作,則會上報一次異常數據(不會重復上報)。
數據包結構:
struct PacketProcessThreadCallbackFunctionalityCheck { // 2 is process thread callback functionality check BYTE PacketType; // probably always true BOOLEAN Abnormal;};
派遣函數地址檢測(report type 0)
在BE內核模塊加載時檢查所有系統模塊的派遣函數是否都是自己本模塊內的函數或者是系統模塊(ntoskrnl)的函數。
在運行中會檢查自身的MJ_IRP_CREATE、MJ_IRP_CLOSE、MJ_IRP_READ、MJ_IRP_WRITE對應的派遣函數是否被修改,如果被修改則會上傳到異常鏈表,否則不會有額外操作。
數據包結構:
struct PacketDispatchFunctionIntegrityCheck { // 0 is dispatch function integrity check BYTE PacketType; // driver name // length = PacketLength - OtherFieldsLength CHAR DriverName[0]; // major number BYTE MajorNumber; // hook function address PVOID Address; // 64 bytes of hook function BYTE Content[64];};
PsLookupThreadByThreadId hook檢測(report type 4)
該函數在線程回調函數中被調用,該函數會檢測PsLookupThreadByThreadId是否被hook,檢測的hook類型僅是FF 25 jmp,即 jmp [addr] 類型的hook。該函數最多追蹤2次jmp,如果出現hook則會上傳到異常鏈表。
(會對封包從1到45字節做異或0x7F的加密操作,第一個字節PacketType不進行加密,不知道為什么要這么做)
數據包結構:
struct PacketPsLookupThreadByThreadIdHookCheck { // 4 is PsLookupThreadByThreadId hook check BYTE PacketType; // PsLookupThreadByThreadId address PVOID FunctionAddress; // FF 25 (4 bytes offset) ULONG JumpOffset1; // address after the first jump PVOID HookFunction1; // whether there is another jump BOOLEAN TwoJump; union { // no another jump // dump 16 bytes of the first hook function BYTE Content1[16]; // have another jump struct { // record the second hook function PVOID HookFunction2; // dump 16 bytes of the second hook function BYTE Content2[16]; }; };};
\\FileSystem\\Filters\\FltMgrMsg對象檢測
\\FileSystem\\Filters\\FltMgrMsg對象涉及到Filter通信,其中有過濾通信的回調函數,因此BE對其進行了檢測。
可以參考該文章:https://www.amossys.fr/fr/ressources/blog-technique/filter-communication-ports/
其中有3個callback會被檢測:
- ConnectNotifyCallback
- DisconnectNotifyCallback
- MessageNotifyCallback
FltMgrMsg對象callback模塊范圍檢測(report type 31)
檢測回調地址是否在某個內核模塊的范圍內,如果不在任何一個模塊的地址范圍內,則上報異常。
數據包結構:
struct PacketFltMgrMsgCallbackRangeCheck { // 31 is FltMgrMsg callback range check BYTE PacketType; // function type: // 0: ConnectNotifyCallback // 1: DisconnectNotifyCallback // 2: MessageNotifyCallback BYTE FunctionType; // address of the function PVOID Address; // 64 bytes content of the callback BYTE Content[64];};
FltMgrMsg對象callback hook檢測(report type 32)
對3個回調函數做hook檢查,方式同回調hook檢測(report type 6),僅report type不同。
在檢測到hook時會上報異常數據,結構如下:
struct PacketFltMgrMsgCallbackHookCheck { // 32 is FltMgrMsg callback hook check BYTE PacketType; // function type: // 0: ConnectNotifyCallback // 1: DisconnectNotifyCallback // 2: MessageNotifyCallback BYTE FunctionType; // hooked offset to the callback function begin BYTE HookOffset; // absolute hooked address PVOID HookAddress; // dump 16 bytes of callback head BTYE CallbackHeadContent[16]; // where to jump PVOID JumpAddress; // content of address after the jump BYTE HookContent[64]; // up to 260 bytes, no terminator CHAR ModulePath[0];};
Dxgkrnl某內部未導出函數范圍檢測(report type 22)(disabled)
首先BE會先Hook該函數,然后在Hook函數中對原始函數進行模塊范圍檢測,目前該檢測還不完善,并且在卸載驅動時也沒有對該私有鏈表進行清理,因此懷疑該檢測未開啟。如果檢測到該函數地址不在任何一個模塊內,則會上報異常數據。
為了避免重復上報,該檢測使用report list 6記錄每個異常上報數據。
數據包結構:
struct PacketDxgkrnlInternalFunctionRangeCheck { // 22 is unknown function range check BYTE PacketType; // address of the function PVOID Address; // 64 bytes content of the function BYTE Content[64];};
infinity hook 檢測(report type 23)
首先檢測系統是否可以進行infinity hook,如果可能進行了infinity hook,則會檢測WmipLoggerContext中每一項的GetCpuClock函數地址,如果該函數地址在模塊地址范圍內,并且該地址所在節的權限為executable + non-paged(從磁盤讀取PE文件進行解析),則判定為正常,否則判定為異常,會上報異常數據。
為了避免重復上報,該檢測使用report list 5記錄每個異常上報數據用于去重。
數據包結構:
struct PacketInfinityHookRangeCheck { // 23 is infinity hook range check BYTE PacketType; // address of the function PVOID Address; // 64 bytes content of the function BYTE Content[64];};
系統模塊檢測
遍歷內核中加載的所有模塊,對其進行檢測,但是會跳過以下幾個模塊。
- hal.dll
- clipsp.sys
- CI.dll
- tpm.sys
- ks.sys
- cdd.dll
- TSDDD.dll
- spsys.sys
- atikmpag.sys
在處理win32k模塊時,由于win32k模塊的內存只在csrss中進行了映射,因此需要附加到csrss后再進行檢查。
模塊異常指令檢測(report type 11)
由于該檢測模塊較為混亂,因此逆向分析的不是很清楚,懷疑是在尋找模塊中一些int 3、hook的指令,并將指令所在的頁面上傳到異常鏈表。
其中對dxgkrnl.sys有特殊檢測,懷疑是在檢測gdi hook,原文鏈接:https://secret.club/2019/10/18/kernel_gdi_hook.html
數據包結構:
struct PacketModuleAbnormalInstructionCheck { // 11 is module abnormal instruction check BYTE PacketType; // length of the module name, up to 64 BYTE ModuleNameLength; // length depends on ModuleNameLength CHAR ModuleName[0]; // offset in page ULONG OffsetInPage; // content of the page which contains the abnormal instruction, up to 0x1000 bytes BYTE Content[0];};
模塊IAT hook檢測(report type 19)
通過解析各個模塊的內存中的PE結構,檢查是否存在某個IAT項的函數地址不在任何一個模塊范圍內,如果是則會上報異常數據。
數據包結構:
struct PacketModuleIATHookCheck { // 19 is module IAT hook check BYTE PacketType; // module name // no length is recorded yet ! CHAR ModuleName[0]; // function index in the IAT ULONG FunctionIndex; // offset of the function IAT entry to the module base ULONG EntryOffset; // function in the IAT entry PVOID Function; // content of the function BYTE Content[64];};
隱藏驅動檢測(report type 15)
通過遍歷\\Device目錄,得到所有Device類型的對象,然后遍歷\\Driver和\\FileSystem目錄,得到所有Driver對象,對每個Device對象找到其內部存儲的Driver指針,然后逐一匹配剛才遍歷得到的Driver對象,如果沒有任何一個Driver對象與其匹配,則判定該Device對應的驅動被隱藏了,會上報異常數據。
數據包結構:
struct PacketHiddenDriverCheck { // 15 is hidden driver check BYTE PacketType; // length of the device name BYTE DeviceNameLength; // name of the device whose driver is hidden CHAR DeviceName[0]; // driver name of the hidden driver // length = PacketLength - OtherFieldsLength CHAR DriverName[0];};
PCI設備檢測(report type 26)
通過I/O指令遍歷PCI設備樹,尋找具有指定特征的PCI設備,懷疑是檢測DMA作弊工具。如果找到具有指定特征的PCI設備,則會上報異常數據。
PCI設備檢測實現參考源碼:https://gitlab.freedesktop.org/xorg/lib/libpciaccess/-/blob/master/src/x86_pci.c
UC上也有人提到過該檢測:https://www.unknowncheats.me/forum/anti-cheat-bypass/304545-detecting-dma-hardware-cheats-12.html
(注意:在第二種上報類型中,Info中的Dev貌似被BE的開發者誤寫成了Bus,導致記錄了兩次Bus而沒有記錄Dev,笑)
數據包結構:
struct PacketHiddenDriverCheckType1 { // 26 is pci device check BYTE PacketType; // PCI enumeration info struct { BYTE Bus; BYTE Dev; BYTE Func; } Info; // 4 bytes read from reg VENDOR_ID (0x0) ULONG VendorId; // 4 bytes read from reg PCI_CLASS (0x08) ULONG PciClass; // 1 byte read from reg HDRTYPE (0x0E) BYTE HdrType; // 4 bytes read from reg PCI_SUB_VENDOR_ID (0x2C) ULONG SubVendorId;};
struct PacketHiddenDriverCheckType2 { // 26 is pci device check BYTE PacketType; // PCI enumeration info struct { BYTE Bus; BYTE Dev; BYTE Func; } Info; // 256 bytes read from reg VENDOR_ID (0x0) BYTE VendorId[256];};
Win32k函數指針表檢測
gDxgkInterface和gDxgkWin32kEngInterface是存儲在Win32k中的兩張函數表,作用類似于SSDT,IChooseYou曾將其用于無模塊驅動的通信,https://www.unknowncheats.me/forum/anti-cheat-bypass/335585-communicating-mapped-driver-using.html,故BE對其進行檢測。
由于win32k僅在csrss模塊的地址空間中進行了映射,因此在檢測時需要附加到csrss進程。
gDxgkInterface 地址范圍檢測(report type 20)
對gDxgkInterface 表中的絕大部分函數進行地址范圍檢測(跳過前兩個函數),檢測其地址是否在win32k模塊范圍內。
數據包結構:
struct PacketWin32kRangeCheckType1 { // 20 is win32k gDxgkInterface range check BYTE PacketType; // function index in the gDxgkInterface table ULONG Index; // function address PVOID Function; // 64 bytes of the function BYTE Content[64];};
gDxgkInterface hook檢測(report type 21)
對上述函數進行hook檢測,檢測方式同回調Hook檢測(report type 6),僅report type不同。FunctionType值為函數在表中的下標。
gDxgkWin32kEngInterface 地址范圍檢測(report type 24)
對gDxgkWin32kEngInterface表中的所有函數進行地址范圍檢測,檢測其地址是否在win32k模塊范圍內。
數據包結構:
struct PacketWin32kRangeCheckType2 { // 20 is win32k gDxgkWin32kEngInterface range check BYTE PacketType; // function index in the gDxgkWin32kEngInterface table ULONG Index; // function address PVOID Function; // 64 bytes of the function BYTE Content[64];};
gDxgkWin32kEngInterface hook檢測(report type 25)
對上述函數進行hook檢測,檢測方式同回調Hook檢測(report type 6),僅report type不同。FunctionType值為函數在表中的下標。
ext_ms_win_core_win32k_full_export_l1 地址范圍檢測(report type 33)
該表未導出,因此BE通過特征碼定位的方式獲得該表,通過BRUSHOBJ_hGetColorTransform函數進行定位,在該函數中搜索如下特征碼,addr1即為ext_ms_win_core_win32k_full_export_l1:
mov rax, [addr1]test rax, raxje addr2call qword ptr [addr3]
對該表中的函數逐個檢測地址,查看其是否在win32k和win32kfull模塊的范圍內,如果不在則會上報異常數據。
數據包結構:
struct PacketWin32kRangeCheckType3 { // 33 is win32k ext_ms_win_core_win32k_full_export_l1 range check BYTE PacketType; // function index in the ext_ms_win_core_win32k_full_export_l1 table ULONG Index; // function address PVOID Function; // 64 bytes of the function BYTE Content[64];};
Dxgkrnl 函數指針表檢測
DxgCoreInterface是Dxgkrnl模塊中的一張函數表。可能曾被用作無模塊通信/繪制,或者僅是預防性檢查。
DxgCoreInterface 地址范圍檢測(report type 12)
對DxgCoreInterface表中的所有函數進行地址范圍檢測,檢測其地址是否在win32k模塊范圍內。
數據包結構:
struct PacketDxgkrnlRangeCheck { // 12 is Dxgkrnl DxgCoreInterface range check BYTE PacketType; // function index in the DxgCoreInterface table ULONG Index; // function address PVOID Function; // 64 bytes of the function BYTE Content[64];};
DxgCoreInterface hook檢測(report type 13)
對上述函數進行hook檢測,檢測方式同回調Hook檢測(report type 6),僅report type不同。
HAL 函數指針表檢測
這是https://www.unknowncheats.me/forum/anti-cheat-bypass/335585-communicating-mapped-driver-using.html這篇文章中提到的另一種通信方式,具體的實現方式是hook HalDispatchTable中的函數,因此BE對該表進行檢測。除此之外,BE還發現HalPrivateDispatchTable也可以被hook,因此又額外加入了對該表的檢測。
HalDispatchTable 地址范圍檢測(report type 27)
對HalDispatchTable 表中的所有函數進行地址范圍檢測,檢測其地址是否在ntoskrnl、hal等系統模塊范圍內。
數據包結構:
struct PacketHalDispatchTableRangeCheck { // 27 is HalDispatchTable range check BYTE PacketType; // function index in the HalDispatchTable table ULONG Index; // function address PVOID Function; // 64 bytes of the function BYTE Content[64];};
HalDispatchTable hook檢測(report type 28)
對上述函數進行hook檢測,檢測方式同回調Hook檢測(report type 6),僅report type不同。FunctionType值為函數在表中的下標。
HalPrivateDispatchTable 地址范圍檢測(report type 29)
對HalPrivateDispatchTable 表中的所有函數進行地址范圍檢測,檢測其地址是否在ntoskrnl、hal等系統模塊范圍內。
數據包結構:
struct PacketHalPrivateDispatchTableRangeCheck { // 29 is HalPrivateDispatchTable range check BYTE PacketType; // function index in the HalPrivateDispatchTable table ULONG Index; // function address PVOID Function; // 64 bytes of the function BYTE Content[64];};
HalPrivateDispatchTable hook檢測(report type 30)
對上述函數進行hook檢測,檢測方式同回調Hook檢測(report type 6),僅report type不同。FunctionType值為函數在表中的下標。
派遣函數 hook檢測(report type 5)
在BE加載時,會對系統內的所有模塊進行掃描,對每個驅動的每個派遣函數進行掃描,檢測是否存在hook。
只會檢測頭部64個字節以內的hook(僅以下兩種形式),并且只會跟蹤一次跳轉,不會跟蹤多次跳轉。
- jmp [addr]
- mov rax, imm
- jmp rax
數據包結構:
struct PacketDispatchFunctionHookCheck { // 5 is dispatch function hook check BYTE PacketType; // major number BYTE MajorNumber; // offset of the hook instructions to the function begin BYTE HookOffset; // address of the hook instructions PVOID HookAddress; // 16 bytes of the hook instructions BYTE HookInstructions[16]; // hook function PVOID HookFunction; // 64 bytes of the hook function BYTE Content[64]; // driver name read from the driver object (DriverObject->DriverName) CHAR DriverName[0];};
驅動句柄打開失敗(report type 10)
嘗試打開\\Driver,\\FileSystem目錄下的Driver對象,如果通過ObOpenObjectByName打開失敗,則會上報異常數據。
數據包結構:
struct PacketOpenDriverObjectFailedCheck { // 10 is open driver object failed check BYTE PacketType; // eg:\\Driver\\xxx or \\FileSystem\\xxx CHAR DriverName[0]; // ObOpenObjectByName status NTSTATUS Status;};
游戲進程線程創建檢測(report type 3)
通過線程創建回調監視游戲內創建線程的操作,如果線程啟動地址不在任何一個游戲模塊內,則判定為異常,上報異常數據。猜測該檢測主要用來檢測DLL注入。
數據包結構:
struct PacketGameThreadCreateCheck { // 3 is thread create check BYTE PacketType; // start address of the thread being created PVOID StartAddress;};
總結
1、在所有hook檢測中只檢測了頭部的64字節,因此中部hook或者尾部hook通常可以更好的繞過檢測,并且不要使用過于常規的hook無條件跳轉(jmp [addr] / mov rax, imm jmp rax),請盡情發揮你的想象
2、BE內核驅動會維護內部的進程、驅動、模塊等鏈表,因此如果使用簡單的斷鏈是沒有用的,并且如果隱藏的不好,出現了數據的不一致性,“隱藏”這一行為也會被當做異常數據上報
3、由于win32k、dxgkrnl等驅動可以用于無模塊通信、繪制等用途,并且不受Patch Guard管控,因此BE對其進行了額外的完整性檢查
4、通過kdmapper等工具加載的驅動是重點關注對象,BE內核驅動會檢查各種函數是否是無模塊地址、并且系統線程的起始地址、堆棧也會被檢查六
相關工作
1、BattlEye去虛擬化內核模塊
https://www.unknowncheats.me/forum/anti-cheat-bypass/489381-bedaisy-sys-devirtualized.html
這個帖子給出了一個使用VTIL脫掉VMP殼的BE內核模塊,本次逆向工作就是在這個帖子的基礎之上完成的。
2、NoVmp
https://github.com/can1357/NoVmp
使用VTIL作為內核,實現了給VMP3脫殼。(但是用在最新版的BE驅動上會崩潰)
3、BE內核驅動逆向
https://github.com/dllcrt0/bedaisy-reversal
這個人也做了個開源的BE內核驅動的逆向,但是細節稍有些粗糙,并且不全。
4、BE shellcode
https://github.com/weak1337/BE-Shellcode
這個人做了對BE應用層的一些shellcode的分析,質量很高。
其他
附件是逆向后的文件,感興趣的可以看一看這些檢測具體是怎么實現的。如果發現我哪里分析的有問題,歡迎指出錯誤。