环境
客户机: VMWare16 + Win10.17763 x64, 4核,8G
宿主机: Win10
单步调试异常
启动应用,EAC驱动加载,首先会主动触发一个单步调试异常。
Single step exception - code 80000004 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
EasyAntiCheat+0x4b0f82:
fffff800`0e750f82 489d popfq
1: kd> uf fffff800`0e750f6c
EasyAntiCheat+0x4b0f6c:
fffff800`0e750f6c 4055 push rbp
fffff800`0e750f6e 488bec mov rbp,rsp
fffff800`0e750f71 32d2 xor dl,dl
fffff800`0e750f73 489c pushfq
fffff800`0e750f75 59 pop rcx
fffff800`0e750f76 488bc1 mov rax,rcx
fffff800`0e750f79 480fbae808 bts rax,8 ;置EFLAGS:TF标志位
fffff800`0e750f7e 50 push rax
fffff800`0e750f7f 489d popfq
fffff800`0e750f81 51 push rcx
fffff800`0e750f82 489d popfq
fffff800`0e750f84 eb02 jmp EasyAntiCheat+0x4b0f88 (fffff800`0e750f88) Branch
EasyAntiCheat+0x4b0f88:
fffff800`0e750f88 8ac2 mov al,dl
fffff800`0e750f8a 488be5 mov rsp,rbp
fffff800`0e750f8d 5d pop rbp
fffff800`0e750f8e c3 ret
反调试很常见的一种调试器检测方法,调试器忽略此次异常即可(gn)。
KdEnterDebugger异常
gn以后,系统卡死无反应并且无法中断到调试器。挂起系统,使用vmss2core转储dump(VMWare官网下载vmss2core)。
使用WinDBG加载dump,查看系统挂起原因。
3: kd> ~0s
0: kd> k
# Child-SP RetAddr Call Site
00 fffff804`46686b80 fffff804`44134084 nt!KiCheckStall+0x7b
01 fffff804`46686bb0 fffff804`4412a3e2 nt!KiFreezeTargetExecution+0x1b8
02 fffff804`46686cb0 fffff804`4412a647 nt!KiCheckForFreezeExecution+0x2a
03 fffff804`46686ce0 fffff804`4405be02 nt!KiProcessNMI+0x57
04 fffff804`46686d30 fffff804`4405bbc3 nt!KxNmiInterrupt+0x82
05 fffff804`46686e70 fffff804`4418397c nt!KiNmiInterrupt+0x203
06 fffff804`466747a0 fffff804`43fcb7cb nt!PpmIdleGuestExecute+0x1c
07 fffff804`466747e0 fffff804`43fcaf7f nt!PpmIdleExecuteTransition+0x6bb
08 fffff804`46674b00 fffff804`4405452c nt!PoIdle+0x33f
09 fffff804`46674c60 00000000`00000000 nt!KiIdleLoop+0x2c
0: kd> ~1s
1: kd> k
# Child-SP RetAddr Call Site
00 ffff9780`ba6f3c80 fffff804`44134084 nt!KiCheckStall+0x7b
01 ffff9780`ba6f3cb0 fffff804`4412a3e2 nt!KiFreezeTargetExecution+0x1b8
02 ffff9780`ba6f3db0 fffff804`4412a647 nt!KiCheckForFreezeExecution+0x2a
03 ffff9780`ba6f3de0 fffff804`4405be02 nt!KiProcessNMI+0x57
04 ffff9780`ba6f3e30 fffff804`4405bbc3 nt!KxNmiInterrupt+0x82
05 ffff9780`ba6f3f70 fffff804`4418397c nt!KiNmiInterrupt+0x203
06 fffff88c`110297a0 fffff804`43fcb7cb nt!PpmIdleGuestExecute+0x1c
07 fffff88c`110297e0 fffff804`43fcaf7f nt!PpmIdleExecuteTransition+0x6bb
08 fffff88c`11029b00 fffff804`4405452c nt!PoIdle+0x33f
09 fffff88c`11029c60 00000000`00000000 nt!KiIdleLoop+0x2c
1: kd> ~2
2: kd> k
# Child-SP RetAddr Call Site
00 ffff9780`ba798c80 fffff804`44134084 nt!KiCheckStall+0x7b
01 ffff9780`ba798cb0 fffff804`4412a3e2 nt!KiFreezeTargetExecution+0x1b8
02 ffff9780`ba798db0 fffff804`4412a647 nt!KiCheckForFreezeExecution+0x2a
03 ffff9780`ba798de0 fffff804`4405be02 nt!KiProcessNMI+0x57
04 ffff9780`ba798e30 fffff804`4405bbc3 nt!KxNmiInterrupt+0x82
05 ffff9780`ba798f70 fffff804`4418397c nt!KiNmiInterrupt+0x203
06 fffff88c`110377a0 fffff804`43fcb7cb nt!PpmIdleGuestExecute+0x1c
07 fffff88c`110377e0 fffff804`43fcaf7f nt!PpmIdleExecuteTransition+0x6bb
08 fffff88c`11037b00 fffff804`4405452c nt!PoIdle+0x33f
09 fffff88c`11037c60 00000000`00000000 nt!KiIdleLoop+0x2c
2: kd> ~3
3: kd> k
# Child-SP RetAddr Call Site
00 ffff9780`ba1ead80 fffff804`4413386b nt!KxTryToAcquireSpinLock+0x6a
01 ffff9780`ba1eadb0 fffff804`447b3da4 nt!KeFreezeExecution+0xb3
02 ffff9780`ba1eaee0 fffff804`441295f1 nt!KdEnterDebugger+0x64
03 ffff9780`ba1eaf10 fffff804`447b7665 nt!KdpReport+0x71
04 ffff9780`ba1eaf50 fffff804`43f108f8 nt!KdpTrap+0x14d
05 ffff9780`ba1eafa0 fffff804`43f10565 nt!KdTrap+0x2c
06 ffff9780`ba1eafe0 fffff804`44062442 nt!KiDispatchException+0x135
07 ffff9780`ba1eb690 fffff804`4405b988 nt!KiExceptionDispatch+0xc2
08 ffff9780`ba1eb870 fffff804`447b3e7b nt!KxDebugTrapOrFault+0x3c8
09 ffff9780`ba1eba00 fffff804`441295f1 nt!KdEnterDebugger+0x13b
0a ffff9780`ba1eba30 fffff804`447b7665 nt!KdpReport+0x71
0b ffff9780`ba1eba70 fffff804`43f108f8 nt!KdpTrap+0x14d
0c ffff9780`ba1ebac0 fffff804`43f10565 nt!KdTrap+0x2c
0d ffff9780`ba1ebb00 fffff804`44062442 nt!KiDispatchException+0x135
0e ffff9780`ba1ec1b0 fffff804`4405b988 nt!KiExceptionDispatch+0xc2
0f ffff9780`ba1ec390 fffff804`447b3e7b nt!KxDebugTrapOrFault+0x3c8
10 ffff9780`ba1ec520 fffff804`441295f1 nt!KdEnterDebugger+0x13b
11 ffff9780`ba1ec550 fffff804`447b7665 nt!KdpReport+0x71
12 ffff9780`ba1ec590 fffff804`43f108f8 nt!KdpTrap+0x14d
13 ffff9780`ba1ec5e0 fffff804`43f10565 nt!KdTrap+0x2c
...
CPU0-2未见异常,CPU3看起来进入了死循环,继续查看:
3: kd> kv
# Child-SP RetAddr : Args to Child : Call Site
00 ffff9780`ba1ead80 fffff804`4413386b : 00000000`0007a120 ffff9780`ba1eb000 00000000`00000000 00000000`00000000 : nt!KxTryToAcquireSpinLock+0x6a
01 ffff9780`ba1eadb0 fffff804`447b3da4 : 00000000`00000000 00000000`0000000f ffff9780`ba1eb010 ffff9780`ba1eb870 : nt!KeFreezeExecution+0xb3
02 ffff9780`ba1eaee0 fffff804`441295f1 : ffff9780`ba1eb010 ffff9780`ba1eb510 ffff9780`ba1eb010 00000000`00000000 : nt!KdEnterDebugger+0x64
03 ffff9780`ba1eaf10 fffff804`447b7665 : ffff9780`ba1eb010 ffff9780`ba1eb510 ffff9780`ba1eb010 00000000`00000000 : nt!KdpReport+0x71
04 ffff9780`ba1eaf50 fffff804`43f108f8 : ffff9780`ba1eb7c8 ffff9780`ba1eb000 ffff9780`ba1eb010 ffff9780`ba1eb510 : nt!KdpTrap+0x14d
05 ffff9780`ba1eafa0 fffff804`43f10565 : ffff9780`ba1eb7c8 ffff9780`ba1eb510 ffff9780`ba1eb010 00000000`00000000 : nt!KdTrap+0x2c
06 ffff9780`ba1eafe0 fffff804`44062442 : ffffab8c`cbaa2900 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiDispatchException+0x135
07 ffff9780`ba1eb690 fffff804`4405b988 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiExceptionDispatch+0xc2
08 ffff9780`ba1eb870 fffff804`447b3e7b : 00000000`00000000 00000000`0000000f ffff9780`ba1ebb30 ffff9780`ba1ec390 : nt!KxDebugTrapOrFault+0x3c8 (TrapFrame @ ffff9780`ba1eb870)
09 ffff9780`ba1eba00 fffff804`441295f1 : ffff9780`ba1ebb30 ffff9780`ba1ec030 ffff9780`ba1ebb30 00000000`00000000 : nt!KdEnterDebugger+0x13b
0a ffff9780`ba1eba30 fffff804`447b7665 : ffff9780`ba1ebb30 ffff9780`ba1ec030 ffff9780`ba1ebb30 00000000`00000000 : nt!KdpReport+0x71
0b ffff9780`ba1eba70 fffff804`43f108f8 : ffff9780`ba1ec2e8 ffff9780`ba1ebb00 ffff9780`ba1ebb30 ffff9780`ba1ec030 : nt!KdpTrap+0x14d
0c ffff9780`ba1ebac0 fffff804`43f10565 : ffff9780`ba1ec2e8 ffff9780`ba1ec030 ffff9780`ba1ebb30 00000000`00000000 : nt!KdTrap+0x2c
0d ffff9780`ba1ebb00 fffff804`44062442 : ffff9780`ba1ec1a8 ffffffff`00000000 00000000`00000000 ffff9780`ba1ec1c8 : nt!KiDispatchException+0x135
0e ffff9780`ba1ec1b0 fffff804`4405b988 : ffff9780`ba1ec388 ffffffff`00000000 00000000`00000000 ffff9780`ba1ec3a8 : nt!KiExceptionDispatch+0xc2
0f ffff9780`ba1ec390 fffff804`447b3e7b : 00000000`00000000 00000000`0000000f ffff9780`ba1ec650 ffff9780`ba1eceb0 : nt!KxDebugTrapOrFault+0x3c8 (TrapFrame @ ffff9780`ba1ec390)
...
3: kd> .trap ffff9780`ba1eb870
3: kd> ub rip l10
nt!KdEnterDebugger+0xee:
fffff804`447b3e2e 488901 mov qword ptr [rcx],rax
fffff804`447b3e31 0fb6054ae3b1ff movzx eax,byte ptr [nt!KdDebuggerNotPresent (fffff804`442d2182)]
fffff804`447b3e38 83e001 and eax,1
fffff804`447b3e3b c1e002 shl eax,2
fffff804`447b3e3e 83c801 or eax,1
fffff804`447b3e41 48894108 mov qword ptr [rcx+8],rax
fffff804`447b3e45 ff054dd4b0ff inc dword ptr [nt!KdDebuggerEnteredCount (fffff804`442c1298)]
fffff804`447b3e4b 418ac6 mov al,r14b
fffff804`447b3e4e 381d2c7dbcff cmp byte ptr [nt!KdPortLocked (fffff804`4437bb80)],bl
fffff804`447b3e54 488b6c2438 mov rbp,qword ptr [rsp+38h]
fffff804`447b3e59 488b742440 mov rsi,qword ptr [rsp+40h]
fffff804`447b3e5e 0f94c3 sete bl
fffff804`447b3e61 011d35d4b0ff add dword ptr [nt!KdDebuggerEnteredWithoutLock (fffff804`442c129c)],ebx
fffff804`447b3e67 488b5c2430 mov rbx,qword ptr [rsp+30h]
fffff804`447b3e6c 488b7c2448 mov rdi,qword ptr [rsp+48h]
fffff804`447b3e71 c70585bab1ff01000000 mov dword ptr [nt!KdEnteredDebugger (fffff804`442cf900)],1 ;触发调试异常(KdDebugTrapOrFault)
当有调试事件发生需要中断到调试器时需要调用nt!KdEnterDebugger函数,函数内部如果触发了异常,则会产生无限递归,资源耗尽后会触发KeBugCheckEx调用。为了正常中断到调试器,可以先把 fffff804`447b3e71 处指令NOP掉。
重新尝试,重启虚拟机,内核附加调试,NOP fffff804`447b3e71(nt!KdEnterDebugger+0x131) 处指令:
0: kd> u nt!KdEnterDebugger+0x131
nt!KdEnterDebugger+0x131:
fffff801`70f2ee71 c70585bab1ff01000000 mov dword ptr [nt!KdEnteredDebugger (fffff801`70a4a900)],1
fffff801`70f2ee7b 4883c420 add rsp,20h
fffff801`70f2ee7f 415e pop r14
fffff801`70f2ee81 c3 ret
fffff801`70f2ee82 cc int 3
fffff801`70f2ee83 cc int 3
fffff801`70f2ee84 cc int 3
fffff801`70f2ee85 cc int 3
0: kd> eb nt!KdEnterDebugger+0x131 90 90 90 90 90 90 90 90 90 90
0: kd> u nt!KdEnterDebugger+0x131 l10
nt!KdEnterDebugger+0x131:
fffff801`70f2ee71 90 nop
fffff801`70f2ee72 90 nop
fffff801`70f2ee73 90 nop
fffff801`70f2ee74 90 nop
fffff801`70f2ee75 90 nop
fffff801`70f2ee76 90 nop
fffff801`70f2ee77 90 nop
fffff801`70f2ee78 90 nop
fffff801`70f2ee79 90 nop
fffff801`70f2ee7a 90 nop
fffff801`70f2ee7b 4883c420 add rsp,20h
fffff801`70f2ee7f 415e pop r14
fffff801`70f2ee81 c3 ret
0: kd> g
启动应用,调试器可以正常中断了:
0: kd> g
Single step exception - code 80000004 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
EasyAntiCheat+0x70f82:
fffff801`756a0f82 489d popfq
2: kd> gn
Break instruction exception - code 80000003 (first chance)
EasyAntiCheat+0x70f92:
fffff801`756a0f92 cc int 3 ;主动触发断点异常
2: kd> r dr0,dr1,dr2,dr3
dr0=fffff80170a4a900 dr1=fffff801707cbb40 dr2=0000000000000000 dr3=0000000000000000
需要注意此时EAC已经设置nt!KdEnteredDebugger(0xfffff80170a4a900)和nt!KeBugCheckEx(0xfffff801707cbb40)两个硬件断点。
检测机制已经很清晰了:
对nt!KdEnteredDebugger下硬件写断点,对nt!KeBugCheckEx下硬件执行断点;
主动触发CC断点异常;
如果内核调试开启且调试器已附加,则内核调试引擎会执行nt!KdEnterDebugger中断到调试器以报告异常;
nt!KdEnterDebugger写入nt!KdEnteredDebugger触发硬件写断点;
资源允许则回到3形成递归调用,资源不足则调用nt!KeBugCheckEx;
nt!KeBugCheckEx触发硬件执行断点,回到3形成递归调用。(系统卡死,不会形成崩溃转储)
gn忽略EAC主动触发的CC异常,继续执行:
2: kd> gn
Single step exception - code 80000004 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
nt!KdExitDebugger+0xd:
fffff801`70f2ee95 4c8d0de4bab1ff lea r9,[nt!KdLogBuffer (fffff801`70a4a980)]
2: kd> ub rip l3
nt!KdExitDebugger:
fffff801`70f2ee88 4053 push rbx
fffff801`70f2ee8a 4883ec20 sub rsp,20h
fffff801`70f2ee8e 83256bbab1ff00 and dword ptr [nt!KdEnteredDebugger (fffff801`70a4a900)],0
2: kd> eb nt!KdExitDebugger+0x6 90 90 90 90 90 90 90
可以看到nt!KdExitDebugger同样会写nt!KdEnteredDebugger字段,按同样方式NOP掉即可。
KdDebuggerEnabled检测
对CC进行gn之后,我们发现虽然系统正常运行,但是调试机再次'失联'了,并且应用提示检测到’调试模式‘,查看dump,可以发现nt!KdDebuggerEnabled被清零了,这是一个对内核调试很重要的字段,清零就意味着客户机不会再响应调试器的任何请求。
0: kd> db nt!KdDebuggerEnabled
fffff801`16047181 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fffff801`16047191 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 14 ................
fffff801`160471a1 00 00 00 01 00 00 00 00-00 00 40 98 f7 ff ff 00 ..........@.....
fffff801`160471b1 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fffff801`160471c1 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
机制很简单,我不打算继续深究其具体实现,打补丁,过校验等,也许可以用一种相对通用的思路来解决这种检测(考虑到TP等也有类似的检测机制):
将nt!KdDebuggerEnabled及nt!KdDebuggerNotPresent换个位置存储
找到系统对这两个变量的引用,重定向到我们指定的位置
将nt!KdDebuggerEnabled置0,nt!KdDebuggerNotPresent置1,原地址展示伪造后的信息以过检测
经过上述操作后,程序对nt!KdDebuggerEnabled和nt!KdDebuggerNotPresent的破坏性修改将不会再影响到调试机制,其中难点在于第2步,可以使用IDA交叉引用查找,也可以WinDBG下硬件断点动态查找(推荐),不多赘述,实现如下:
# Windows 10 Kernel Version 17763 MP (4 procs) Free x64
# Product: WinNt, suite: TerminalServer SingleUserTS
# Built by: 17763.1.amd64fre.rs5_release.180914-1434
# 将nt!KdDebuggerEnabled及nt!KdDebuggerNotPresent重定向到nt!KdDebuggerEnabled+2处
# 需要注意:1. 所选择的重定向HOOK是没有通用性的,需灵活修改应用,2. 根据自己的调试环境自行调整偏移和补丁
eb nt!KdDebuggerEnabled 01 00 01 00
eb nt!KdCheckForDebugBreak+0xc80ee+2 56
eb nt!KdPollBreakIn+0x34+2 89
eb nt!KdPollBreakIn+0xf3+2 ca
eb nt!KdPollBreakIn+0x8b+3 32
eb nt!KdPollBreakIn+0x114+3 a9
eb nt!KdpCreateRemoteFile+0x54+3 c9
eb nt!KdpCreateRemoteFile+0x141+3 dc
eb nt!KdpSendWaitContinue+0xa0+3 dd
eb nt!KdpSendWaitContinue+0x780+3 fd
eb nt!KdExitDebugger+0x33+3 c2
eb nt!KdEnterDebugger+0xf1+3 4c
eb nt!KdDebuggerEnabled 00 01 01 00
PS: 不要处理 nt!ExpQuerySystemInformation,否则NtQuerySystemInformation依然可以检测到调试器。
未完待续...
经过上面的处理后,调试器功能已经正常了,不过应用还不能正常运行,提示检测到‘测试签名’,‘内核调试’等。