<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數組中的各個函數指針,為程式提供不同浮點數處理功能。