在 WINDBG 中定位 ExceptionAddress 核心技术部陈庆 关键字 : 调试异常 windbg second chance ExceptionAddress 摘要 : 在 windbg 中调试时, 碰上 second chance 时, 栈回溯已经看不到与 ExceptionAddress 直接相关的信息, 但我们调试的目的就是要找到 ExceptionAddress 本文介绍了几种通用思路来解决这个问题 一 问题起因 助 ping.exe 的进程空间, 临时研究一下 Intel 指令前缀 用借 cdb.exe 加载 ping.exe, 执行到 PE 入口, 人工修改附近的 代码, 替换成欲测试的带有指令前缀的其它指令 cdb.exe -hd -o ping.exe (c24.188c): Break instruction exception - code 80000003 (first chance) e a x = 0 0 0 0 0 0 0 0 e b x = 0 0 0 0 0 0 0 0 e c x = c d c 7 0 0 0 0 edx=001be1e8 esi=fffffffe edi=00000000 eip=76f50fab esp=0015f338 ebp=0015f364 iopl=0 ntdll!ldrpdodebuggerbreak+0x2c: 76f50fab cc int 3 > g $exentry eip=002f2aa7 esp=0015f7bc ebp=0015f7c4 iopl=0 002f2aa7 e805030000 call ping! security_init_cookie (002f2db1) > eb eip f0 cc 90 90 45
> u eip l 3 002f2aa9 90 nop 002f2aaa 90 nop >!chkimg -d -lo 1 ping // 检查文件是否被篡改? 002f2aa7-002f2aaa 4 bytes - ping!maincrtstartup [ e8 05 03 00:f0 cc 90 90 ] 4 errors : ping (002f2aa7-002f2aaa) > p (c24.188c): Illegal instruction - code c000001d (first chance) (c24.188c): Illegal instruction - code c000001d (!!! second chance!!!) e a x = 0 0 0 0 0 0 0 0 e b x = 0 0 1 5 f 3 0 8 e c x = 0 0 0 0 0 0 0 0 edx=002f2aa7 通用思路在这种情况下定位触发异常的指令 (ExceptionAddress) 二 解决方案 1) 基本原理在 cdb 中碰上 seccond chance 时, 先查看调用栈回溯, 但此时已经看不到与 ExceptionAddress 直接相关的信息 > kpn # ChildEBP RetAddr 00 0015f2f4 76ec014d ntdll!ntraiseexception+0x12 01 0015f2f4 00000000 ntdll!kiuserexceptiondispatcher+ 0x29 从上述显示中看到, 碰上 second chance 时, 停在 NtRaise Exception() 中间 检查一下 NtRaiseException() 的函数体 : > uf ntdll!ntraiseexception 76ed15cc b82f010000 mov eax,12fh eip=76ed15de esp=0015f2f4 ebp=0015f7c4 iopl=0 76ed15d1 33c9 xor ecx,ecx 76ed15d3 8d542404 lea edx,[esp+4] ntdll!ntraiseexception+0x12: 76ed15de 83c404 add esp,4 结果触发 c000001d 异常, 停下来的时候已是 second chance 单就本例而言, 我能猜到是 lock int 3 触发异常, 扩展一下, 有无 76ed15d7 64ff15c0000000 call dword ptr fs:[0c0h] // TEB.WOW32Reserved 76ed15de 83c404 add esp,4 76ed15e1 c20c00 ret 0Ch > dt ntdll!_teb WOW32Reserved @$teb +0x0c0 WOW32Reserved : 0x744c2320 Void 46
NtRaiseException() 的函数原型是 : NTSTATUS NTAPI NtRaiseException ( IN PEXCEPTION_RECORD ExceptionRecord, 76ec0135 740c je ntdll!kiuserexceptiondispatche r+0x1f (76ec0143) ntdll!kiuserexceptiondispatcher+0x13: 76ec0137 5b pop ebx IN PCONTEXT IN BOOLEAN ThreadContext, HandleException 76ec0138 59 pop ecx 76ec0139 6a00 push 0 ); KiUserExceptionDispatcher() 调用 NtRaiseException() 时, 是这样的 : K i U s e r E x c e p t i o n D i s p a t c h e r ( E x c e p t i o n R e c o r d, ThreadContext ) NtRaiseException( ExceptionRecord, ThreadContext, FALSE ) 检查一下 KiUserExceptionDispatcher () 的函数体 : > uf ntdll!kiuserexceptiondispatcher 76ec013b 51 push ecx 76ec013c e88ffd0000 call ntdll!ntcontinue (76ecfed0) 76ec0141 eb0b jmp ntdll!kiuserexceptiondispatch er+0x2a (76ec014e) ntdll!kiuserexceptiondispatcher+0x1f: 76ec0143 5b pop ebx 76ec0144 59 pop ecx 76ec0145 6a00 push 0 // HandleException 76ec0147 51 push ecx // ThreadContext 76ec0124 fc cld 76ec0148 53 push ebx // ExceptionRecord 76ec0125 8b4c2404 mov ecx,dword ptr [esp+4] 76ec0129 8b1c24 mov ebx,dword ptr [esp] 76ec012c 51 push ecx 76ec012d 53 push ebx 76ec012e e8c8b10400 call ntdll!rtldispatchexception 76ec0149 e87e140100 call ntdll!ntraiseexception (76ed15cc) ntdll!kiuserexceptiondispatcher+0x2a: 76ec014e 83c4ec add esp,0ffffffech 76ec0151 890424 mov dword ptr [esp],eax (76f0b2fb) 76ec0154 c744240401000000 mov dword ptr [esp+4],1 76ec0133 0ac0 or al,al 76ec015c 895c2408 mov dword ptr [esp+8],ebx 47
76ec0160 c744241000000000 mov dword ptr [esp+10h],0 eip=002f2aa7 esp=0015f7bc ebp=0015f7c4 iopl=0 76ec0168 54 push esp 76ec0169 e89ab60400 call ntdll!rtlraiseexception (76f0b808) 76ec016e c20800 ret 8 从前面的执行显示看到, 碰上 second chance 时,esp=0015f2f4, 此时栈布局如下 : > dds esp l 5 0015f2f4 76ed15de ntdll!ntraiseexception+0x12 0015f2f8 76ec014e ntdll!kiuserexceptiondispatcher+0x2a 0015f2fc 0015f308 ExceptionRecord 0015f300 0015f358 ThreadContext 0015f304 00000000 HandleException windbg 有一个.exr 命令, 用于查看 ExceptionRecord 信息 : >.exr poi(esp+8) ExceptionAddress: 002f2aa7 (ping!maincrtstartup) ExceptionCode: c000001d (Illegal instruction) ExceptionFlags: 00000000 NumberParameters: 0 windbg 另有一个.cxr 命令, 用于查看 ThreadContext 信息 : >.cxr poi(esp+c) 无论是.exr 还是.cxr, 都直接定位了触发异常的指令 (ExceptionAddress) 碰上 second chance 时, 就这个版本的 ntdll 而言,ebx 指向 ExceptionRecord(ecx 在 76ed15d1 处已被 xor 清零 ), 因此可以对 ebx 使用.exr 命令 : >.exr ebx ExceptionAddress: 002f2aa7 (ping!maincrtstartup) ExceptionCode: c000001d (Illegal instruction) ExceptionFlags: 00000000 NumberParameters: 0 简单总结一下, 碰上 second chance 时, 执行 :.exr poi(esp+8) 或 :.cxr poi(esp+c) 或 :.exr ebx 上述三种办法都可以快速定位触发异常的指令 (ExceptionAddress) 48
2) 更简捷的方案上面的办法是最原理性的, 不过最简捷的办法是这个 : >.exr -1 ExceptionAddress: 002f2aa7 (ping!maincrtstartup) ExceptionCode: c000001d (Illegal instruction) 4 errors :!ping (002f2aa7-002f2aaa) CONTEXT: 0015f358 -- (.cxr 0x15f358) ExceptionFlags: 00000000 NumberParameters: 0 eip=002f2aa7 esp=0015f7bc ebp=0015f7c4 iopl=0 指定 -1 做.exr 的地址时, 调试器显示最近一次异常对应的信息 另有偷懒的办法 : >!analyze -v FAULTING_IP: ping!maincrtstartup+0 EXCEPTION_RECORD: ffffffff -- (.exr 0xffffffffffffffff) ExceptionAddress: 002f2aa7 (ping!maincrtstartup) ExceptionCode: c000001d (Illegal instruction) ExceptionFlags: 00000000 NumberParameters: 0 CHKIMG_EXTENSION:!chkimg -lo 50 -d!ping 002f2aa7-002f2aaa 4 bytes - ping!maincrtstartup [ e8 05 03 00:f0 cc 90 90 ] Resetting default scope L AST_CONTROL _TR A NSFER: from 74e833aa to 002f2aa7 FAILED_INSTRUCTION_ADDRESS: ping!maincrtstartup+0 三 延伸思路 1) 用 sxe 将 second chance 变成 first chance 前面介绍的办法都是碰上 second chance 时不进一步破坏现场 49
的情况下如何快速定位 ExceptionAddress e i p = 0 0 f 5 2 a a 7 e s p = 0 0 1 d f c 8 4 e a x =74 e 8 3 3 9 8 e b x =7e f d e 0 0 0 如果所遭遇的异常可以稳定触发, 并且每 ebp=001dfc8c iopl=0 up ei pl zr e c x = 0 0 0 0 0 0 0 0 e d x = 0 0 f 5 2 a a 7 次现场都差不多, 还可以用 sxe 将 second na pe nc chance 变成 first chance, 此时停下来的 c s = 0 0 2 3 s s = 0 0 2 b d s = 0 0 2 b e i p = 0 0 f 5 2 a a 7 e s p = 0 0 1 d f c 8 4 EIP 就是引发异常的指令 : e s = 0 0 2 b f s = 0 0 5 3 g s = 0 0 2 b ebp=001dfc8c iopl=0 up ei pl zr cdb.exe -hd -o ping.exe na pe nc (1270.16 6 8): B reak instruction exception - code 80000003 (first chance) ea x = 0 0 0 0 0 0 0 0 ebx = 0 0 0 0 0 0 0 0 ecx=2e820000 edx=0012dfe8 esi=fffffffe edi=00000000 e i p =7 6 f 5 0 f a b e s p = 0 0 1 d f 8 0 0 0 0f52aa7 e805030000 call ping! security_init_cookie (00f52db1) > eb eip f0 cc 90 90 > u eip l 3 c s = 0 0 2 3 s s = 0 0 2 b d s = 0 0 2 b e s = 0 0 2 b f s = 0 0 5 3 g s = 0 0 2 b 00f52aa7 f0cc lock int 3 2) 在引发异常的线程栈上搜索 ContextFlags ebp=001df82c iopl=0 up ei pl zr 00f52aa7 f0cc lock int 3 有时候通过分析栈布局寻找 CONTEXT na pe nc 00f52aa9 90 nop 太费劲了, 我们知道它一定在栈上, 只是 c s = 0 0 2 3 s s = 0 0 2 b d s = 0 0 2 b 00f52aaa 90 nop 不知道它在哪里, 此时可以尝试暴力搜索 e s = 0 0 2 b f s = 0 0 5 3 g s = 0 0 2 b > sxe c000001d CONTEXT > p 先来学习一下 CONTEXT 结构 : ntdll!ldrpdodebuggerbreak+0x2c: 76f50fab cc int 3 > g $exentry e a x =74 e 8 3 3 9 8 e b x =7e f d e 0 0 0 e c x = 0 0 0 0 0 0 0 0 e d x = 0 0 f 5 2 a a 7 (1270.1668): Illegal instruction - code c000001d (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. > dt ntdll!_context poi(esp+c) +0x000 ContextFlags : 0x1007f // CONTEXT_ALL CONTEXT_XSTATE +0x004 Dr0 : 0 +0x008 Dr1 : 0 +0x00c Dr2 : 0 50
+0x010 Dr3 : 0 +0x0c4 Esp : 0x15f7bc 0x00000010L) +0x014 Dr6 : 0 +0x0c8 SegSs : 0x2b #define CONTEXT_ EXTENDED_ +0x018 Dr7 : 0 R E G I S T E R S ( C O N T E X T_ i 3 8 6 + 0 x 01c FloatSave : _ >? $PEB 0x00000020L) FLOATING_SAVE_AREA Evaluate expression: 2130567168 = # d e f i n e C O N T E X T _ F U L L +0x08c SegGs : 0x2b 7efde000 (CONTEXT_CONTROL +0x090 SegFs : 0x53 +0x094 SegEs : 0x2b +0x098 SegDs : 0x2b +0x09c Edi : 0 +0x0a0 Esi : 0 +0x0a4 Ebx : 0x7efde000 // 流程到达 $exentry 时,CONTEXT.Ebx == $PEB +0x0a8 Edx : 0x2f2aa7 // 流程到达 $exentry 时,CONTEXT.Edx == Eip +0x0ac Ecx : 0 CONTEXT 结构第一个成员 ContextFlags 的取值在 winnt.h 中定义 : #define WOW64_CONTEXT_i386 0x00010000 #define WOW64_CONTEXT_i486 0x00010000 # d e f i n e C O N T E X T_ C O N T R O L (CONTEXT_i386 0x00000001L) # d e f i n e C O N T E X T_ I N T E G E R (CONTEXT_i386 0x00000002L) #define CONTEXT_SEGMENTS (CONTEXT_i386 0x00000004L) # d e f i n e C O N T E X T _ A L L (CONTEXT_CONTROL # d e f i n e C O N T E X T _ X S T A T E (CONTEXT_i386 0x00000040L) 一般来说 ContextFlags 是常量, 可以作为特征值 我们只想在栈区搜索特征值, 可以通过 TEB 中的 StackLimit StackBase 在定位栈区 > dt -b ntdll!_teb NtTib. +0x000 NtTib : +0x000 ExceptionList : Ptr32 +0x004 StackBase : Ptr32 +0x0b0 Eax : 0x74e83398 # define CONTEX T_ FLOATING _ +0x008 StackLimit : Ptr32 +0x0b4 Ebp : 0x15f7c4 P O I N T ( C O N T E X T _ i 3 8 6 +0x00c SubSystemTib : Ptr32 +0x0b8 Eip : 0x2f2aa7 0x00000008L) +0x010 FiberData : Ptr32 +0x0bc SegCs : 0x23 # d e f i n e C O N T E X T_ D E B U G _ +0x010 Version : Uint4B +0x0c0 EFlags : 0x10246 REGISTERS (CONTE X T_ i3 8 6 +0x014 ArbitraryUserPointer : 51
Ptr32 2009 年 Ken Johnson 给了另一种暴力搜索 CONTEXT 的方案, +0x018 Self : Ptr32 下述命令在引发异常的线程栈上搜索 ContextFlags, 以此定位 CONTEXT: > ~#e s -d poi(@$teb+8) poi(@$teb+4) 1007f 0015f358 0001007f 00000000 00000000 00000000. 2005 年 Ivan Brugiolo 给了如下搜索方案 : >.foreach (CONTE X T {~ # e s - [1]d poi(@$teb+8) poi(@$teb+4) 1007f}) {.echo CONTEXT;.cxr CONTEXT} 0x0015f358 拿 SegGs SegFs SegEs SegDs 作为特征值进行暴力搜索, 定位 SegGs 之后间接定位 CONTEXT: >.foreach (PSegGs {s -[w1]d 0 L? ffffffff @gs @fs @es @ ds}) {? ${PSegGs}-8c;.cxr ${PSegGs}-8c} L 后面有个? 号, 这是为了取消 windbg 默认的 256MB 范围上限 这个方案的误报会很多, 要当心 稍好点的方案是在引发异常的线程栈上搜索 : >.f o r e a c h ( P S e g G s {~ # e s - [1] d p o i (@ $ T E B+8) poi(@$teb+4) @gs @fs @es @ds}) {.cxr ${PSegGs}-8c} eip=002f2aa7 esp=0015f7bc ebp=0015f7c4 iopl=0 eip=002f2aa7 esp=0015f7bc ebp=0015f7c4 iopl=0 1007f 是我当前环境的值, 用上述命令的时候可能需要修正这个值, 比如 1003f 3) 在引发异常的线程栈上搜索 SegGs SegFs SegEs SegDs 四 参考文献 [1] Windows Debugging Help [2] Debugger tricks: Find all probable CONTEXT records in a crash dump - [2009-03-30] 52