<C++反汇编与逆向分析>的作者在书中P21页列写了一段代码:
int main()
{
int nInt0;
scanf("%f",&nInt);
}
并简短的提到,运行上面这段程序并输入小数,将会导致程序崩溃,这是由于在浮点寄存器没有初始化前进行浮点操作,
将无法转换小数部分。解决方案是在代码中任意位置定义一个浮点型变量(附注,并初始化),即可对浮点寄存器进行初始化。
出于好奇,我验证了作者所说并得到了如下的错误提示:
嗯,vs的调试功能实在太有限(也可能是我不会用),于是我换用windbg调试,并在异常处得到下列堆栈回溯:
(714.220): Break instruction exception - code 80000003 (first chance)
eax=00000001 ebx=7ffdf000 ecx=77f5168d edx=00140608 esi=02009b8c edi=0012ff80
eip=00404863 esp=0012fad8 ebp=0012fc94 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
floatex!_NMSG_WRITE+0x73:
00404863 cc int 3
0:000> kb
ChildEBP RetAddr Args to Child
0012fc94 004047e4 000000ff 0012fca8 00401291 floatex!_NMSG_WRITE+0x73 [crt0msg.c @ 221]
0012fca0 00401291 0012fcb4 0040baba 00000002 floatex!_FF_MSGBANNER+0x44 [crt0msg.c @ 169]
0012fca8 0040baba 00000002 0012fef8 0040264e floatex!_amsg_exit+0x11 [crt0.c @ 255]
0012fcb4 0040264e 00000000 0012ff7c 0012fd28 floatex!_fptrap+0xa [crt0fp.c @ 47]
0012fef8 004010ca 0042da38 0042b01d 0012ff30 floatex!_input+0x113e [input.c @ 836]
0012ff20 0040103d 0042b01c 0012ff7c 00160014 floatex!scanf+0x5a [scanf.c @ 56]
0012ff80 0040122c 00000001 00440ed0 00440e20 floatex!main+0x2d [c:\studio\floatex\float.cpp @ 7]
0012ffc0 77e5eb69 00160014 02009b8c 7ffdf000 floatex!mainCRTStartup+0xfc [crt0.c @ 206]
WARNING: Stack unwind information not available. Following frames may be wrong.
0012fff0 00000000 00401130 00000000 78746341 kernel32!CreateProcessInternalW+0x1177
注意上面每行栈回溯的最后都标有(crt库)文件及行数,这倒提醒我一件事:如果装过SDK,就能在编译器目录下找到crt库的源码,如vc++6.0对应的crt源文件可能位于:
C:\Program Files\Microsoft Visual Studio\VC98\CRT\SRC
sdk提供这样的功能为我定位和分析这种"Runtime Errot"提供了极大的便利。接下来,重启windbg并添加源文件路径,再次重现错误:
0:000> .srcpath C:\Program Files\Microsoft Visual Studio\VC98\CRT\SRC ;设置crt库源文件搜索路径
Source search path is: C:\Program Files\Microsoft Visual Studio\VC98\CRT\SRC
0:000> g
ModLoad: 77d10000 77d9d000 C:\WINDOWS\system32\user32.dll
ModLoad: 77c40000 77c80000 C:\WINDOWS\system32\GDI32.dll
;...此处删去部分模块加载的信息
(2dc.658): Break instruction exception - code 80000003 (first chance)
;为scanf输入值后将触发Runtime error。点击Retry后,程序会进入调试状态并触发断点
floatex!_NMSG_WRITE+0x73:
00404863 cc int 3
0:000> kb
ChildEBP RetAddr Args to Child
0012fc94 004047e4 000000ff 0012fca8 00401291 floatex!_NMSG_WRITE+0x73 [crt0msg.c @ 221]
0012fca0 00401291 0012fcb4 0040baba 00000002 floatex!_FF_MSGBANNER+0x44 [crt0msg.c @ 169]
0012fca8 0040baba 00000002 0012fef8 0040264e floatex!_amsg_exit+0x11 [crt0.c @ 255] ;<--弹Runtime error对话框的代码
0012fcb4 0040264e 00000000 0012ff7c 0012fd28 floatex!_fptrap+0xa [crt0fp.c @ 47]
0012fef8 004010ca 0042da38 0042b01d 0012ff30 floatex!_input+0x113e [input.c @ 836]
0012ff20 0040103d 0042b01c 0012ff7c 00160014 floatex!scanf+0x5a [scanf.c @ 56]
0012ff80 0040122c 00000001 00440ed0 00440e20 floatex!main+0x2d [c:\studio\floatex\float.cpp @ 7]
0:000> .frame 3 ;_amsg_exit(位于frame 2)本身不值得研究,所以从frame 3着手
03 0012fcb4 0040264e floatex!_fptrap+0xa [crt0fp.c @ 47]
切换帧栈后,定位到_fptrap函数中,很可惜,这几乎是一个空函数:
void __cdecl _fptrap(void)
{
_amsg_exit(_RT_FLOAT);
}
基于这样的现实,我猜测引起“Runtime Error”的成因还在上一个函数中,因此只能不厌其烦的再次切换到上一层堆栈,进入input函数。但是,我仔细核对了2次input的代码,发现input函数通过_fassign函数指针来调用_fptrap函数。在调用_fassign前仅仅检测格式化字符串的内容,并没有对浮点数输入有特殊的处理(具体代码在座的各位也可以一起分析一下,限于篇幅的原因就不再贴出来了)。更重要的一点,根据我的分析,就算换成了浮点数,依然会调用_fassign,如下图:
int main()
{
float val=0;
scanf("%f",&val);
}
不过,这次从函数指针_fassig进去后,就会发现无法显示源码了!起初,我怀疑windbg出错了,就查看反汇编代码,发现浮点数寄存器初始化与否,_fassign前后2次指向的代码不同:
1).如果未初始化,则指向_fptrap,反汇编代码行肯定寥寥无几:
0:000> uf _fptrap
floatex!_fptrap [crt0fp.c @ 46]:
46 0040a4b0 55 push ebp
46 0040a4b1 8bec mov ebp,esp
47 0040a4b3 6a02 push 2
47 0040a4b5 e8766effff call floatex!_amsg_exit (00401330)
47 0040a4ba 83c404 add esp,4
48 0040a4bd 5d pop ebp
48 0040a4be c3 ret
2).如果经过初始化,_fassign指向一段未公开的代码,但其反汇编代码也不是很复杂:
0:000> uf .
floatex!_fassign:
00403500 55 push ebp
00403501 8bec mov ebp,esp
00403503 83ec0c sub esp,0Ch
00403506 837d0800 cmp dword ptr [ebp+8],0
0040350a 7420 je floatex!_fassign+0x2c (0040352c)
floatex!_fassign+0xc:
0040350c 8b4510 mov eax,dword ptr [ebp+10h]
0040350f 50 push eax
00403510 8d4df8 lea ecx,[ebp-8]
00403513 51 push ecx
00403514 e8a7680000 call floatex!_atodbl (00409dc0)
00403519 83c408 add esp,8
0040351c 8b550c mov edx,dword ptr [ebp+0Ch]
0040351f 8b45f8 mov eax,dword ptr [ebp-8]
00403522 8902 mov dword ptr [edx],eax
00403524 8b4dfc mov ecx,dword ptr [ebp-4]
00403527 894a04 mov dword ptr [edx+4],ecx
0040352a eb18 jmp floatex!_fassign+0x44 (00403544)
floatex!_fassign+0x2c:
0040352c 8b5510 mov edx,dword ptr [ebp+10h]
0040352f 52 push edx
00403530 8d45f4 lea eax,[ebp-0Ch]
00403533 50 push eax
00403534 e807690000 call floatex!_atoflt (00409e40)
00403539 83c408 add esp,8
0040353c 8b4d0c mov ecx,dword ptr [ebp+0Ch]
0040353f 8b55f4 mov edx,dword ptr [ebp-0Ch]
00403542 8911 mov dword ptr [ecx],edx
floatex!_fassign+0x44:
00403544 8be5 mov esp,ebp
00403546 5d pop ebp
00403547 c3 ret
上下两段代码在长度上有着明显的差别,我粗略的看了一下第二段代码的实现是将用户输入到屏幕上的数字字符串转换成浮点数,而不是弹出"Runtime Error"----令人不悦的对话框!那么,问题来了,是什么造成如此的差别?别急,下面慢慢分析。
网上粗略的浏览了一下,大多数汇编代码在进行浮点运算前都会初始化浮点控制器,进而可以推测,win32程序也少不了这个步骤,而且在进入main函数之前,浮点控制器已经完成就绪。很明显,上述的初始化工作一定是在c++启动代码中完成的。让我们借助IDA移步到mainCRTStartup函数中。
在__cinit中,我发现一处可疑的调用call __FPinit
__cinit __cinit proc near ; CODE XREF: _mainCRTStartup+D2p
__cinit push ebp
__cinit+1 mov ebp, esp
__cinit+3 cmp __FPinit, 0
__cinit+A jz short loc_403A82
__cinit+C call __FPinit
__cinit+12
__cinit+12 loc_403A82: ; CODE XREF: __cinit+Aj
__cinit+12 push offset ___xi_z
__cinit+17 push offset ___xi_a
__cinit+1C call _initterm
__cinit+21 add esp, 8
__cinit+24 push offset ___xc_z
__cinit+29 push offset ___xc_a
__cinit+2E call _initterm
__cinit+33 add esp, 8
__cinit+36 pop ebp
__cinit+37 retn
__cinit+37 __cinit endp
本想着查看__FPinit的定义,然而,它不过是一个函数指针,指向__fpmath
.data:00431A38 __FPinit dd offset __fpmath ; DATA XREF: __cinit+3r
继续跟进__fpmath,我们会发现正是它完成了初始化浮点控制器的任务。顺带说一下,在__fpmath中会调用__cfltcvt_init,下面是调用流程图:
__cfltcvt_init会使用__cfltcvt_tab函数地址数组设置诺干回调函数,包括前文提到的__fassign:
__cfltcvt_init __cfltcvt_init proc near ; CODE XREF: __fpmath+6p
__cfltcvt_init push ebp
__cfltcvt_init+1 mov ebp, esp
__cfltcvt_init+3 mov __cfltcvt_tab, offset __cfltcvt
__cfltcvt_init+D mov off_431D08, offset __cropzeros
__cfltcvt_init+17 mov off_431D0C, offset __fassign <---设置__fassign函数指针的值
__cfltcvt_init+21 mov off_431D10, offset __forcdecpt
__cfltcvt_init+2B mov off_431D14, offset __positive
__cfltcvt_init+35 mov off_431D18, offset __cfltcvt
__cfltcvt_init+3F pop ebp
__cfltcvt_init+40 retn
__cfltcvt_init+40 __cfltcvt_init endp
这是__cfltcvt_tab数组的定义,共定义了6个函数地址,供__cfltcvt_init设置。通过__cfltcvt_tab数组来设置浮点处理函数的目的可能是为了兼容其他编译选项,如:MultiThread Debug\MultiThread等选项,这些不同的选项使用的浮点数处理函数略有差别
.data:00431D04 __cfltcvt_tab dd offset __fptrap ; DATA XREF: __cfltcvt_init+3w
.data:00431D04 ; __output+6ADr
.data:00431D08 off_431D08 dd offset __fptrap ; DATA XREF: __cfltcvt_init+Dw
.data:00431D08 ; __output+6F1r
.data:00431D0C off_431D0C dd offset __fptrap ; DATA XREF: __cfltcvt_init+17w
.data:00431D0C ; __input+1138r
.data:00431D10 off_431D10 dd offset __fptrap ; DATA XREF: __cfltcvt_init+21w
.data:00431D10 ; __output+6CFr
.data:00431D14 off_431D14 dd offset __fptrap ; DATA XREF: __cfltcvt_init+2Bw
.data:00431D18 off_431D18 dd offset __fptrap ; DATA XREF: __cfltcvt_init+35w
最后,我猜测,在编译阶段,如果,IDE发现程序将进行浮点运算,则使__Fpinit指向__fpmath,初始化浮点数控制器;否则保持__Fpinit为空。同时,按编译选项填写__cfltcvt_tab数组中的各个函数指针,为程序提供不同浮点数处理功能。