1、序

   由于最近对逆向工程产生了浓厚的兴趣,所以就利用UltraEdit32撰写了一个麻雀虽小,但五脏俱全的“test.c”程序。然后用OD对它进行逆向工程,逐步分析机器代码。主要目的是:探索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!

   分析完整个流程后,那个心情呀,可真舒畅!