标题: MSDN系列(42)--windbg禁止在ibp处设置硬件断点 创建: 2021-12-03 09:50 更新: 链接: https://scz.617.cn/windows/202112030950.txt -------------------------------------------------------------------------- 目录: ☆ 背景介绍 ☆ ibp处能否设置硬件断点与线程数强相关 ☆ Win10并行加载机制 ☆ 进程启动过程中部分关键点 ☆ 后记 -------------------------------------------------------------------------- ☆ 背景介绍 在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 00000044`9029d330 00007fff`63a2345f dbgeng!ParseBpCmd+0x17c 01 00000044`9029d480 00007fff`63a24586 dbgeng!ProcessCommands+0xb5f 02 00000044`9029d570 00007fff`63946bf2 dbgeng!ProcessCommandsAndCatch+0x86 03 00000044`9029d5c0 00007fff`63946f14 dbgeng!Execute+0x346 04 00000044`9029dad0 00007ff6`a06a66de dbgeng!DebugClient::ExecuteWide+0x94 05 00000044`9029db30 00007ff6`a06a93bb cdb!MainLoop+0x532 06 00000044`9029fbb0 00007ff6`a06ac82d cdb!wmain+0x4df 07 00000044`9029fe50 00007fff`885b84d4 cdb!__wmainCRTStartup+0x14d 08 00000044`9029fe90 00007fff`88d41791 KERNEL32!BaseThreadInitThunk+0x14 09 00000044`9029fec0 00000000`00000000 ntdll!RtlUserThreadStart+0x21 可以看出cdb交互式调试的部分框架,若以后再碰上调试cdb本身的罕见需求,或可参 考。断点命中后,不停地单步p,观察各个条件检查的情况。因为代码片段很短,不 需要.logopen/.logclose什么的。运气不错,很快手工对比出条件检查的分叉点 -------------------------------------------------------------------------- /* * A环境 dbgeng!ParseBpCmd+0x1b1 */ 00007fff`63915c01 41397958 cmp dword ptr [r9+58h],edi ds:00000144`ca5069a8=00000001 -------------------------------------------------------------------------- /* * B环境 dbgeng!ParseBpCmd+0x1b1 */ 00007ff8`1db85c01 41397958 cmp dword ptr [r9+58h],edi ds:0000022f`cb3795a8=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 00000073`b5e7ce10 00007fff`6391cb32 dbgeng!ProcessInfo::ProcessInfo+0x140 01 00000073`b5e7cea0 00007fff`639d1310 dbgeng!NotifyCreateProcessEvent+0x296 02 00000073`b5e7d290 00007fff`639d0ee4 dbgeng!LiveUserTargetInfo::ProcessDebugEvent+0x380 03 00000073`b5e7d650 00007fff`6394a261 dbgeng!LiveUserTargetInfo::WaitForEvent+0x684 04 00000073`b5e7d810 00007fff`6394a801 dbgeng!RawWaitForEvent+0x3bd 05 00000073`b5e7d8d0 00007ff6`a06a630c dbgeng!DebugClient::WaitForEvent+0xb1 06 00000073`b5e7d910 00007ff6`a06a93bb cdb!MainLoop+0x160 07 00000073`b5e7f990 00007ff6`a06ac82d cdb!wmain+0x4df 08 00000073`b5e7fc30 00007fff`885b84d4 cdb!__wmainCRTStartup+0x14d 09 00000073`b5e7fc70 00007fff`88d41791 KERNEL32!BaseThreadInitThunk+0x14 0a 00000073`b5e7fca0 00000000`00000000 ntdll!RtlUserThreadStart+0x21 Evaluate expression: 0 = 00000000`00000000 -------------------------------------------------------------------------- # Child-SP RetAddr Call Site 00 00000073`b5e7ce40 00007fff`6391cb96 dbgeng!ThreadInfo::ThreadInfo+0x163 01 00000073`b5e7cea0 00007fff`639d1310 dbgeng!NotifyCreateProcessEvent+0x2fa 02 00000073`b5e7d290 00007fff`639d0ee4 dbgeng!LiveUserTargetInfo::ProcessDebugEvent+0x380 03 00000073`b5e7d650 00007fff`6394a261 dbgeng!LiveUserTargetInfo::WaitForEvent+0x684 04 00000073`b5e7d810 00007fff`6394a801 dbgeng!RawWaitForEvent+0x3bd 05 00000073`b5e7d8d0 00007ff6`a06a630c dbgeng!DebugClient::WaitForEvent+0xb1 06 00000073`b5e7d910 00007ff6`a06a93bb cdb!MainLoop+0x160 07 00000073`b5e7f990 00007ff6`a06ac82d cdb!wmain+0x4df 08 00000073`b5e7fc30 00007fff`885b84d4 cdb!__wmainCRTStartup+0x14d 09 00000073`b5e7fc70 00007fff`88d41791 KERNEL32!BaseThreadInitThunk+0x14 0a 00000073`b5e7fca0 00000000`00000000 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 00000052`5b2ccd70 00007ff8`1db8c2b9 dbgeng!ThreadInfo::ThreadInfo+0x163 01 00000052`5b2ccdd0 00007ff8`1dc41345 dbgeng!NotifyCreateThreadEvent+0x151 02 00000052`5b2cce50 00007ff8`1dc40ee4 dbgeng!LiveUserTargetInfo::ProcessDebugEvent+0x3b5 03 00000052`5b2cd210 00007ff8`1dbba261 dbgeng!LiveUserTargetInfo::WaitForEvent+0x684 04 00000052`5b2cd3d0 00007ff8`1dbba801 dbgeng!RawWaitForEvent+0x3bd 05 00000052`5b2cd490 00007ff7`5e8a630c dbgeng!DebugClient::WaitForEvent+0xb1 06 00000052`5b2cd4d0 00007ff7`5e8a93bb cdb!MainLoop+0x160 07 00000052`5b2cf550 00007ff7`5e8ac82d cdb!wmain+0x4df 08 00000052`5b2cf7f0 00007ff8`3ac884d4 cdb!__wmainCRTStartup+0x14d 09 00000052`5b2cf830 00007ff8`3ca61791 KERNEL32!BaseThreadInitThunk+0x14 0a 00000052`5b2cf860 00000000`00000000 ntdll!RtlUserThreadStart+0x21 Evaluate expression: 2 = 00000000`00000002 -------------------------------------------------------------------------- # Child-SP RetAddr Call Site 00 00000052`5b2ccd70 00007ff8`1db8c2b9 dbgeng!ThreadInfo::ThreadInfo+0x163 01 00000052`5b2ccdd0 00007ff8`1dc41345 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: 000000f1`8df73000 Unfrozen Start: notepad!WinMainCRTStartup (00007ff6`67fd8850) 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的出场。