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 数据结构如下:
1
2
3
4
5
struct AbnormalListItem {
/
/
because nobody writes to report
list
0
, so some parts of the structure
is
unknown
BYTE Unknown[
10
];
BYTE Content[
64
];
};
通过IrpWrite上传的数据包如下:
1
2
3
4
5
6
7
8
9
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中
1
2
3
4
5
6
struct PatternBlackListItemType0 {
/
/
pattern
in
black
list
up to
32
bytes
BYTE Pattern[
32
];
/
/
length up to
32
ULONG Length;
};
在检测线程启动时,会向g_PatternBlackList添加一个9字节长度的硬编码的特征(看起来像是有关ROP的一些特征?不太清楚。)
1
48
81
C4
80
01
00
00
5F
C3
对应amd64汇编
回调黑名单特征(upload type 1)该检测针对的是进程、线程的前置、后置回调,注册表回调,映像加载回调,对这些函数的头部64个字节进行特征检测。
1
2
3
4
5
6
7
8
9
10
11
12
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驱动是否被篡改。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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该函数,并对该函数的地址范围进行检查。
1
2
3
4
5
6
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会对自身的驱动的关键部位进行检查,检查是否被篡改。如果出现异常则会上报。
数据包结构:
1
2
3
4
5
6
7
8
9
10
11
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中,方便在后续过程中检查是否存在黑名单特征。
数据包结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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等工具加载的模块。
数据包结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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,待进一步进行黑名单检查。
数据包结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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时才会上报异常数据,结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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)检测回调地址是否在某个内核模块的范围内,如果不在任何一个模块的地址范围内,则上报异常。
数据包结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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,待进一步检测黑名单特征。
数据包结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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"对象?)
数据包结构:
1
2
3
4
5
6
7
8
9
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,从而达到检测回调是否被通过某些手段摘除,无法正常工作。
如果回调无法正常工作,则会上报一次异常数据(不会重复上报)。
数据包结构:
1
2
3
4
5
6
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对应的派遣函数是否被修改,如果被修改则会上传到异常链表,否则不会有额外操作。
数据包结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
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不进行加密,不知道为什么要这么做)
数据包结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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)检测回调地址是否在某个内核模块的范围内,如果不在任何一个模块的地址范围内,则上报异常。
数据包结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
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时会上报异常数据,结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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记录每个异常上报数据。
数据包结构:
1
2
3
4
5
6
7
8
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记录每个异常上报数据用于去重。
数据包结构:
1
2
3
4
5
6
7
8
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
数据包结构:
1
2
3
4
5
6
7
8
9
10
11
12
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项的函数地址不在任何一个模块范围内,如果是则会上报异常数据。
数据包结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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对应的驱动被隐藏了,会上报异常数据。
数据包结构:
1
2
3
4
5
6
7
8
9
10
11
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,笑)
数据包结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
};
1
2
3
4
5
6
7
8
9
10
11
12
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模块范围内。
数据包结构:
1
2
3
4
5
6
7
8
9
10
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模块范围内。
数据包结构:
1
2
3
4
5
6
7
8
9
10
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:
1
2
3
4
mov rax, [addr1]
test rax, rax
je addr2
call qword ptr [addr3]
对该表中的函数逐个检测地址,查看其是否在win32k和win32kfull模块的范围内,如果不在则会上报异常数据。
数据包结构:
1
2
3
4
5
6
7
8
9
10
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模块范围内。
数据包结构:
1
2
3
4
5
6
7
8
9
10
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等系统模块范围内。
数据包结构:
1
2
3
4
5
6
7
8
9
10
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等系统模块范围内。
数据包结构:
1
2
3
4
5
6
7
8
9
10
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
数据包结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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打开失败,则会上报异常数据。
数据包结构:
1
2
3
4
5
6
7
8
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注入。
数据包结构:
1
2
3
4
5
6
struct PacketGameThreadCreateCheck {
/
/
3
is
thread create check
BYTE PacketType;
/
/
start address of the thread being created
PVOID StartAddress;
};
总结
在所有hook检测中只检测了头部的64字节,因此中部hook或者尾部hook通常可以更好的绕过检测,并且不要使用过于常规的hook无条件跳转(jmp [addr] / mov rax, imm jmp rax),请尽情发挥你的想象
BE内核驱动会维护内部的进程、驱动、模块等链表,因此如果使用简单的断链是没有用的,并且如果隐藏的不好,出现了数据的不一致性,“隐藏”这一行为也会被当做异常数据上报
由于win32k、dxgkrnl等驱动可以用于无模块通信、绘制等用途,并且不受Patch Guard管控,因此BE对其进行了额外的完整性检查
通过kdmapper等工具加载的驱动是重点关注对象,BE内核驱动会检查各种函数是否是无模块地址、并且系统线程的起始地址、堆栈也会被检查
相关工作
BattlEye去虚拟化内核模块https://www.unknowncheats.me/forum/anti-cheat-bypass/489381-bedaisy-sys-devirtualized.html 这个帖子给出了一个使用VTIL脱掉VMP壳的BE内核模块,本次逆向工作就是在这个帖子的基础之上完成的。
NoVmphttps://github.com/can1357/NoVmp 使用VTIL作为内核,实现了给VMP3脱壳。(但是用在最新版的BE驱动上会崩溃)
BE内核驱动逆向https://github.com/dllcrt0/bedaisy-reversal 这个人也做了个开源的BE内核驱动的逆向,但是细节稍有些粗糙,并且不全
BE shellcodehttps://github.com/weak1337/BE-Shellcode 这个人做了对BE应用层的一些shellcode的分析,质量很高
其他附件是逆向后的文件,感兴趣的可以看一看这些检测具体是怎么实现的。如果发现我哪里分析的有问题,欢迎指出错误。
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界
上传的附件: