首页
社区
课程
招聘
[原创]第一章:1.1、寻找main函数入口
发表于: 2010-5-24 18:20 58559

[原创]第一章:1.1、寻找main函数入口

2010-5-24 18:20
58559
逆向的第一步是什么?这要问你学习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>;  MSVCR90D._CrtSetCheckCount
    ……  ……
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

      看到这里各位读者是不是感觉逆向很简单,但也充满挑战?如果你掌握了以上三种方法,那么恭喜你,你已经成功的走出了第一步,这很有纪念意义。

【返回到目录】:http://bbs.pediy.com/showthread.php?t=113689

[课程]Android-CTF解题方法汇总!

收藏
免费 9
支持
分享
最新回复 (78)
雪    币: 1085
活跃值: (114)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
2
第一次坐沙发,哈哈。
刚试过了,堆栈法以前没用过。

期待其它文章。
2010-5-24 20:02
0
雪    币: 216
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
呵呵,看完了,就是记不住啊
2010-5-24 20:32
0
雪    币: 13
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
hha 不错啦 楼主很是实在啊 很有用的东西啦
2010-5-24 20:51
0
雪    币: 364
活跃值: (91)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
5
不错不错 LZ 辛苦了!
2010-5-24 21:06
0
雪    币: 201
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
mark一下。。

可以慢慢看了
2010-5-25 02:17
0
雪    币: 244
活跃值: (39)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
7
,   楼主的治学很严谨,看过楼主的不少好文章。支持一下,再接再厉,哈哈。
2010-5-25 03:53
0
雪    币: 207
活跃值: (13)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
很好,楼主开始了一个新方向,将逆向系统化,条理化。
2010-5-25 08:52
0
雪    币: 883
活跃值: (314)
能力值: ( LV9,RANK:280 )
在线值:
发帖
回帖
粉丝
9
希望能借这位朋友吉言,可以让这套教程有些意义。

-------------------------------------------------------
本教程是边写作,边发表,因此各位如果觉得在整体结构上哪里需要调整,或有什么好的建议,还望各位不惜赐教,让我们在交流中提高自己,服务大家。
2010-5-25 09:09
0
雪    币: 26
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
通俗易懂 辛苦
如果能先科普下程序在执行main函数前系统都做了些啥,那就更perfect了
2010-5-25 09:17
0
雪    币: 213
活跃值: (147)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
看来逆向的门槛将会越来越低了
2010-5-25 09:38
0
雪    币: 72
活跃值: (10)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
12
如果都能高质量的完成,也算是很好很好的一件事了,论坛上这样能让我们新手看的文章,现在不是很多。谢谢LZ。
2010-5-25 12:56
0
雪    币: 242
活跃值: (418)
能力值: ( LV11,RANK:188 )
在线值:
发帖
回帖
粉丝
13
找main的主要方法是
在开始的那段中找关闭进程的API调用,exit/ exitprocess/ TerminateProcess之类。在其前的调用通常是 main

还有找DLL的入口,loadlibrary调用的lpkloaddll在调用dll入口之前是
call ebx这样调用DLL入口的,所以OD挂个DLL直接执行到此DLL main的返回位置,retn之后就得到了本机上 中loadlibrary调用  dll main的关键点,因为user32.dll总是最早载入,本机上可以是一直用。
2010-5-25 14:37
0
雪    币: 72
活跃值: (10)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
14
我个人觉得,DEBUG和RELEASE,如果从一开始就放在一起对比(工作量就大了)会比较好。

当然分析DEBUG再加上PDB是件相对比较容易的事情。

第一次分析main之前的动作,可以多次进入CALL,根据OD提示的API参考MSDN,也就大概知道编译器在main之前所做的工作了。

如果要往下深入,我想分析RELEASE是必然的,如果从一开始就熟悉编译器的优化方式,对以后的分析能够起到比较好的作用。

我也是初学中的初学,一点愚见,如果有什么问题,请谅解。
2010-5-25 20:47
0
雪    币: 883
活跃值: (314)
能力值: ( LV9,RANK:280 )
在线值:
发帖
回帖
粉丝
15
回复13楼的XPoy
      非常感谢你提出的建议,其实我也一直在想,本篇文章作为开篇,怎样写会比较好一些,应不应该带上DLL入口相关的内容。但是后来自己仔细论证了一下,我感觉第一篇文章作为整个教程的一个基调,切不可贪多。一定要做到有目的的写作,也就是说我的教程究竟想教会别人做什么。
     我感觉,我的教程所面向的对象仍然是以基础薄弱,刚入门的读者为主,因此我要做的事情仅仅是让他们了解最为重要的、最为迫切的知识。修剪枝蔓才能确立主干,既然我们的立足点是逆向工程,那么就应该尽量减少其他附加知识的,以降低阅读门槛。

回复14楼的yasm
     首先非常感谢你的反馈,你说的观点也没什么问题,对于Release版,我想在第一篇文章就出现会吓到很多读者,我在后面的教程中会逐步引入的。
2010-5-25 21:22
0
雪    币: 271
活跃值: (196)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
学习了,终于看到通俗易懂的教程了
2010-5-25 23:27
0
雪    币: 206
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17
谢谢楼主,可以系统的深层次的学习一下了
2010-5-26 08:01
0
雪    币: 235
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
18
LZ 文章写的不错 !值得期待
2010-5-26 09:23
0
雪    币: 201
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
19
楼主很强大!!!!!
2010-5-26 15:48
0
雪    币: 226
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
严重期待,希望不要等太久...学习ing
2010-5-26 16:00
0
雪    币: 100
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
21
hha 不错啦 楼主很是实在啊 很有用的东西啦
2010-5-26 16:01
0
雪    币: 883
活跃值: (314)
能力值: ( LV9,RANK:280 )
在线值:
发帖
回帖
粉丝
22
本教程计划2-3天跟新一篇(事情很多,否则就天天更新了),估计最多不会超过3个月就会更新完毕的。

如果各位初学你想的朋友能从今天开始跟着本教程一直学下去,三个月后相信你会脱胎换骨的。
2010-5-27 01:05
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
23
太好了,学习!!!!!!
2010-5-27 14:19
0
雪    币: 202
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
24
谢谢分享~~ 我新手对我很有帮助
2010-5-27 23:47
0
雪    币: 201
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
25
学习了,哈哈~
2010-5-28 16:04
0
游客
登录 | 注册 方可回帖
返回
//