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的分析,质量很高。
其他
附件是逆向后的文件,感兴趣的可以看一看这些检测具体是怎么实现的。如果发现我哪里分析的有问题,欢迎指出错误。
