windbg禁止在ibp处设置硬件断点

一、背景介绍

在A、B两个测试环境中分别执行

C:\temp\cdb.exe -noinh -snul -hd -o -G notepad.exe

由于未指定-g,cdb断在ibp(初始化断点)。A环境中无法在ibp处ba设置硬件断点,会报错,这符合预期。

ba e1 /1 @$exentry
^ Unable to set breakpoint error
The system resets thread contexts after the process
breakpoint so hardware breakpoints cannot be set.
Go to the executable’s entry point and set it then.
‘ba e1 /1 @$exentry’

尽量避免在ibp对任何地址设置硬件断点,因为后面会过ZwContinue,该函数会切换CONTEXT,必然导致DR*寄存器被修改。理论上在ibp处不是不能设硬件断点,而是设了之后,很快就会被破坏。为了避免将来这种破坏引起误会,cdb干脆禁止在ibp处使用ba设置硬件断点,如果尝试ba命令,会提示到了@$exentry之后才可以设硬件断点。其实在@$exentry之前很多地方都可以正常使用ba设置硬件断点,比如”sxe cpr”、”sxe ld:ntdll”命中时、流程到达ntdll!RtlUserThreadStart时,这几处都可以ba。

禁止在ibp设置硬件断点是cdb自己做的限制,更确切点,在dbgeng!ParseBpCmd中做的限制,并非系统级限制,并非来自ntdll.dll或ntoskrnl.exe的限制。有点类似资源管理器对NTFS文件系统做了一些额外的限制,但如果换个shell就可以绕过限制,因为底层NTFS文件系统并无那些限制。

本来一切都符合预期,但意外地发现,B环境中可以在ibp处ba设置硬件断点,没有报错,bl可以看到硬件断点,当然,后来还是被ZwContinue破坏而失效。B环境发生的事很不友好,我在B环境中被坑了一把,当时忘了ibp处设置硬件断点的坑,发现ba断不下来,还奇怪呢,云海提醒之后才重新想起缘由。

A、B环境都是Win10企业版2016 LTSB 1607。补丁有些不同,ntoskrnl.exe不同,但notepad.exe、ntdll.dll、cdb.exe、dbgeng.dll这些都相同。云海在更早期的其他环境中测试,重现B环境出现的现象,他发现另一处不同,图形版windbg的行为符合预期,命令行版cdb的行为同B环境。

针对A、B差异我做了点调试分析。这事本身没有什么技术价值,但调试分析过程有价值。面对疑问动用调试器去分析一二,这方面张银奎师兄是我辈楷模。

二、ibp处能否设置硬件断点与线程数强相关

A环境如下,B环境略有出入,不影响大局

Win10

Win10企业版2016 LTSB 1607(OS Build 14393.4704)

ntoskrnl.exe

10.0.14393.4704 (rs1_release.211004-1917)

ntdll.dll

10.0.14393.4704 (rs1_release.211004-1917)

cdb.exe
dbgeng.dll

10.0.19041.1 (WinBuild.160101.0800)

cdb.exe只是个壳,调试的活儿一般都在dbgeng.dll中,直接IDA分析dbgeng.dll。在其中查找”hardware breakpoints cannot be set”,这是ba的报错信息。找到后看交叉引用,定位一段代码

/*

dbgeng!ParseBpCmd+0x17c
/ 00007FFF63915BCC 48 85 C9 test rcx, rcx 00007FFF63915BCF 74 3A jz short loc_7FFF63915C0B 00007FFF63915BD1 44 8B 81 88 0F 00 00 mov r8d, [rcx+0F88h] 00007FFF63915BD8 41 8D 40 FE lea eax, [r8-2] 00007FFF63915BDC 3B C7 cmp eax, edi 00007FFF63915BDE 77 2B ja short loc_7FFF63915C0B 00007FFF63915BE0 45 85 C0 test r8d, r8d 00007FFF63915BE3 74 10 jz short loc_7FFF63915BF5 00007FFF63915BE5 8B 81 8C 0F 00 00 mov eax, [rcx+0F8Ch] 00007FFF63915BEB 2D 00 04 00 00 sub eax, 400h 00007FFF63915BF0 83 F8 05 cmp eax, 5 00007FFF63915BF3 76 16 jbe short loc_7FFF63915C0B 00007FFF63915BF5 00007FFF63915BF5 loc_7FFF63915BF5: 00007FFF63915BF5 44 3B F7 cmp r14d, edi 00007FFF63915BF8 75 11 jnz short loc_7FFF63915C0B 00007FFF63915BFA 41 0F BA E2 09 bt r10d, 9 00007FFF63915BFF 73 0A jnb short loc_7FFF63915C0B /

dbgeng!ParseBpCmd+0x1b1
/ 00007FFF63915C01 41 39 79 58 cmp [r9+58h], edi 00007FFF63915C05 0F 84 94 07 00 00 jz loc_7FFF6391639F … /

dbgeng!ParseBpCmd+0x94f
*/
00007FFF6391639F 48 8D 15 FA CF 4E 00 lea rdx, aUnableToSetBre ; “Unable to set breakpoint error\nThe sys”…
00007FFF639163A6 B9 11 10 00 00 mov ecx, 1011h ; unsigned int

00007FFF639163AB E8 B0 C4 FD FF call ErrorDesc(ulong,ushort const *)

dbgeng!ErrorDesc负责错误信息的输出,”dbgeng!ParseBpCmd+0x17c”开始的代码是组合判断逻辑。F5看伪代码,几个逻辑与下来,为真才报错。就是说,大家都在ibp,但某些条件检查结果不同,结果A报错、B不报错。没有dbgeng的私有符号,公有符号未能揭示前述代码片段的意义,先动态调试,对比A、B环境中过这段代码时的差异。

最简单的办法是在当前cdb中执行.dbgdbg,呼出另一个cdb来调试当前cdb。为了行文方便,称当前cdb为cdb_0,.dbgdbg呼出来的cdb称为cdb_1。

在cdb_1中执行

.prompt_allow +reg +ea +dis
ba e1 dbgeng!ParseBpCmd+0x17c “kpn”

在cdb_0中执行

ba e1 /1 @$exentry

ba导致cdb_1中断点命中,调用栈如下

# Child-SP RetAddr Call Site
00 000000449029d330 00007fff63a2345f dbgeng!ParseBpCmd+0x17c
01 000000449029d480 00007fff63a24586 dbgeng!ProcessCommands+0xb5f
02 000000449029d570 00007fff63946bf2 dbgeng!ProcessCommandsAndCatch+0x86
03 000000449029d5c0 00007fff63946f14 dbgeng!Execute+0x346
04 000000449029dad0 00007ff6a06a66de dbgeng!DebugClient::ExecuteWide+0x94
05 000000449029db30 00007ff6a06a93bb cdb!MainLoop+0x532
06 000000449029fbb0 00007ff6a06ac82d cdb!wmain+0x4df
07 000000449029fe50 00007fff885b84d4 cdb!__wmainCRTStartup+0x14d
08 000000449029fe90 00007fff88d41791 KERNEL32!BaseThreadInitThunk+0x14
09 000000449029fec0 0000000000000000 ntdll!RtlUserThreadStart+0x21

可以看出cdb交互式调试的部分框架,若以后再碰上调试cdb本身的罕见需求,或可参
考。断点命中后,不停地单步p,观察各个条件检查的情况。因为代码片段很短,不
需要.logopen/.logclose什么的。运气不错,很快手工对比出条件检查的分叉点

/*

A环境 dbgeng!ParseBpCmd+0x1b1
*/

00007fff63915c01 41397958 cmp dword ptr [r9+58h],edi ds:00000144ca5069a8=00000001

/*

B环境 dbgeng!ParseBpCmd+0x1b1
*/

00007ff81db85c01 41397958 cmp dword ptr [r9+58h],edi ds:0000022fcb3795a8=00000003

流程到达”dbgeng!ParseBpCmd+0x1b1″时,A、B环境的edi都为1,但dwo(@r9+0x58)不同,A中该值为1,B中该值为3。现在需要搞清楚dwo(@r9+0x58)含义,在哪儿设置的?

IDA里看出r9的类型是”class ProcessInfo *”,但PDB没有提供ProcessInfo的定义,只能猜dwo(@r9+0x58)是个notepad进程相关的值,要求它为1,然后cdb才施加ibp+ba保护措施,否则啥也不管。

既然dwo(@r9+0x58)是ProcessInfo类的成员,很可能在构造函数中被设置,即便不是如此,找到类实例的地址,再对偏移0x58处设置写数据断点,理论上也能找出设置dwo(@r9+0x58)的代码。

思路变成,重新调试cdb_0,拦截ProcessInfo类的构造函数,通过this指针(rcx)获取类实例地址,对偏移0x58处设置写数据断点。这里开始微妙起来,cdb_0断在ibp时,用于调试notepad进程的ProcessInfo实例已经生成了,我咋知道的?因为.dbgdbg呼出的cdb_1断不住dbgeng!ProcessInfo::ProcessInfo啊。有很多办法让cdb_1在cdb_0的更早阶段介入,我用最直白的办法

C:\temp\cdb.exe -noinh -snul -hd -G -2 C:\temp\cdb.exe -noinh -snul -hd -o -G notepad.exe

命令行参数-2的官方解释是

If the target application is a console application, this option causes it
to live in a new console window. The default is for a target console
application to share the window with CDB or NTSD.

用了-2,cdb_0、cdb_1不再共用一个console,这相当有必要。最左侧的cdb对应前面说的cdb_1,右侧的cdb对应前面说的cdb_0。现在是cdb_0要调试notepad,cdb_1要调试cdb_0,别搞混了。

直接断在cdb_1的ibp,在cdb_1中执行

.prompt_allow +reg +ea +dis
bp dbgeng!ProcessInfo::ProcessInfo “kpn;r $t0=@rcx+0x58;ba w1 @$t0 \”kpn;? dwo(@$t0)\”;gc”

A环境中看到2次命中,dwo(@r9+0x58)依次被赋值0、1

# Child-SP RetAddr Call Site
00 00000073b5e7ce10 00007fff6391cb32 dbgeng!ProcessInfo::ProcessInfo+0x140
01 00000073b5e7cea0 00007fff639d1310 dbgeng!NotifyCreateProcessEvent+0x296
02 00000073b5e7d290 00007fff639d0ee4 dbgeng!LiveUserTargetInfo::ProcessDebugEvent+0x380
03 00000073b5e7d650 00007fff6394a261 dbgeng!LiveUserTargetInfo::WaitForEvent+0x684
04 00000073b5e7d810 00007fff6394a801 dbgeng!RawWaitForEvent+0x3bd
05 00000073b5e7d8d0 00007ff6a06a630c dbgeng!DebugClient::WaitForEvent+0xb1
06 00000073b5e7d910 00007ff6a06a93bb cdb!MainLoop+0x160
07 00000073b5e7f990 00007ff6a06ac82d cdb!wmain+0x4df
08 00000073b5e7fc30 00007fff885b84d4 cdb!__wmainCRTStartup+0x14d
09 00000073b5e7fc70 00007fff88d41791 KERNEL32!BaseThreadInitThunk+0x14
0a 00000073b5e7fca0 0000000000000000 ntdll!RtlUserThreadStart+0x21

Evaluate expression: 0 = 00000000`00000000

# Child-SP RetAddr Call Site
00 00000073b5e7ce40 00007fff6391cb96 dbgeng!ThreadInfo::ThreadInfo+0x163
01 00000073b5e7cea0 00007fff639d1310 dbgeng!NotifyCreateProcessEvent+0x2fa
02 00000073b5e7d290 00007fff639d0ee4 dbgeng!LiveUserTargetInfo::ProcessDebugEvent+0x380
03 00000073b5e7d650 00007fff6394a261 dbgeng!LiveUserTargetInfo::WaitForEvent+0x684
04 00000073b5e7d810 00007fff6394a801 dbgeng!RawWaitForEvent+0x3bd
05 00000073b5e7d8d0 00007ff6a06a630c dbgeng!DebugClient::WaitForEvent+0xb1
06 00000073b5e7d910 00007ff6a06a93bb cdb!MainLoop+0x160
07 00000073b5e7f990 00007ff6a06ac82d cdb!wmain+0x4df
08 00000073b5e7fc30 00007fff885b84d4 cdb!__wmainCRTStartup+0x14d
09 00000073b5e7fc70 00007fff88d41791 KERNEL32!BaseThreadInitThunk+0x14
0a 00000073b5e7fca0 0000000000000000 ntdll!RtlUserThreadStart+0x21

Evaluate expression: 1 = 00000000`00000001

B环境中看到4次命中,dwo(@r9+0x58)依次被赋值0、1、2、3。B环境中前2次调用栈回溯同A环境,后2次如下

# Child-SP RetAddr Call Site
00 000000525b2ccd70 00007ff81db8c2b9 dbgeng!ThreadInfo::ThreadInfo+0x163
01 000000525b2ccdd0 00007ff81dc41345 dbgeng!NotifyCreateThreadEvent+0x151
02 000000525b2cce50 00007ff81dc40ee4 dbgeng!LiveUserTargetInfo::ProcessDebugEvent+0x3b5
03 000000525b2cd210 00007ff81dbba261 dbgeng!LiveUserTargetInfo::WaitForEvent+0x684
04 000000525b2cd3d0 00007ff81dbba801 dbgeng!RawWaitForEvent+0x3bd
05 000000525b2cd490 00007ff75e8a630c dbgeng!DebugClient::WaitForEvent+0xb1
06 000000525b2cd4d0 00007ff75e8a93bb cdb!MainLoop+0x160
07 000000525b2cf550 00007ff75e8ac82d cdb!wmain+0x4df
08 000000525b2cf7f0 00007ff83ac884d4 cdb!__wmainCRTStartup+0x14d
09 000000525b2cf830 00007ff83ca61791 KERNEL32!BaseThreadInitThunk+0x14
0a 000000525b2cf860 0000000000000000 ntdll!RtlUserThreadStart+0x21

Evaluate expression: 2 = 00000000`00000002

# Child-SP RetAddr Call Site
00 000000525b2ccd70 00007ff81db8c2b9 dbgeng!ThreadInfo::ThreadInfo+0x163
01 000000525b2ccdd0 00007ff81dc41345 dbgeng!NotifyCreateThreadEvent+0x151

Evaluate expression: 3 = 00000000`00000003

找到A、B环境中dwo(@r9+0x58)在何处被赋值。IDA里简单看了一下,对于A、B环境,dbgeng!LiveUserTargetInfo::ProcessDebugEvent处理的调试事件不同。cdb_0断在ibp之前,A环境中cdb_1只收到CREATE_PROCESS_DEBUG_EVENT(3),B环境中cdb_1先收到CREATE_PROCESS_DEBUG_EVENT(3),还额外收到2次CREATE_THREAD_DEBUG_EVENT(2)。

dwo(@r9+0x58)应该是notepad进程的线程数。进程创建时肯定有0号线程创建,也就是主线程,线程数至少为1。不知为何B环境中在notepad主线程之外额外多创建了2个线程,导致dwo(@r9+0x58)为3。

看dbgeng!ParseBpCmd+0x1b1处的代码逻辑,dbgeng设计时检查了ibp处的线程数,要求只有主线程存在,若有其他线程存在,dbgeng认为不是严格的初始化断点处,可能有其他变化发生,干脆不管ibp+ba保护措施的事了。

在cdb_0中用~*检查A、B环境ibp处的线程数,只要不止一个线程,就没有ibp+ba保护措施,符合调试分析结论。

泄露的XPSP1源码中没有ProcessInfo类,但有这些函数可以参考

/*

XPSP1\NT\sdktools\debuggers\ntsd64\event.cpp
/ ProcessDebugEvent /

XPSP1\NT\sdktools\debuggers\ntsd64\callback.cpp
*/

NotifyCreateThreadEvent

三、Win10并行加载机制

为什么B环境ibp处多了2个线程?若有Win10并行加载机制介入,就会出现B环境的现象,但这是充分非必要条件,存在其他解释。不管那么多,针对Win10并行加载机制做点实验。

在B环境中以管理员级cmd执行

reg.exe add “HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\notepad.exe” /v MaxLoaderThreads /t REG_DWORD /d 1 /f

将notepad.exe的MaxLoaderThreads设为1,这会关闭notepad.exe进程的并行加载机制。

在B环境中执行

C:\temp\cdb.exe -noinh -snul -hd -o -G notepad.exe

~*
. 0 Id: 1378.2ac Suspend: 1 Teb: 000000f18df73000 Unfrozen Start: notepad!WinMainCRTStartup (00007ff667fd8850)
Priority: 0 Priority class: 32 Affinity: 3

ba e1 /1 @$exentry
^ Unable to set breakpoint error
The system resets thread contexts after the process
breakpoint so hardware breakpoints cannot be set.
Go to the executable’s entry point and set it then.
‘ba e1 /1 @$exentry’

MaxLoaderThreads为1,导致ibp处只有主线程(0号线程),ibp+ba保护措施启用。

四、 进程启动过程中部分关键点

下面是2017年分析Win10 16299的进程启动过程中部分关键点,现在可能有变化,仅供参考。

ntdll!LdrInitializeThunk
ntdll!LdrpInitialize
ntdll!_LdrpInitialize
ntdll!LdrpInitializeProcess
ntdll!LdrpInitializeExecutionOptions (检查EXE的IFEO)
ntdll!LdrpInitShimEngine (初始化SHIM引擎)
ntdll!LdrpEnableParallelLoading (开启并行加载机制)
ntdll!LdrpMapAndSnapDependency
ntdll!LdrpDoDebuggerBreak (初始化断点)
ntdll!ZwContinue (会切换CONTEXT)
ntdll!RtlUserThreadStart
KERNEL32!BaseThreadInitThunk

$exentry/$iment()

五、后记

通过调试分析,大概知道了造成A、B差异的原因。并行加载机制和CPU/Core数目共同作用,导致停留在ibp时线程数目不同,只存在主线程时ibp+ba保护措施才会生效。

但未能揭示终极真相。1CPU/1Core时,最终在哪里限制多线程并行加载,没找到。1CPU/2Core时,相比cdb,windbg又是如何限制多线程并行加载的,没去找。

至少可以说,dbgeng的ibp+ba保护措施没有考虑到Win10的出场。

版权声明

本站“技术博客”所有内容的版权持有者为绿盟科技集团股份有限公司(“绿盟科技”)。作为分享技术资讯的平台,绿盟科技期待与广大用户互动交流,并欢迎在标明出处(绿盟科技-技术博客)及网址的情形下,全文转发。
上述情形之外的任何使用形式,均需提前向绿盟科技(010-68438880-5462)申请版权授权。如擅自使用,绿盟科技保留追责权利。同时,如因擅自使用博客内容引发法律纠纷,由使用者自行承担全部法律责任,与绿盟科技无关。

Spread the word. Share this post!

Meet The Author

C/ASM程序员

Leave Comment