内核补丁保护(三)触发 PG 检查
😄
0 触发PG检查方式
从目前的研究来看,触发 PG 检查的方法有很多种。主要为 DPC 函数执行、系统线程执行、APC 等。
1 DPC 函数触发 PG 检查
1.1 DPC函数触发PG
以下 DPC 函数执行的时候,会触发 PatchGuard 检查执行。前 10 个函数中,会通过触发异常来开始 PatchGuard 检查执行(通过检查参数DeferredContext是否是Canonical地址);而后两个函数是是直接调用 PatchGuard 检查执行。
注意:KiBalanceSetManagerDeferredRoutine
函数仅用于方法 5。
1 | ExpTimerDpcRoutine; |
下面以 ExpTimerDpcRoutine
函数来进行分析。
1 | VOID __fastcall ExpTimerDpcRoutine ( |
该函数开头会校验参数
DeferredContext
是否是 Canonical 地址:- 如果是规范地址或者
0
,则执行正常的 DPC 处理流程。 - 如果不是规范地址,则将会跳转到
loc_14048981C
。
注意: ja 指令不是简单用于比较大小,实际上是通过 rflags.CF 和 rflags.ZF 来判断是否跳转。1
2
3
4
5
6
7.text:000000014035E095 mov rax, rbx // DeferredContext
.text:000000014035E098 sar rax, 2Fh // 规范地址:FFFFFFFF`FFFFFFFF, -1
.text:000000014035E098 // 不规范地址:FFFFFFFF`FFFFFFFE, -2
.text:000000014035E09C mov r14d, 1
.text:000000014035E0A2 add rax, r14
.text:000000014035E0A5 cmp rax, r14 // CF | ZF == 0, jmp
.text:000000014035E0A8 ja loc_14048981C // Non-Canonical- 如果是规范地址或者
然后进入到
__try
块里面。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.text:000000014048981C loc_14048981C:
.text:000000014048981C ; __unwind { // __C_specific_handler ; Non-Canonical
.text:000000014048981C and [rsp+1B8h+var_184], 0
.text:0000000140489821 mov byte ptr [rdi], 0
.text:0000000140489824 mov rax, r15
.text:0000000140489827 shr rax, 8
.text:000000014048982B mov [rdi+20h], rax
.text:000000014048982F mov [rsp+1B8h+var_47], rsi
.text:0000000140489837 mov ecx, esi
.text:0000000140489839 mov rax, rbx
.text:000000014048983C rol rax, cl
.text:000000014048983F mov [rsp+1B8h+var_97], rax
.text:0000000140489847 mov rax, rdi
.text:000000014048984A ror rax, cl
.text:000000014048984D mov [rsp+1B8h+var_4F], rax
.text:0000000140489855 xor [rdi+28h], r15
.text:0000000140489859 xor [rdi+30h], rsi
.text:000000014048985D
.text:000000014048985D loc_14048985D:
.text:000000014048985D
.text:000000014048985D ; __try { // __finally(ExpTimerDpcRoutine$fin$2)
.text:000000014048985D ; __try { // __finally(ExpTimerDpcRoutine$fin$0)
.text:000000014048985D ; __try { // __except at loc_1404898AB
.text:000000014048985D ; __try { // __except at loc_140489868
.text:000000014048985D mov rcx, rbx
.text:0000000140489860 call KiCustomAccessRoutine0
.text:0000000140489865 nop
.text:0000000140489865 ; } // starts at 14048985D可以看到,按照执行流程,会进入到
000000014048985D
的子__try
里面调用KiCustomAccessRoutine0
函数。这里需要对
KiCustomAccessRoutineX
函数和KiCustomRecurseRoutineY
函数进行解释(0 <= X,Y <= 9
)。上述的 11 个 DPC 函数中会调用硬编码好的
KiCustomAccessRoutineX
函数,然后该函数调用对应的KiCustomRecurseRoutineX
函数。进入到KiCustomRecurseRoutineX
函数后,就像进入到”俄罗斯轮盘游戏“(在左轮手枪塞入一颗子弹,然后随意转动转轮并开枪,在一定概率下子弹会被命中),如下图。使用
AccessX
函数进入到Recurse
轮盘。AccessX
函数。函数会
(DeferredContext & 0x3) + 1
,取参数低 2 位并加 1,带入到Recurse
函数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18.text:0000000140402FD0 KiCustomAccessRoutine0 proc near
.text:0000000140402FD0 ; __unwind { // KiCustomAccessHandler0
.text:0000000140402FD0 sub rsp, 28h
.text:0000000140402FD4 mov rdx, rcx
.text:0000000140402FD7 and ecx, 3
.text:0000000140402FDA inc ecx
.text:0000000140402FDC ror rax, cl
.text:0000000140402FDF xor r8, rax
.text:0000000140402FE2 xor r9, rax
.text:0000000140402FE5 xor r10, rax
.text:0000000140402FE8 xor r11, rax
.text:0000000140402FEB xor eax, eax
.text:0000000140402FED call KiCustomRecurseRoutine0
.text:0000000140402FF2 nop
.text:0000000140402FF3 add rsp, 28h
.text:0000000140402FF7 retn
.text:0000000140402FF7 ; } // starts at 140402FD0
.text:0000000140402FF7 KiCustomAccessRoutine0 endpRecurse
函数。在
Recurse
轮盘函数里面如果(DeferredContext & 0x3) + 1
递减为0
时,就会从rdx(DeferredContext)
里读取数据。本身DeferredContext
就是 Non-Canonical 地址,所以这里会出发#GP
异常。1
2
3
4
5
6
7
8
9
10
11
12
13.text:0000000140402FB0 KiCustomRecurseRoutine0 proc near ; CODE XREF: KiCustomRecurseRoutine9+8↑p
.text:0000000140402FB0 ; KiCustomAccessRoutine0+1D↓p
.text:0000000140402FB0 ; DATA XREF: ...
.text:0000000140402FB0 sub rsp, 28h
.text:0000000140402FB4 dec ecx
.text:0000000140402FB6 jz short loc_140402FBD
.text:0000000140402FB8 call KiCustomRecurseRoutine1
.text:0000000140402FBD
.text:0000000140402FBD loc_140402FBD: ; CODE XREF: KiCustomRecurseRoutine0+6↑j
.text:0000000140402FBD mov eax, [rdx]
.text:0000000140402FBF add rsp, 28h
.text:0000000140402FC3 retn
.text:0000000140402FC3 KiCustomRecurseRoutine0 endp注意观察,在上面的
__try
块,对应的__except
在loc_140489868
。在loc_140489868
里就会开始对 PatchGuard 进行解密了。1
2
3
4
5.text:000000014048985D ; __try { // __except at loc_140489868
.text:000000014048985D mov rcx, rbx
.text:0000000140489860 call KiCustomAccessRoutine0
.text:0000000140489865 nop
.text:0000000140489865 ; } // starts at 14048985D
异常处理块。
找到对应的异常处理块如下,查看其异常处理过滤器
ExpTimerDpcRoutine$filt$1
。1
2
3
4
5
6
7
8
9
10
11
12
13.text:0000000140489868 loc_140489868:
.text:0000000140489868 ; __except(ExpTimerDpcRoutine$filt$1) // owned by 14048985D
.text:0000000140489868 and [rsp+1B8h+var_F8], 0
.text:0000000140489871 mov rax, [rsp+1B8h+var_47]
.text:0000000140489879 mov [rsp+1B8h+var_F8], rax
.text:0000000140489881 mov rdx, [rsp+1B8h+var_97]
.text:0000000140489889 mov ecx, eax
.text:000000014048988B ror rdx, cl
.text:000000014048988E mov eax, [rdx]
.text:0000000140489890 mov r14d, 1
.text:0000000140489896 mov rbx, [rsp+1B8h+arg_8]
.text:000000014048989E mov rdi, [rsp+1B8h+arg_0]
.text:000000014048989E ; } // starts at 14048985D接下来会对 PGContext 进行解密。解密代码可能驻留在异常过滤器、异常处理程序或终止处理程序中。定位解密代码的小技巧就是:解密代码中会使用
KiWaitAlways
和KiWaitNever
两个全局变量中的随机值。实际上这两个全局变量是用来解密 DPC 的(在异常过滤函数中是用来解密PGContext),在 PGContext 填充过程中保存了这两个全局变量,但是并没有看到用来加密 PGContext。
1.2 第一层解密
解密一共分为两层:
- 第一层在 DPC 函数中(如
ExpTimerDpcRoutine
函数其中的一个异常处理过滤器)。 - 第一层解密完成后就会调用 PGContext 中加密的
CmpAppendDllSection
函数,在CmpAppendDllSection
函数中完成第二层解密。
在上一节 1.1 的异常过滤器 ExpTimerDpcRoutine$filt$1
中,会进行第一层解密。
如下代码,会将解密后的
&pg_entry
保存在[rbp+58h]
。紧接着过滤器会对pg_entry
进行验证。这里的
pg_entry
实际上就是PGContext.CmpAppendDllSection
,地址是随机的。1
2
3
4
5
6
7
8
9
10
11
12
13
14.text:00000001404140A0 ExpTimerDpcRoutine$filt$1:
.text:00000001404140A0 ; __except filter // owned by 14048985D
.text:00000001404140A0 push rbp
.text:00000001404140A2 sub rsp, 30h
...
.text:00000001404141B1 mov rax, [rbp+0E8h]
.text:00000001404141B8 xor rcx, rax
.text:00000001404141BB mov [rbp+98h], rcx
.text:00000001404141C2 mov rax, [rbp+98h]
.text:00000001404141C9 mov rcx, 0FFFF800000000000h
.text:00000001404141D3 or rax, rcx
.text:00000001404141D6 mov [rbp+98h], rax
.text:00000001404141DD mov rax, [rbp+98h]
.text:00000001404141E4 mov [rbp+58h], rax // 计算得到 [rbp+58h],里面保存 pg_entry 的地址由上一步可知, 解密后的
&pg_entry
保存在[rbp+58h]
中。接下来会将函数CmpAppendDllSection
前 8 字节写入pg_entry
,然后进行异或。紧接着再将函数CmpAppendDllSection
前 4 字节(0x1131482E
)重写入pg_entry
,最后调用pg_entry
。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.text:00000001404144AA mov rax, 85131481131482Eh // CmpAppendDllSection 前 8 字节
.text:00000001404144B4 mov [rbp+48h], rax
...
.text:00000001404144DE mov qword ptr [rbp+48h], 1131482Eh // CmpAppendDllSection 前4字节
.text:00000001404144E6 mov rax, [rbp+48h] // rax = rcx = [rbp + 48h]
.text:00000001404144EA mov rcx, [rbp+48h] // rax = rcx = [rbp + 48h]
.text:00000001404144EE mov [rbp+48h], rcx // rax = rcx = [rbp + 48h]
.text:00000001404144F2 mov rcx, [rbp+48h] // rax = rcx = [rbp + 48h]
.text:00000001404144F6 mov rax, [rbp+58h] // rax = &pg_entry
.text:00000001404144FA mov [rax], ecx // *(QWORD*)&pg_entry = 0x1131482E;
.text:00000001404144FC mov eax, [rbp+8Ch] // [rbp+8Ch] == 0
.text:0000000140414502 test al, 1
.text:0000000140414504 jz short loc_14041451C
...
.text:000000014041451C loc_14041451C:
.text:000000014041451C mov rax, [rbp+58h]
.text:0000000140414520 xor r9d, r9d
.text:0000000140414523 xor r8d, r8d
.text:0000000140414526 mov rdx, [rbp+50h]
.text:000000014041452A mov rcx, [rbp+58h] // rax = &pg_entry
.text:000000014041452A // rcx = &pg_entry
.text:000000014041452A // rdx = [rbp + 0x50]
.text:000000014041452A // r8 = 0
.text:000000014041452A // r9 = 0
.text:000000014041452E call _guard_dispatch_icall // jmp rax
.text:000000014041452E // rax = &PGContext.CmpAppendDllSection
.text:0000000140414533 nop可以看到这里对
&pg_entry
地址前 4 字节重写,然后就跳去指向pg_entry
。这个
pg_entry
就是从CmpAppendDllSection
拷贝到 PGContext 中的。下面将会执行该函数。
参考内容:
1.3 第二层解密CmpAppendDllSection
从上一篇内核补丁保护(二)初始化 3.1我们知道,CmpAppendDllSection
函数长度为 0xC8
字节,但是最后 4 字节是用 0x90(nop)
填充的 。从看雪文章《攻破 Window AMD64 平台的 PatchGuard - 搜索加密的 PatchGuard Context》知道,CmpAppendDllSection + 0xC4
保存着 PGContext_FirstSec 的长度。
注意:是解密得到 CmpAppendDllSection
函数,因为 PGContext 是加密的。
解密执行 CmpAppendDllSection
分为两部分:
- 上半部分是自解密得到
CmpAppendDllSection
; - 下半部分是解密得出
PatchGuardEntryPoint
(Check PG 的核心函数)。
上半部分
上面已经分析到,pg_entry
的前 8 字节已经提前重写好,然后就跳转到 pg_entry
开始执行。
注意:如下图,此时的 pg_entry
函数仅有前 8 个字节,解密的秘钥存放在 rdx 寄存器中。 当第一条指令(4字节长度)执行结束后会解密出 8 字节数据,并写回到当前地址。这说明两点:
- 这是自解密代码,下面即将要执行的代码都会提前解密好,秘钥存放在
rdx
寄存器中。 - 页面必须具备可读、可写、可执行,否则会发生异常。
解密结果为:0xBC541CD31131482E ^ 0xB4052D9B560DCCFC = 0x08513148473C84D2
。可以看到高 4 字节和 CmpAppendDllSection
前 4-7 字节完全符合。
CmpAppendDllSection
上半部分代码:
1 | INIT:0000000140A36190 CmpAppendDllSection proc near |
说明:PG 检查时并不是直接执行复制到 PGContext 中的 CmpAppendDllSection
函数,而是需要执行的时候进行自解密得到 CmpAppendDllSection
函数。
下半部分
CmpAppendDllSection
下半部分代码:
1 | INIT:0000000140A361F8 sub rcx, 78h |
注意:PGContext_FirstSec
表示 PGContext 的第一部分,并不是完整的 PGContext。
从上面代码可以看到,解密完成后,实际上 CmpAppendDllSection + 0xC4
保存着用来计算 PGContext_FirstSec 总大小的索引值 index
。
即:PGContext_FirstSec.Length = Index*8+0xC0
。
- LOOP 循环。这里是将整个 PGContext_FirstSec 进行解密,从高地址向低地址进行解密。注意:这里每一轮解密都会使用
btc
修改密钥——btc rax, rax
。 - 调用
PatchGuardEntryPoint
函数。PatchGuardEntryPoint
函数是 Check PG 的核心函数。位于PGContext_FirstSec + 0x7E8
。
PatchGuardEntryPoint
函数位于 INITKDBG
区段。
2 动态获取Pg_entry
从《内核补丁保护(一)初始化》知道,系统初始化时如果检测到有内核调试器存在就不会初始化 PGContext。所以要使被调试系统在调试状态下还能启用 PatchGuard 的方法:
在被调试系统开机时,先不打开 Windbg 调试器,等被调试设备启动开始转圈后(这时候大概PG已经完成初始化,并且BootDebug断点没有触发),然后立刻挂上调试器。这样就能保证在启用 PG 的情况下,还能存在调试器。记得保存快照。
借助周壑 x64内核研究08_PatchGuard(2) 的方法触发 PatchGuard。方法是:在系统非分页内存池找到大小大于
0x8000 + 0xAA8
的内存块 PGContext(上一篇文章已经分析了,如下图),将其 PTE 的 NX 位置 1 触发异常动态得到pg_entry
函数地址。异常成功触发后(得到
pg_entry
函数地址0xFFFF858BB8D0251D
),但是在 Windows 19044 下,给该地址下硬件断点ba
不会被命中(猜测在这之前 DR7 寄存器已被清空),如果下软件断点bp
则直接异常蓝屏。尝试过以下两种方法,但是只有方法二成功。
- 方法一:用驱动下硬件断点。在驱动中写个死循环给
0xFFFF858BB8D0251D
下硬件断点,但是仅下一次断点也会触发异常蓝屏。- 方法二:HOOK DPC 函数。分析上面获取
pg_entry
函数地址触发异常时,找到触发 PatchGuard 的位置,发现是 DPC 函数nt!ExpCenturyDpcRoutine
。
方法一下硬件断点的代码:
1 | typedef NTSTATUS (*PNtGetThreadContext)(IN HANDLE, IN OUT PCONTEXT); |
使用周壑 x64内核研究08_PatchGuard(2) 的方法触发 PatchGuard。得到的堆栈如下:
1 | kd> kv |
从 #1a
号堆栈可以看到,PatchGuard 检查时在 DPC 函数 nt!ExpCenturyDpcRoutine
执行时,调用 nt!KiCustomAccessRoutine9 —> .. nt!KiCustomRecurseRoutine2
触发的异常。
然后使用上一篇的定位小技巧,静态分析 nt!ExpCenturyDpcRoutine
的每一个异常过滤器或 __finally
找到 KiWaitNever/KiWaitAlways
。发现在 __finally(ExpCenturyDpcRoutine$fin$0)
,对应上 #09
号堆栈。此时也能看到 0xffff858bb8d0251d
就是 pg_entry
。所以方法二就是 HOOK ExpCenturyDpcRoutine$fin$0
函数。
3 HOOK ExpCenturyDpcRoutine$fin$0
3.1 寻找 HOOK 点
在 x64 下,不能使用 jmp/call 立即数地址
,可以修改成 jmp/call 通用寄存器
。
如下图,ShellCode 一共 12 字节,分析了一下 ExpCenturyDpcRoutine$fin$0
函数。
观察 ExpCenturyDpcRoutine$fin$0
函数找一个 12 字节,尽量不要找有 jcc/call
的位置。
1 | .text:00000001404129AF ExpCenturyDpcRoutine$fin$0: |
如上找到如下 12 字节。
1 | .text:00000001404129D4 48 83 65 78 00 and qword ptr [rbp+78h], 0 |
注意:这里的要跳转的目标地址是动态的,且驱动中无法关闭动态随机基址(项目属性—链接器—随机基址、固定基址),所以可以在驱动设置断点然后在 Windbg 中手动修改目标地址。
恢复快照后,得到 HOOK 点的地址如下:
1 | kd> u fffff8047462819b |
3.2 HOOK 代码
Main.c
1 |
|
HookFuck.asm
1 | option casemap :none |
驱动成功运行后,可以看到目标地址已经被 ShellCode HOOK 了:
1 | kd> u fffff8047462819b |
3.3 解密得到CmpAppendDllSection
命中ShellCode
PatchGuard 检查时,当 ShellCode 被命中后,HookFuck
函数就会被执行。接下来单步调试,根据 1.2 章节的分析,解密得到 Pg_entry
入口地址和 CmpAppendDllSection
的解密秘钥。
单步到如下指令,此时 rax = rcx = r11 = &Pg_entry = 0xffff858bb8d0251d
,密钥 rdx = 0xdca6e531adcf3a55
。
1 | fffff804`746283d8 498bcb mov rcx,r11 |
在
0xffff858bb8d0251d
下硬件断点(不要下软件断点)。1
2
3kd> ba e1 ffff858bb8d0251d
kd> bl
0 e Disable Clear ffff858b`b8d0251d e 1 0001 (0001)继续运行后硬件断点即被命中。
查看
CmpAppendDllSection
解密上半部分。单步运行后,
Pg_entry
被命中,下面就开始自解密。每次自解密并写入 8 字节,而每次指令执行 4 字节,所以会提前解密好指令序列。
下半部分(1)。上半部分解密结束后,下半部分会迎来一个循环。循环之前,首先会取
CmpAppendDllSection + 0xC4
,即为PGContext_FirstSec.Index
。此时的
PGContext_FirstSec.Index = 0x3914
。此次PGContext_FirstSec.Length = Index*8+0xC0 == 0x1C960
。1
2
3
4
5kd> r rcx
rcx=0000000000003914
...
ffff858b`b8d0259c 483184cac0000000 xor qword ptr [rdx+rcx*8+0C0h],rax ds:002b:ffff858b`b8d1ee7d=dca6e531adcf3a55
ffff858b`b8d025a4 48d3c8 ror rax,cl注意:由于 PGContext 所处的内存一般会在两分半钟左右刷新一次,也就是这次 PatchGuard 检查没问题之后,会将完整的 PGContext 拷贝至新分配的非分页内存中,并且 PGContext 大小和基地址都是动态变化的。
此时将解密前的
PGContext_FirstSec
Dump 到文件:1
2kd> .writemem c:\Dump\dump ffff858bb8d0251d L?1c96+1
Writing 1c961 bytes..........................................................解密的 LOOP 循环。这里是将 PGContext_FirstSec 进行解密,从高地址向低地址进行解密。
注意:
- 这里每一轮解密都会使用 btc 修改密钥——
ror rax,cl; btc rax, rax
(LOOP中RCX-=1)。 - 第一轮解密的密钥和一开始解密
CmpAppendDllSection
的密钥是相同的,都是0xdca6e531adcf3a55
。
- 这里每一轮解密都会使用 btc 修改密钥——
下半部分的 LOOP 循环解密完成后,再 Dump 一次解密后的内存。
可以看到,LOOP 循环解密后,
CmpAppendDllSection
的代码又被完整解密出来了(但是其他内容是加密的)。1
2
3
4
5
6
7
8kd> u ffff858bb8d0251d l30
ffff858b`b8d0251d 2e483111 xor qword ptr cs:[rcx],rdx
ffff858b`b8d02521 48315108 xor qword ptr [rcx+8],rdx
ffff858b`b8d02525 48315110 xor qword ptr [rcx+10h],rdx
ffff858b`b8d02529 48315118 xor qword ptr [rcx+18h],rdx
ffff858b`b8d0252d 48315120 xor qword ptr [rcx+20h],rdx
ffff858b`b8d02531 48315128 xor qword ptr [rcx+28h],rdx
...下半部分(2)。这部分主要就是调用
PatchGuardEntryPoint
函数。1
2
3
4
5
6ffff858b`b8d025ab e2ef loop ffff858b`b8d0259c
ffff858b`b8d025ad 8b82e8070000 mov eax,dword ptr [rdx+7E8h] ds:002b:ffff858b`b8d02d05=0001a371
ffff858b`b8d025b3 4803c2 add rax,rdx
ffff858b`b8d025b6 4883ec28 sub rsp,28h
ffff858b`b8d025ba ffd0 call rax // PatchGuardEntryPoint
...调用
PatchGuardEntryPoint
函数前的寄存器状态1
2
3
4
5
6
7
8
9
10kd> r
rax=ffff858bb8d1c88e rbx=000000000000392d rcx=0000000000000000
rdx=ffff858bb8d0251d rsi=0000000000000001 rdi=000000000000392d
rip=ffff858bb8d025b6 rsp=fffff804770b7908 rbp=fffff804770a3770
r8=0000000000000000 r9=0000000000000000 r10=ffff858bb8d0251d
r11=0000000000000000 r12=fffff804770a3770 r13=fffff804770a3448
r14=97d1c8152fd921c5 r15=fffff80474215000
iopl=0 nv up ei ng nz na po nc
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00000286
ffff858b`b8d025b6 4883ec28 sub rsp,28h堆栈情况如下:
1
2
3
4
5
6
7
8
9kd> dq rsp
fffff804`770b7908 fffff804`746283e3 00000000`00000000
fffff804`770b7918 00000000`00000000 fffff804`742eb2f0
fffff804`770b7928 fffff804`770b7fd0 fffff804`770b8210
fffff804`770b7938 fffff804`74555895 fffff804`770b7fd0
fffff804`770b7948 00000000`00000000 00000000`0032c58e
fffff804`770b7958 00000000`0032c58b fffff804`74294878
fffff804`770b7968 fffff804`745e3441 fffff804`00000003
fffff804`770b7978 fffff804`770a3700 fffff804`7709e000
4 PatchGuardEntryPoint 函数分析
我们主要关注这个函数做的三件事:
清空
DR7
寄存器,屏蔽硬件断点。IDTR.Base = [&pg_entry + 0x8E0]
获取新的 IDTR 寄存器基址,IDTR.Limit = 0x12F
。- 拷贝
CmpAppendDllSection
前 64 字节至IDTR.Base + 0x10
的位置。 - 清空
DR7
寄存器,屏蔽硬件断点。
验证多个关键函数是否被修改过,如
ExpWorkerThread
。如果这些关键函数已经被修改过,则会调用
KeBugCheck
触发蓝屏。如果检查结果成功,则会向初始化一个工作项
IO_WORKITEM
。该工作项关联的回调函数WorkerRoutine
是从以下三个函数之一。- KiMachineCheckControl(从该数组中选择一个),如果是方法 ⑦ 初始化的 PG,则传给该函数的参数是
&PGcontext
。 - FsRtlUninitializeSmallMcb,传给该函数的参数也是
&PGcontext
。(多数情况下将挑选该函数) - sub_140xxxxxx,该函数目前尚不明确。
- KiMachineCheckControl(从该数组中选择一个),如果是方法 ⑦ 初始化的 PG,则传给该函数的参数是
初始化一个工作项
IO_WORKITEM
。
1 | v2 = a2; // a1 = 0(循环结束条件) |
Windbg 一些小技巧:
1 | dt _TEB -ny LastError; |
5 执行 WorkItem
下面回到 CmpAppendDllSection
函数继续分析。上一小节已经分析了 PatchGuardEntryPoint
函数,目前代码执行到地址 INIT:0000000140A3622F
。
下面跳转 r8
就是去将工作项插入队列(ExQueueWorkltem/IoQueueWorkItem
),然后让 System
进程的线程池中的某个工作线程去执行回调例程。
1 | INIT:0000000140A36220 loc_140A36220: |
6 FsRtlUninitializeSmallMcb
如上两节分析,在函数 PatchGuardEntryPoint
中初始化一个工作项 IO_WORKITEM
,该 WorkItem 回调例程是三选一,大概率情况下一般回调例程会是 FsRtlUninitializeSmallMcb
函数。然后 jmp r8
去插入该工作项(ExQueueWorkltem/IoQueueWorkItem
)。
在 PatchGuardEntryPoint
函数中会对部分重要的函数进行完整性检查,但是对 PGContext 内容的完整检查是由 FsRtlUninitializeSmallMcb
调用 FsRtlMdlReadCompleteDevEx
函数完成的。这些完整性检查包括核心驱动的 IAT、GDT、IDT(包括中断向量表)、SSDT、CRx、MSR寄存器、内核全局变量、内核全局指针、进程模块链表、内核堆栈、对象类型、Local APIC、第三方通知回调、某些配置和 Flags、KPP Self 等。
FsRtlUninitializeSmallMcb
、FsRtlMdlReadCompleteDevEx
这些函数原型都在 INITKDBG
节中。
7 DPC 方式检查过程
8 其他触发PG检查的方式
8.1 系统线程方式
使用系统线程触发 PG 检查方式的概率不是很高。
在 PG_KiInitializePatchGuardContext
函数中使用方法三初始化 PGContext
时,在 PG_KiInitializePatchGuardContext
函数中调用 Pg_InitMethod3SystemThread
函数来初始化一个系统线程。
通过系统线程来触发 PG 检查适用于方法三初始的 PGContext。
初始化过程如下:
1 | ExpLicenseWatchInitWorker --> PG_KiFilterFiberContext(pKiServiceTablesLocked) // 参数 KPRCB[0].HalReserved[6] |
在 PG_KiInitializePatchGuardContext
调用 Pg_InitMethod3SystemThread
函数,在函数 Pg_InitMethod3SystemThread
中使用 KI_FILTER_FIBER_PARAM.pPsCreatesystemThread
来创建一个系统线程,线程回调函数为 KI_FILTER_FIBER_PARAM.Pg_Method3stubTocheckRoutine_sub_1405B9FB0
。
通过分析线程回调函数 Pg_Method3stubTocheckRoutine_sub_1405B9FB0
:
在
Pg_InitMethod3SystemThread
触发异常,在异常处理中调用KI_FILTER_FIBER_PARAM.pPsCreatesystemThread
创建系统线程。线程回调函数的参数可以将其命名为
pg_StartContext
:1
2
3
4
5
6
7struct pg_StartContext
{
ULONG64 pEvent_0x00; // a pointer to the event
ULONG64 bRandom_ShouldRunKeRundownApcQueues_0x08; // set at 0x1408A970B
ULONG64 unknown_0x10;
KEVENT event_0x18;
};在线程回调函数中
KeWaitForSingleObject(pEvent_0x00, 0, 0, 0, 0)
,等待没有设置超时时间,而该事件是在PG_KiInitializePatchGuardContext
结束时通知的。线程回调函数等待信号之后,就开始进行几乎类似于 DPC 触发 PG Check 的解密过程
第一阶段使用到
KiWaitAlways
、KiWaitNever
参与解密,第二阶段就是CmpAppendDlSection
自解密。一旦验证结束,就会调用
KeDelayExecutionThread/KeWaitForSingleObject
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20if ( v32 )
{
do
{
if ( v35 )
KeDelayExecutionThread(0, 0, &Interval);
else
KeWaitForSingleObject(v4, 0, 0, 0, &Interval);
v31 = *v36;
}
while ( !*v36 );
}
v34 = *(_QWORD *)(v31 + 32);
v9 = v8 ^ v31;
*v36 = 0i64;
v10 = v8 ^ v34;
if ( v35 )
KeDelayExecutionThread(0, 0, &Interval);
else
KeWaitForSingleObject(v4, 0, 0, 0, &Interval);这里的
Interval
等待时间约为2 min~ 2 min 10 seconds
。1
2
3
4
5v2 = ExGenRandom(1i64);
v32 = *(_QWORD *)(v1 + 8) == 1i64;
Interval.QuadPart = -1200000000i64
- (v2
- 100000000 * ((unsigned __int64)(v2 * (unsigned __int128)0xABCC77118461CEFDui64 >> 64) >> 26));
8.2 内核 APC 方式
使用 PsEnumProcessThreads
来枚举 System
系统进程中的系统线程,如果哪一个线程回调函数等于 PopIrpWorkerControl
,初始化则将该线程 ETHREAD
保存至 PGContext(将APC插入到该线程),然后将 ETHREAD
存放在该 KAPC
结构中。即 ETHREAD.StartAddress = PopIrpWorkerControl
。
该 APC 的 KernelRoutine = KiDispatchCallout
,也就是说 KiDispatchCallout
为 APC 回调函数(PG Check 函数)。
实际上 System
进程中有好多个这样的线程:
1 | kd> !process 0 0 |
APC 回调函数中解密类似于 DPC 触发 PG Check 的解密过程:第一阶段使用到 KiWaitAlways
、KiWaitNever
参与解密,第二阶段就是 CmpAppendDlSection
自解密。
8.3 全局变量(通知回调)
- b-x TV » callback, first time linking PatchGuard to mssecflt.sys
- https://github.com/0xcpu/ExecutiveCallbackObjects/blob/b04c7cd396efbf0ab57c2c519c4d93690ad08843/542875F90F9B47F497B64BA219CACF69/README.md
- https://shhoya.github.io/windows_pginit.html
- https://github.com/zhuhuibeishadiao/PatchGuardResearch