代码逆向(一)——寻找main函数入口

逆向的第一步是什么?这要问你学习C语言的第一步是什么,很自然的,逆向的第一步当然也是大名鼎鼎“HelloWorld!”了。但是也不要因此就误认为这一节会很简单,如果你是第一次接触逆向的话,那么这一节还是有些难度的。

      好的,让我们先写一个世界上最出名的程序:

int _tmain(int argc, _TCHAR* argv[])
{
    printf("Hello World!/r/n");
    return 0;
}

      不错!很好的开始!然后用VS2008以Debug方式编译下,再用OllyDbg打开看看:

00411078 >JMP Test_0.004117B0
0041107D  JMP Test_0.00412CC0
00411082  JMP <JMP.&MSVCR90D._lock>
00411087  JMP <JMP.&KERNEL32.GetProcAddress>
0041108C  JMP Test_0.00411440
00411091  JMP Test_0.00413310
00411096  JMP <JMP.&MSVCR90D.?terminate@@YAXXZ>
0041109B  JMP <JMP.&MSVCR90D._exit>
004110A0  JMP <JMP.&KERNEL32.GetCurrentThreadId>
004110A5  JMP <JMP.&MSVCR90D._initterm>

      看看我们的程序停在了什么鬼地方,如果各位初学读者试图从这里就开始分析的话那真的很恐怖,相信30分钟内你的自信心将被打击到零……
      我们都知道其实编译器在编译我们的程序前会做很多准备工作,而这些准备工作由于涉及的东西较多且每个由此编译器生成的程序都一样,因此我们不必深究,只需快速且准确的找到main函数即可。
      但是这对于初学逆向的朋友来说也是最难的,下面我就教各位读者怎样突破这个障碍。
      想要找到main函数,那么我们就要从C语言本身讲起,在刚刚开始学习C语言的时候我们就被不幸的告知,我们的程序中必须要包含一个名字叫做main的函数,不管你多讨厌它都必须如此,后来便成了习惯……
      后来查查C99标准,发现“int main(int argc, char *argv[])”与“int main(void)”都是被接受的,然后又查查MSDN,可以清晰看到一句话“The main and main functions can take the following three optional arguments”,也就是告诉了我们main函数其实是有3个参数的,其后面的例子更是证明了这句话确实是微软写上去的:

    main( int argc, char *argv[ ], char *envp[ ] )

      嗯,他们又在标准上较劲了,但是考虑到我们大部分程序都是用vs编译的(而且Borland的C++的参数也是如此),因此我们还是做墙头草,随大流吧……

      到这里有的读者可能会感到疑惑,如果我们使用的是符合C99标准的main函数呢?例如我们源码的main函数不就是两个参数吗。但是在这里我要很负责的告诉大家,不管我们代码中实际使用了几个参数,在程序被编译时其main函数肯定是三个参数的,因为这取决于Windows系统的机制。

      因此现在已经为我们识别main函数提供了很好的特征,既有三个参数,且前两个参数为地址量的call就应该是我们的main函数了。除此之外,我们通过MSDN可知应用程序会随着main函数结束而退出,这又给了我们第二个有力的特征,既main函数很定是在程序退出代码附近的(而且目前的主流调试、反汇编工具都可以正确识别出退出函数exit)。

      有了这些特征,我们再想找到main函数就不难了,目前我为大家提供三种方法:

1.1.1、字符串搜索法

      安装完各个版本的C++编译器后,逐个写Hello World,然后用OllyDbg的搜索字符串功能搜索这个字符串,最后逐步回溯即可,下面我为大家演示一下我做的步骤。

      用OllyDbg打开目标文件后,先记住程序默认停在哪里,然后在CPU窗格点击右键,依次选择【超级字符串参考】>【查找ASCII字符】,选择我们的“Hello World”后双击即可到main函数中,代码如下:

004113A0  PUSH EBP                                ; 函数入口
004113A1  MOV EBP, ESP
004113A3  SUB ESP, 0C0
004113A9  PUSH EBX
004113AA  PUSH ESI
004113AB  PUSH EDI
004113AC  LEA EDI, DWORD PTR SS:[EBP-C0]
004113B2  MOV ECX, 30
004113B7  MOV EAX, CCCCCCCC
004113BC  REP STOSD
004113BE  MOV ESI, ESP
004113C0  PUSH Test_0.0041573C                     ; /Hello World!/r/n
004113C5  CALL DWORD PTR DS:[<&MSVCR90D.printf>]   ; /printf
004113CB  ADD ESP, 4
004113CE  CMP ESI, ESP
004113D0  CALL Test_0.00411145
004113D5  XOR EAX, EAX
004113D7  POP EDI
004113D8  POP ESI
004113D9  POP EBX
004113DA  ADD ESP, 0C0
004113E0  CMP EBP, ESP
004113E2  CALL Test_0.00411145
004113E7  MOV ESP, EBP
004113E9  POP EBP
004113EA  RETN

      我们单击选择函数入口后,可以看到CPU窗格下面的信息窗格中显示如下信息:

跳转来自 0041100F
 
      我们单击选择此信息后,点击鼠标右键,并选择【转到 JMP 来自0041100F】后即可来到上层调用函数(以后我们将之称为“返回到调用”):

0041100A  JMP <JMP.&KERNEL32.DebugBreak>
0041100F  JMP Test_0.004113A0                       ; 我们停到这里
00411014  JMP Test_0.004124E0

      遇到这种情况直接在返回到调用,此时来到真正调用main函数的地方:

0041195F  MOV EAX, DWORD PTR DS:[417148]
00411964  PUSH EAX
00411965  MOV ECX, DWORD PTR DS:[41714C]
0041196B  PUSH ECX
0041196C  MOV EDX, DWORD PTR DS:[417144]
00411972  PUSH EDX
00411973  CALL Test_0.0041100F                     ; 我们停到这里
00411978  ADD ESP, 0C
0041197B  MOV DWORD PTR DS:[41715C], EAX
00411980  CMP DWORD PTR DS:[417150], 0
00411987  JNZ SHORT Test_0.00411995
00411989  MOV EAX, DWORD PTR DS:[41715C]
0041198E  PUSH EAX                                 ; /status => 0
0041198F  CALL DWORD PTR DS:[<&MSVCR90D.exit>]     ; /exit

      通过上面的代码我们便看到了main函数的典型特征,临近exit,且有三个参数。接下来我们要做的就是不断地重复上面的步骤,一直到找到程序入口点为止。

      最后你要做的就是针对不同的版本不同城上的编译器重复上面的步骤,直到收集到你认为足够丰富的信息后结束,从此你就再也不用怕为找不到main函数而苦恼了。

1.1.2、栈回溯法

      栈回溯的方法是先找到main函数中的那个“HelloWorld”,下断点并按【F9】键运行后查看堆栈情况,我这里的堆栈情况如下:

0012FE9C   7C930208  ntdll.7C930208       ; 我们停在这里
0012FEA0   FFFFFFFF
0012FEA4   7FFDE000
0012FEA8   CCCCCCCC
    ……   ……
0012FF64   CCCCCCCC
0012FF68  /0012FFB8
0012FF6C  |00411978  返回到 Test_0.00411978 来自 Test_0.0041100F
0012FF70  |00000001
0012FF74  |003D2C60
0012FF78  |003D2D40
0012FF7C  |0A641DBC
0012FF80  |7C930208  ntdll.7C930208
0012FF84  |FFFFFFFF
0012FF88  |7FFDE000
0012FF8C  |00369E99
0012FF90  |00000000
0012FF94  |00000000
0012FF98  |00130000  ASCII "Actx "
0012FF9C  |00000000
0012FFA0  |0012FF7C
0012FFA4  |00000020
0012FFA8  |0012FFE0  指向下一个 SEH 记录的指针
0012FFAC  |0041107D  SE处理程序
0012FFB0  |0A3788D4
0012FFB4  |00000000
0012FFB8  ]0012FFC0
0012FFBC  |004117BF  返回到 Test_0.004117BF 来自 Test_0.004117D0
0012FFC0  /0012FFF0
0012FFC4   7C817077  返回到 kernel32.7C817077

      对于这些信息我们只需要关注注释前面有“返回到”三个字的,离我们最近是:

0012FF6C  |00411978  返回到 Test_0.00411978 来自 Test_0.0041100F

      鼠标单击选择该项后,按【Enter】键即可来到返回地址00411978处:

0041195F   .  A1 48714100   MOV EAX, DWORD PTR DS:[417148]
00411964   .  50            PUSH EAX
00411965   .  8B0D 4C714100 MOV ECX, DWORD PTR DS:[41714C]
0041196B   .  51            PUSH ECX
0041196C   .  8B15 44714100 MOV EDX, DWORD PTR DS:[417144]
00411972   .  52            PUSH EDX
00411973   .  E8 97F6FFFF   CALL Test_0.0041100F
00411978   .  83C4 0C       ADD ESP, 0C                              ; 我们停在这里
0041197B   .  A3 5C714100   MOV DWORD PTR DS:[41715C], EAX
00411980   .  833D 50714100>CMP DWORD PTR DS:[417150], 0
00411987   .  75 0C         JNZ SHORT Test_0.00411995
00411989   .  A1 5C714100   MOV EAX, DWORD PTR DS:[41715C]
0041198E   .  50            PUSH EAX                                 ; /status => 0
0041198F   .  FF15 80824100 CALL DWORD PTR DS:[<&MSVCR90D.exit>]     ; /exit

      此时我们又来到了这个熟悉的地方,接下来的事情就要各位读者自己发挥了(重复上面的步骤)。


1.1.3、逐步分析法

      以上讲的两种方法都是在学习与知识储备时用的,不可能收到什么实战效果。假如我们现在碰到了一个现在就需要我们分析的软件,而且它的编译环境我们以前没碰到过,这就要求我们纯手工分析并找到main函数了。

      之所以将之称为逐步分析法,是因为我们不需要阅读它代码的具体含义,而是只需要以JMP与CALL为单位逐个跟进,从而根据main函数的特征判定main函数的所在位置。

      其实这种方法有点类似于文件搜索,先搜索根目录、在逐层加深搜索其子目录,直到找到我们需要的东西。

      那我们的程序为例,我们的OEP处就是一个JMP,因此其“根目录”也就是第一层代码里是不可能有我们的main函数了,当我们跟进这个JMP后会发现如下代码:

004117B0   > /8BFF          MOV EDI, EDI
004117B2  /.  55            PUSH EBP
004117B3  |.  8BEC          MOV EBP, ESP
004117B5  |.  E8 96F8FFFF   CALL Test_0.00411050
004117BA  |.  E8 11000000   CALL Test_0.004117D0
004117BF  |.  5D            POP EBP
004117C0  /.  C3            RETN

      我们发现第二层代码里也没有我们的main函数,但是有两个CALL。因此我们跟进第一个CALL中,为了节省篇幅,我在这里就不贴出代码了,我在这里并没有发现main函数,但是发现了数个JMP与CALL。不过需要注意的是,我们一定要注意采用逐层搜索的思想,因此这里的CALL与JMP就不要再继续跟下去了,我们现在要住的是返回上一层,看看第二个CALL里是什么:

004117D0  MOV EDI, EDI
004117D2  PUSH EBP
004117D3  MOV EBP, ESP
004117D5  PUSH -2
    ……  ……
00411813  CALL Test_0.004110FF
    ……  ……
00411830  CALL DWORD PTR DS:[<&KERNEL32.Interlocke>;  kernel32.InterlockedCompareExchange
    ……  ……
0041184E  JMP SHORT Test_0.0041185D
00411850  PUSH 3E8                                 ; /Timeout = 1000. ms
00411855  CALL DWORD PTR DS:[<&KERNEL32.Sleep>]    ; /Sleep
0041185B  JMP SHORT Test_0.00411825
    ……  ……
004118EB  PUSH Test_0.004157C8                     ;  _
004118F0  PUSH 0
004118F2  PUSH 1F4
004118F7  PUSH Test_0.00415750                     ;  f
004118FC  PUSH 2
004118FE  CALL DWORD PTR DS:[<&MSVCR90D._CrtDbgRep>;  MSVCR90D._CrtDbgReportW
00411904  ADD ESP, 14
    ……  ……
00411913  PUSH 0                                   ; /NewValue = 0
00411915  PUSH Test_0.0041756C                     ; |pTarget = Test_0.0041756C
0041191A  CALL DWORD PTR DS:[<&KERNEL32.Interlocke>; /InterlockedExchange
    ……  ……
00411929  PUSH Test_0.00417590
0041192E  CALL Test_0.00411172
00411933  ADD ESP, 4
    ……  ……
0041193A  PUSH 0
0041193C  PUSH 2
0041193E  PUSH 0
00411940  CALL DWORD PTR DS:[417590]                ; 注意这里,虽然这个CALL也有三个参数,但是仔细分析一下我们就会发现
00411940                                            ; 这并不是main函数,因为main函数的后两个参数是指针,这里的0与2显然
00411940                                            ; 不符合要求。其次他也并非是临近exit的。
00411946  PUSH 1
00411948  CALL DWORD PTR DS:[<&MSVCR90D._CrtSetChe>; 
0041195F  MOV EAX, DWORD PTR DS:[417148]
00411964  PUSH EAX
00411965  MOV ECX, DWORD PTR DS:[41714C]
0041196B  PUSH ECX
0041196C  MOV EDX, DWORD PTR DS:[417144]
00411972  PUSH EDX
00411973  CALL Test_0.0041100F                     ; 终于来到我们熟悉的main函数里了!
00411978  ADD ESP, 0C
    ……  ……
0041198E  PUSH EAX                                 ; /status => 0
0041198F  CALL DWORD PTR DS:[<&MSVCR90D.exit>]     ; /exit
    ……  ……
0041199E  CALL DWORD PTR DS:[<&MSVCR90D._cexit>]   ;  MSVCR90D._cexit
    ……  ……
004119AB  JMP SHORT Test_0.004119FF
    ……  ……
004119B7  MOV ECX, DWORD PTR SS:[EBP-14]
004119BA  PUSH ECX
004119BB  MOV EDX, DWORD PTR SS:[EBP-28]
004119BE  PUSH EDX
004119BF  CALL Test_0.00411181
004119C4  ADD ESP, 8
004119C7  RETN

  • 5
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值