【學習逆向工程,分析機器代碼】(一)(C語言篇)

【學習逆向工程,分析機器代碼】(一)(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!

   分析完整個流程後,那個心情呀,可真舒暢!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章