栈被破坏了可一点都不好玩儿!尤其是当你在分析crash dump或者程序发生异常的时候,我猜首先要做的事,可能就是先查看一下儿堆栈调用。
但是发现当前线程的栈被破坏了,你的主要分析工具也无法显示堆栈,这可咋办哩?
尽管如此,有时候也可以修复被破坏的堆栈。我已经出了一些关于.NET和C++调试的教程,但是大家的要求,我也会再展示一个例子
.net 调试:http://sela.co.il/syl/syllabus.aspx?CourseCode=DNDebug
c++调试:http://sela.co.il/syl/syllabus.aspx?CourseCode=CPPDbg
你可以从http://s.sashag.net/ocrImV这个网盘中下载本次例子中需要的dump文件、符号文件以及程序源码文件!
OK,我们使用WinDbg(32-bit)打开dump文件,获取如下信息:
User Mini Dump File: Only registers, stack and portions of memory are available
. . .
This dump file has an exception of interest stored in it.
The stored exception information can be accessed via .ecxr.
(1ed0.870): Access violation – code c0000005 (first/second chance not available)
eax=00000000 ebx=00000001 ecx=73536122 edx=00000000 esi=002af37c edi=0000004e
eip=00000000 esp=002af1a8 ebp=00000000 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246
00000000 ?? ???
0:000> k
ChildEBP RetAddr
WARNING: Frame IP not in any known module. Following frames may be wrong.
002af1a4 00000000 0x0
看到当前指令的地址是0x00000000的时候就猜到一定不是什么好事儿,意味着指令指针寄存器(EIP)已经被破坏了。同时你也可以看到EBP也被破坏了,EBP的值也是0x00000000,
这也是为什么k命令无法显示任何堆栈信息的原因。
好在ESP寄存器的值看起来是个正常的值,也不能确定这个值一定正常的,但是我们可以尝试读取ESP指向的内存,我们我们能正常读取它指向的内存数据,那几乎100%确定,ESP仍指向的是栈地址。
因为这是一个mini-dump文件,只包含和栈相关的内存数据。
如果ESP确实指向原始栈地址,我们可以尝试手动查看栈上的数据,寻找和返回地址相似的数据。在返回地址前面,我们应该能看到一个之前PUSH到栈里面的EBP的值,这个EBP是上一个函数的EBP。
其实每一个返回地址前面的EBP都保存的是上一个函数的EBP地址,除非开启了栈帧优化(FPO),关于FPO我打算以后再写一篇文章专门来介绍。
关于FPO以及其历史的介绍,网上有一篇文章讲的很详细: https://blogs.msdn.microsoft.com/larryosterman/2007/03/12/fpo/
如果你忘记了堆栈的内存布局,可以来复习一下Intel x86 函数调用约定: http://www.unixwiz.net/techtips/win32-callconv-asm.html
下面是ESP包含的原始栈信息,这时可以设置好指向BatteryMeter.pdb文件的符号路径,方便使用dds命令时正确的显示符号信息:
0:000> dds ESP
002af1a8 00000000
002af1ac 002af120
002af1b0 00000000
002af1b4 014cfe90
002af1b8 002af0fc
002af1bc 742fd594 uxtheme!StreamInit+0x36
002af1c0 002af180
002af1c4 01850815
002af1c8 0000029e
002af1cc 00000000
002af1d0 00000000
002af1d4 737990fa
002af1d8 002af210
002af1dc 013719be BatteryMeter!RecurseDeep+0x4e […\batterymeterdlg.cpp @ 135]
002af1e0 00000004
002af1e4 77dbc290 mfc100u!AfxDlgProc […\dlgcore.cpp @ 22]
002af1e8 00000000
002af1ec 002af284
002af1f0 00000001
002af1f4 00a24a74
002af1f8 00a5ec90
002af1fc 00a24cf0
002af200 002af228
002af204 002af198
002af208 002af234
002af20c 73799332
002af210 002af248
002af214 013719be BatteryMeter!RecurseDeep+0x4e […\batterymeterdlg.cpp @ 135]
首先很高兴能看到ESP指向的内存是dump文件中的有效内存,说明我们正在查看堆栈。这里有几个地址很有可能是返回地址,并且紧接在它们前面的地址就是上一个函数堆栈的EBP。
我们可以挨个检查这些保存在栈上的EBP值指向的内存,如果是在栈上,说明这个栈是可以被我们看到的。
0:000> dd 002af0fc L1
002af0fc ????????
0:000> dd 002af210 L1
002af210 002af248
尝试第一个EBP失败了,但是第二个成功了,当我们拿到了一个有效的保存在栈上的EBP的时候,我们就可以开始手动修复堆栈了,这个EBP指向的是前一个栈帧的EBP,正常的EBP就这样一层一层链起来的:
0:000> dds 002af210 L2
002af210 002af248
002af214 013719be BatteryMeter!RecurseDeep+0x4e […\batterymeterdlg.cpp @ 135]
0:000> dds 002af248 L2
002af248 002af280
002af24c 013719be BatteryMeter!RecurseDeep+0x4e […\batterymeterdlg.cpp @ 135]
0:000> dds 002af280 L2
002af280 002af2b8
002af284 013719be BatteryMeter!RecurseDeep+0x4e […\batterymeterdlg.cpp @ 135]
0:000> dds 002af2b8 L2
002af2b8 002af2f0
002af2bc 013719be BatteryMeter!RecurseDeep+0x4e […\batterymeterdlg.cpp @ 135]
0:000> dds 002af2f0 L2
002af2f0 002af304
002af2f4 013719f7 BatteryMeter!CBatteryMeterDlg::OnCPUSelectorChanged+0x27 […\batterymeterdlg.cpp @ 142]
0:000> dds 002af304 L2
002af304 002af318
002af308 77d92c8c mfc100u!_AfxDispatchCmdMsg+0x58 […\cmdtarg.cpp @ 112]
我们可以一直这样遍历EBP,直到最后一层栈帧,根据目前已经获取的堆栈信息,我们发现在栈被破坏之前,有个名为RecurseDeep的函数,一直在调用自己,至少调用了4次。
WinDbg有一个命令可以帮我们来重建堆栈,我们只需要猜测一下ESP、EBP、EIP的值,它就会按照我们给定的值去构造一个合理的堆栈。
我们可以假设EBP就是我们刚才找到的第一个在栈上的EBP的值,EIP可以假设为刚才EBP后面的返回地址,而ESP可以和EBP的值一样,然后就可以看到Windbg输出以下信息:
0:000> k = 002af210 002af210 013719be
ChildEBP RetAddr
002af210 013719be BatteryMeter!RecurseDeep+0x4e
002af248 013719be BatteryMeter!RecurseDeep+0x4e
002af280 013719be BatteryMeter!RecurseDeep+0x4e
002af2b8 013719be BatteryMeter!RecurseDeep+0x4e
002af2f0 013719f7 BatteryMeter!RecurseDeep+0x4e
002af304 77d92c8c BatteryMeter!CBatteryMeterDlg::OnCPUSelectorChanged+0x27 002af318
77d92e51 mfc100u!_AfxDispatchCmdMsg+0x58 002af334
77dc6d36 mfc100u!CCmdTarget::OnCmdMsg+0x124 002af358
77e1c4cb mfc100u!CPropertySheet::OnCmdMsg+0x1d
002af388 77e1bc7f mfc100u!CWnd::OnNotify+0x7b
002af454 002af478 mfc100u!CWnd::OnWndMsg+0x9e
… source information and the rest of the stack snipped for brevity
此时,我们已经利用已知极少的信息,将一个被破坏堆栈恢复成了正常的调用堆栈的样子,并且帮助我们定位了导致堆栈被破坏的罪魁祸首!
查看BatteryMeter!RecurseDeep的源码,就可以找出导致堆栈破坏的根本原因:原来这个破坏堆栈的函数,并没有破坏自己的栈帧,而是返回到了之前函数的栈帧的地方,并用零覆盖了一小块内存区域
参考资料:
-
https://blogs.msdn.microsoft.com/ntdebugging/2010/05/12/x64-manual-stack-reconstruction-and-stack-walking/
-
https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/manually-walking-a-stack
-
http://www.hexblog.com/?p=104