【學習逆向工程,分析機器代碼】(一)(C語言篇)
by dreamerate
1、序
由於最近對逆向工程產生了濃厚的興趣,所以就利用UltraEdit32撰寫了一個麻雀雖小,但五臟俱全的“test.c”程序。然後用OllyDBG對它進行逆向工程,逐步分析機器代碼。主要目的是:探索C/C++編譯器是如何產生機器代碼;及驗證CRT函數及帶參數的自定義函數的call對棧產生的影響;push和pop對棧具體的實現;分析for結構和if結構及while產生的機器代碼。爲此我分別生成了一個優化版本及另一個未經優化版本。
2、一個具體而微的C程序
包括以下內容:
1)主函數main:主函數main內有一個變量及一些CRT函數的調用和一個if結構;
2)函數my_strcmp:它是一個字節串比較的自定義函數。函數有兩個參數:一個源字節串,另一個目標字節串;並且函數體內則有三個變量;另外在程序結構上,有一個for循環及if結構。
------------------------------------------------------------------------
源程序如下:
#include <windows.h> #include <stdio.h> #include <conio.h> //#include <ctype.h>
int main() { char buffer[100]; printf("請輸入序列號:/n"); scanf( "%s", buffer ); if ( my_strcmp( buffer, "SN12345" ) == 0 ) printf("註冊成功!/n"); else printf( "註冊失敗!/n" ); getche(); return 0; }
// 爲了測試,代碼並沒有優化,並且還特意使用了三個局部變量 // int my_strcmp( const char* pszSrc, const char* pszDest ) { char* pSrc = (char*)pszSrc; char* pDest = (char*)pszDest; int iResult = 0; for ( ; *pSrc != 0 && *pDest != 0 ; pSrc++, pDest++ ) { iResult = *pSrc - *pDest; if ( iResult != 0 ) return iResult; } return 0; }
------------------------------------------------------------------------
3、編譯
在XP SP2環境下,開一個cmd.exe,鍵入VC6,進入我們的text.c目錄,鍵入b,完成未優化版本編譯。鍵入b_opt,完成優化版本編譯。 以下是vc6.bat和b.bat及b_opt.bat的批處理內容:
VC6.bat ----------------------------------------------------------------------- @echo off set VC6DIR=I:/Program Files/Microsoft Visual Studio/VC98 set include=I:/DXSDK/Include;%VC6DIR%/Include;%VC6DIR%/atl/include;%VC6DIR%/mfc/include set lib=I:/DXSDK/Lib;%VC6DIR%/lib;%VC6DIR%/mfc/lib set path=c:/;I:/Program Files/Microsoft Visual Studio/Common/MSDev98/Bin;%VC6DIR%/Bin set %VC6DIR%= echo on -----------------------------------------------------------------------
b.bat ----------------------------------------------------------------------- cl.exe /c /Gz test.c link.exe /subsystem:console test_opt.obj LIBC.LIB kernel32.lib -----------------------------------------------------------------------
b_opt.bat ----------------------------------------------------------------------- cl.exe /c /Gz /O2 /Fotest_opt.obj test.c link.exe /subsystem:console /OUT:test_opt.exe test_opt.obj LIBC.LIB kernel32.lib -----------------------------------------------------------------------
4、逆向過程
打開OllyDBG,加載test_opt.exe,然後在00401000地址設置斷點。按下F9後我們來到斷點處,接着便是F8一路逐行分析代碼:
4.1 〖O2優化版本〗 ------------------------------------------------------------------------------------------------------------------------ // 主函數: int main()
imgae地址 機器代碼 彙編代碼 註釋 --------- ----------- --------------------------------- --------------------------------------------------------- 00401000 /$ 83EC 64 sub esp, 64 ; char buffer[100]; //esp - 100
00401003 |. 68 5C804000 push 0040805C ; push ["請輸入序列號:/n"] //esp - 4 00401008 |. E8 AA000000 call <printf> ; call printf
0040100D |. 8D4424 04 lea eax, dword ptr [esp+4] ; lea eax, [buffer] //獲取buffer的指針 00401011 |. 50 push eax ; push [buffer] //esp - 4 00401012 |. 68 58804000 push 00408058 ; push ["%s"] //esp - 4 00401017 |. E8 84000000 call <scanf> ; call scanf
0040101C |. 83C4 0C add esp, 0C ; esp + 12 // 釋放剛剛函數的參數調用的3個push,堆棧平衡。 ; // 此時esp的值又指向buffer了
0040101F |. 8D4C24 00 lea ecx, dword ptr [esp] ; lea eax, [buffer] //獲取buffer的指針。 00401023 |. 68 48804000 push 00408048 ; push ["SN12345"] // 傳入我們的序列號, esp - 4 00401028 |. 51 push ecx ; push [buffer] // esp - 4 00401029 |. E8 42000000 call <my_strcmp> ; 調用自定義函數比較字節串。注意!自定義的函數在執行完後, ; 會執行 retn <stack used bytes>釋放參數棧。而CRT的則不會。 ; call 指令內部實現: esp - 4, <my_strcmp>, ; 然後在那函數內的retn也會釋放這個esp佔用的4字節。
0040102E |. 85C0 test eax, eax ; 測試結果 00401030 |. 75 18 jnz short 0040104A ; 如果剛剛鍵入的序列號和系統的不配備,就跳到“註冊失敗”
00401032 |. 68 3C804000 push 0040803C ; push ["註冊成功!/n"] // esp - 4 00401037 |. E8 7B000000 call <printf> ; call printf 0040103C |. 83C4 04 add esp, 4 ; 釋放printf參數調用佔用的stack,堆棧平衡
0040103F |. E8 7D590000 call <getche> ; call getche
00401044 |. 33C0 xor eax, eax ; 執行return 0; 清空返回值EAX
00401046 |. 83C4 64 add esp, 64 ; 釋放buffer[100]
00401049 |. C3 retn ; 結束main函數
0040104A |> 68 30804000 push 00408030 ; push ["註冊失敗!/n"] // esp - 4 0040104F |. E8 63000000 call <printf> ; call printf 00401054 |. 83C4 04 add esp, 4 ; 釋放printf參數調用佔用的stack,堆棧平衡
00401057 |. E8 65590000 call <getche> ; call getche
0040105C |. 33C0 xor eax, eax ; 執行return 0; 清空返回值EAX
0040105E |. 83C4 64 add esp, 64 ; 釋放buffer[100]
00401061 /. C3 retn ; 結束main函數 ------------------------------------------------------------------------------------------------------------------------ // 自定義函數: int my_strcmp( const char* pszSrc, const char* pszDest )
imgae地址 機器代碼 彙編代碼 註釋 --------- ----------- --------------------------------- --------------------------------------------------------- 00401070 >/$ 8B4C24 04 mov ecx, dword ptr [esp+4] ; 獲取參數pszSrc。由於CPU執行了call指令,esp目前指向 ; 本函數地址,esp+4則指向第一個參數pszSrc, ; 壓參數時是由右至左,所以+4則是指最後入棧的參數
00401074 |. 56 push esi ; 備份esi寄存器,esp - 4
00401075 |. 8039 00 cmp byte ptr [ecx], 0 ; 判斷pszSrc指向的第一個字符是否爲NULL 00401078 |. 74 1F je short 00401099 ; 如果爲NULL就退出函數
0040107A |. 8B7424 0C mov esi, dword ptr [esp+C] ; 獲取第二個參數指針pszDest。因爲esp+8是esi的備份,so...
0040107E |. 2BF1 sub esi, ecx ; pszDest -= pszSrc,得到一個pszDest的偏移, ; 從而讓下一條指令的esi+ecx完成索引pszDest串操作
00401080 |> 8A140E /mov dl, byte ptr [esi+ecx] ; for結構。獲取pszDest指向的字符到dl中
00401083 |. 84D2 |test dl, dl ; 測試 *pszDest == 0 00401085 |. 74 12 |je short 00401099 ; 如果爲0就退出函數。表示已到pszDest串尾
00401087 |. 0FBE01 |movsx eax, byte ptr [ecx] ; 獲取pszSrc指向的當前字符到eax中 0040108A |. 0FBED2 |movsx edx, dl ; 獲取pszDest指向的當前字符到edx中 0040108D |. 2BC2 |sub eax, edx ; iResult = *pSrc - *pDest。O2優化的結果。優化爲這三條
0040108F |. 75 0A |jnz short 0040109B ; if ( iResult != 0 ) return iResult;
00401091 |. 8A41 01 |mov al, byte ptr [ecx+1] ; al = *(pszSrc + 1); 下一個pszSrc指向的字符 00401094 |. 41 |inc ecx ; pszSrc++; pszSrc指針+1 00401095 |. 84C0 |test al, al ; 測試是否爲0 00401097 |.^ 75 E7 /jnz short 00401080 ; 如果不爲0表示還未到串尾,繼續進行下一輪比較
00401099 |> 33C0 xor eax, eax ; 返回0表示相等,和strcmp一樣
0040109B |> 5E pop esi ; 恢復esi
0040109C /. C2 0800 retn 8 ; 執行retn <stack used bytes>釋放參數棧(pszSrc和pszDest) ------------------------------------------------------------------------------------------------------------------------
4.2 〖未經優化版本〗 ------------------------------------------------------------------------------------------------------------------------- // 主函數: int main()
imgae地址 機器代碼 彙編代碼 註釋 --------- ----------- --------------------------------- --------------------------------------------------------- 00401000 /$ 55 push ebp ; backup ebp 00401001 |. 8BEC mov ebp, esp ; backup esp 00401003 |. 83EC 64 sub esp, 64 ; char buffer[100]; 00401006 |. 68 30804000 push 408030 ; ASCII "請輸入序列號:/n" 0040100B E8 CB000000 call <printf> 00401010 |. 83C4 04 add esp, 4 00401013 |. 8D45 9C lea eax, dword ptr [ebp-64] 00401016 |. 50 push eax 00401017 |. 68 40804000 push 408040 ; ASCII "%s" 0040101C |. E8 A3000000 call <scanf> ; call scanf 00401021 |. 83C4 08 add esp, 8 00401024 |. 68 44804000 push 408044 ; /Arg2 = ASCII "SN12345" 00401029 |. 8D4D 9C lea ecx, dword ptr [ebp-64] ; | 0040102C |. 51 push ecx ; |Arg1 = [buffer] 0040102D |. E8 2B000000 call <my_strcmp> ; /call my_strcmp 00401032 |. 85C0 test eax, eax 00401034 |. 75 0F jnz short 00401045 00401036 |. 68 54804000 push 408054 ; ASCII "註冊成功!/n" 0040103B |. E8 9B000000 call <printf> 00401040 |. 83C4 04 add esp, 4 00401043 |. EB 0D jmp short 00401052 00401045 |> 68 60804000 push 408060 ; ASCII "註冊失敗!/n" 0040104A |. E8 8C000000 call <printf> 0040104F |. 83C4 04 add esp, 4 00401052 |> E8 8A590000 call <getche> 00401057 |. 33C0 xor eax, eax 00401059 |. 8BE5 mov esp, ebp ; resume esp 0040105B |. 5D pop ebp ; resume ebp 0040105C /. C3 retn ------------------------------------------------------------------------------------------------------------------------- // 自定義函數: int my_strcmp( const char* pszSrc, const char* pszDest )
imgae地址 機器代碼 彙編代碼 註釋 --------- ----------- --------------------------------- --------------------------------------------------------- 0040105D >/$ 55 push ebp ; backup ebp //call+2parms + current = 4 * 4 = 16D = 10H 0040105E |. 8BEC mov ebp, esp ; backup esp 00401060 |. 83EC 0C sub esp, 0C ; 定義三個變量 = 12D = 0CH 00401063 |. 8B45 08 mov eax, dword ptr [ebp+8] ; char* pSrc = (char*)pszSrc; // ebp=push ebp, ebp-4=call, ebp-8=last push param... 00401066 |. 8945 F4 mov dword ptr [ebp-C], eax 00401069 |. 8B4D 0C mov ecx, dword ptr [ebp+C] ; char* pDest = (char*)pszDest; 0040106C |. 894D F8 mov dword ptr [ebp-8], ecx 0040106F |. C745 FC 00000>mov dword ptr [ebp-4], 0 ; int iResult = 0; 00401076 |. EB 12 jmp short 0040108A 00401078 |> 8B55 F4 /mov edx, dword ptr [ebp-C] ; edx = pSrc 0040107B |. 83C2 01 |add edx, 1 ; pSrc++ 0040107E |. 8955 F4 |mov dword ptr [ebp-C], edx ; 00401081 |. 8B45 F8 |mov eax, dword ptr [ebp-8] ; eax = pDest 00401084 |. 83C0 01 |add eax, 1 ; pDest++ 00401087 |. 8945 F8 |mov dword ptr [ebp-8], eax 0040108A |> 8B4D F4 mov ecx, dword ptr [ebp-C] ; *pSrc != 0 0040108D |. 0FBE11 |movsx edx, byte ptr [ecx] 00401090 |. 85D2 |test edx, edx ; 測試是否到串尾 00401092 |. 74 28 |je short 004010BC ; 如果是就退出函數 00401094 |. 8B45 F8 |mov eax, dword ptr [ebp-8] ; *pDest != 0 00401097 |. 0FBE08 |movsx ecx, byte ptr [eax] 0040109A |. 85C9 |test ecx, ecx ; 測試是否到串尾 0040109C |. 74 1E |je short 004010BC ; 如果是就退出函數 0040109E |. 8B55 F4 |mov edx, dword ptr [ebp-C] ; 將pSrc指向的字符賦給EDX 004010A1 |. 0FBE02 |movsx eax, byte ptr [edx] ; 將pSrc指向的字符賦給EAX 004010A4 |. 8B4D F8 |mov ecx, dword ptr [ebp-8] ; 將pDest指針賦給ECX 004010A7 |. 0FBE11 |movsx edx, byte ptr [ecx] ; 將pDest指向的字符賦給EDX 004010AA |. 2BC2 |sub eax, edx ; iResult = *pSrc - *pDest; 004010AC |. 8945 FC |mov dword ptr [ebp-4], eax 004010AF |. 837D FC 00 |cmp dword ptr [ebp-4], 0 ; if ( iResult != 0 ) 004010B3 |. 74 05 |je short 004010BA ; 如果==0就繼續比較下一字符 004010B5 |. 8B45 FC |mov eax, dword ptr [ebp-4] ; 否則就return iResult; 004010B8 |. EB 04 |jmp short 004010BE ; 否則就return iResult; 004010BA |>^ EB BC /jmp short 00401078 ; 繼續比較下一字符 004010BC |> 33C0 xor eax, eax 004010BE |> 8BE5 mov esp, ebp ; resume esp 004010C0 |. 5D pop ebp ; resume ebp 004010C1 /. C2 0800 retn 8 ; 執行retn <stack used bytes>釋放參數棧(pszSrc和pszDest) ------------------------------------------------------------------------------------------------------------------------
由上面的代碼可看出:
1)由於在編譯時我給cl.exe添加了優化選項O2(大寫字母o和阿拉伯數字2),這個選項將會盡最大程度的優化PE的執行速度。 所以這機器代碼看起來和C的源程序不太像(具體參照my_strcmp內C程序的實現及未經優化版本的反彙編代碼);
2)if和while及for:根據它們條件的複雜度,相應的編譯成適合地跳轉指令;
3)全局變量:被統一放在PE的.data區。在需要使用的代碼處都是以地址操作的;
4)局部變量和參數:都是放在棧中。一般以esp來操作,由於棧是向下伸長的,所以每增加一個參數的傳遞(push操作)或是 增加局部變量,都是以“sub esp,<N>”完成的,而它的釋放則是“add esp,<N>”。
另外,在跟蹤的過程中,我發現CPU在執行call指令時,是先esp-4存<func_next_addr>入棧再jmp <func_addr>的,當執行函數的retn指令時便回收esp+4出棧<func_next_addr>,繼續執行下一條指令。雖然這個過程中我們在代碼中看不見,不過這些具體的操作是由call及retn內部實現的。另外,push、pop指令都是一樣的,成對操作!從而完成堆棧平衡的機制。^_^
5、總結
在跟蹤代碼的過程中,明白了之前看別人反彙編代碼鬱悶的幾個地方。那就是一般CRT函數在進行call之後,編譯器不會主動地在CRT函數內幫你釋放參數佔用的棧,而是在call之後主動插上一條“add esp, <參數佔用的棧數量,以機器字爲單位>”來維持堆棧平衡。在自定義的函數中,我們則無須擔心這個問題。編譯器會在return處釋放參數佔用的棧(retn <N>)。像這種東西只有真正分析過機器代碼才知道的。
另外,在未經優化的版本中,所產生的機器代碼幾乎和C源程序一模一樣。並且在每個函數的實現細節幾乎如下:
開頭必有: push ebp mov ebp, esp 結尾必有: mov esp, ebp pop ebp
由此大家都可見,未經優化的版本內的局部變量及參數不是直接用esp而是ebp!
分析完整個流程後,那個心情呀,可真舒暢!
|