- 献词
- 致谢
- 前言
- 第一部分 IDA 简介
- 第 1 章 反汇编简介
- 第 2 章 逆向与反汇编工具
- 第 3 章 IDA Pro 背景知识
- 第二部分 IDA 基本用法
- 第 4 章 IDA 入门
- 第 5 章 IDA 数据显示窗口
- 第 6 章 反汇编导航
- 第 7 章 反汇编操作
- 第 8 章 数据类型与数据结构
- 第 9 章 交叉引用与绘图功能
- 第 10 章 IDA 的多种面孔
- 第三部分 IDA 高级应用
- 第 11 章 定制 IDA
- 第 12 章 使用 FLIRT 签名来识别库
- 第 13 章 扩展 IDA 的知识
- 第 14 章 修补二进制文件及其他 IDA 限制
- 第四部分 扩展 IDA 的功能
- 第 15 章 编写 IDA 脚本
- 第 16 章 IDA 软件开发工具包
- 第 17 章 IDA 插件体系结构
- 第 18 章 二进制文件与 IDA 加载器模块
- 第 19 章 IDA 处理器模块
- 第五部分 实际应用
- 第 20 章 编译器变体
- 第 21 章 模糊代码分析
- 第 22 章 漏洞分析
- 第 23 章 实用 IDA 插件
- 第六部分 IDA 调试器
- 第 24 章 IDA 调试器
- 第 25 章 反汇编器/ 调试器集成
- 第 26 章 其他调试功能
- 附录 A 使用 IDA 免费版本 5.0
- 附录 B IDC/SDK 交叉引用
20.3 定位 main 函数
如果你足够幸运,拥有你想要分析的 C/C++ 程序的源代码,那么,最好将 main
函数作为你分析的起点,因为从理论上讲,这里是执行开始的地方。在分析二进制文件时,这是一个不错的策略。但是,如我们所知,编译器/链接器(及库的使用)增加了其他一些在 main
函数之前执行的代码,这使得问题更加复杂。因此,如果认为程序作者所写的 main
函数就是一个二进制文件的入口点,这往往并不准确。
实际上,所有程序都有一个 main
函数,这仅仅是一个 C/C++ 编译器约定,而非在编写程序时的一个无法变通的规则。如果你曾经编写过 Windows GUI 应用程序,那么你一定熟悉 main
函数的 WinMain
变体。分析 C/C++ 以外的程序时,你会发现,其他语言对它们的入口点函数使用其他的名称。无论它叫做什么,我们将这类函数统称为 main
函数。
在第 12 章中,我们讨论了 IDA 签名文件的概念,如何生成这些文件以及它们的应用。IDA 利用特殊的启动签名来识别一个程序的 main
函数。如果 IDA 能够根据其签名文件中的一个启动顺序匹配一个二进制文件的启动顺序,那么,IDA 就能够基于它对已匹配启动例程的行为的理解,定位一个程序的 main
函数。这种方法非常有效,除非 IDA 无法将一个二进制文件中的启动序列与已知的签名匹配起来。一般而言,程序的启动代码与生成该代码的编译器及该代码所针对的平台密切相关。
如第 12 章所述,启动签名被集中在一起,存储在特定于二进制文件类型的签名文件中。例如,PE 加载器使用的启动签名存储在 pe.sig 文件中,而 MS-DOS 加载器使用的启动签名则存储在 exe.sig 文件中。IDA 拥有给定二进制文件类型的签名文件,并不能完全保证它能够识别这类程序的 main
函数。由于编译器的种类极其繁多,IDA 不可能提供每一种可能的签名,也就不可能拥有每一种启动顺序。
对于许多文件类型,如 ELF 和 Mach-O,IDA 根本不包含任何启动签名。因此,IDA 也就不可能使用签名来定位一个 ELF 二进制文件中的 main
函数(尽管如果这个函数叫做 main
,IDA 就能够找到它)。
这一节讨论的重点是你让明白一个事实:有时候,你必须自己设法定位一个程序的 main
函数。在这类情况下,你需要采取一些方法了解程序如何调用 main
函数。以一个经过一定程度的模糊处理的二进制文件为例,遇到这类文件,IDA 肯定无法匹配一个启动签名,因为启动例程本身也经过模糊处理。如果你努力对该文件进行去模糊处理(第 21 章的主题),你可能不但需要自己定位 main
函数,而且需要定位原始的启动例程。
在使用传统 main
函数1 的 C 和 C++ 程序中,启动代码的一个责任是设置 main
所需的栈参数、整数 argc
(命令行参数的数量)、字符指针数组 argv
(一个指针数组,这里的指针指向包含命令行参数的字符串)以及字符指针数组 envp
(一个由指向字符串的指针构成的数组,这些字符串包含在程序调用时设置的环境变量)。下面的代码摘自一个动态链接、去除符号的 FreeBSD 8.0 二进制文件,它说明 gcc 生成的启动代码如何在 FreeBSD 系统上调用 main
函数:
1. Windows GUI 应用程序需要 WinMain
函数而非 main
函数。有关 WinMain
的文档资料,请访问 http://msdn2.microsoft.com/en-us/library/ms633559.aspx 。
.text:08048365 mov dword ptr [esp], offset _term_proc ; func .text:0804836C ➋ call _atexit .text:08048371 ➌ call _init_proc .text:08048376 lea eax, [ebp+arg_0] .text:08048379 mov [esp+8], esi .text:0804837D mov [esp+4], eax .text:08048381 mov [esp], ebx .text:08048384 ➊ call sub_8048400 .text:08048389 ➎ mov [esp], eax ; status .text:0804838C ➍ call _exit
在这段代码中,结果表明,调用 sub_804840
(➊)实际上就是调用 main
函数。这段代码是典型的启动顺序,因为它在调用 main 之前调用了初始化函数( _atexit
➋和 _init_proc
➌),而在 main
函数返回后调用了 _exit
函数(➍)。调用 _exit
可确保程序在 main
函数返回时完全终止,而不是在调用 _exit
后终止。注意,传递给 _exit
的参数(➎)是 main
函数在 EAX
中返回的值,因此,程序的退出代码为 main
函数的返回值。
如果前面的程序为静态链接且去除了符号,那么,这时的启动例程将与前面例子中的启动例程的结构相同。但是,没有一个库函数会使用对我们有用的名称。这时, main
函数仍然会“脱颖而出”,因为它是唯一一个使用 3 个参数调用的函数。当然,尽早应用 FLIRT 签名还有助于还原许多库函数的名称,并使 main
函数和前面的例子中一样凸显出来。
在不同的平台上运行时,同一个编译器可能会生成截然不同的代码,为证明这一点,我们看下面这个例子。它同样使用 gcc 创建,是一个动态链接、去除符号的、Linux 系统上的 Linux 二进制文件:
.text:080482B0 start proc near .text:080482B0 xor ebp, ebp .text:080482B2 pop esi .text:080482B3 mov ecx, esp .text:080482B5 and esp, 0FFFFFFF0h .text:080482B8 push eax .text:080482B9 push esp .text:080482BA push edx .text:080482BB ➊ push offset sub_80483C0 .text:080482C0 ➋ push offset sub_80483D0 .text:080482C5 push ecx .text:080482C6 push esi .text:080482C7 ➌ push offset loc_8048384 .text:080482CC call ___libc_start_main .text:080482D1 hlt .text:080482D1 start endp
在这个例子中, start
仅仅调用了一个函数: __libc_start_main
。调用 __libc_start_ main
的目的是执行在前面的 FreeBSD 例子中执行的所有任务,包括调用 main
函数并最终调用 exit
。由于 __libc_start_main
是一个库函数,因此,如果它知道 main
函数的确切位置,肯定是通过它的一个参数(这里它似乎有 8 个参数)获知的。很明显,其中的两个参数➊和➋是函数指针,而第三个参数➌是 .text
节中的某个位置的指针。在这个代码清单中,有几条关于哪一个函数可能为 main
函数的线索,因此,你需要分析这 3 个可能位置的代码,以正确确定 main
函数的位置。这可能是一种有益的练习。你可能还记得,传递给 __libc_start_main
的第一个参数(在栈的最顶端,因而最后被压入)实际上是一个指向 main 的指针。有两个因素阻止 IDA 将 loc_8048384
确定为函数(它可能名为 sub_8048384
)。第一个因素是它从未被直接调用,因此, loc_8048384
绝不会是一条调用指令的目标。第二个因素是,虽然 IDA 基于已识别函数的“序言”,提供了它们的“启发”(这也是 sub_80483C0
和 sub_80483D0
被确定为函数的原因,即使它们同样从未被直接调用),但是, loc_8048384
(main 函数)处的函数并没有使用 IDA 能够识别的“序言”。这段“惹事生非”的“序言”(包括注释)如下所示:
.text:08048384 loc_8048384: ; DATA XREF: start+17↑o .text:08048384 lea ecx, [esp+4] ; address of arg_0 into ecx .text:08048388 and esp, 0FFFFFFF0h ; 16 byte align esp .text:0804838B push dword ptr [ecx-4] ; push copy of return address .text:0804838E ➊ push ebp ; save caller's ebp .text:0804838F ➋ mov ebp, esp ; initialize our frame pointer .text:08048391 push ecx ; save ecx .text:08048392 ➌ sub esp, 24h ; allocate locals
很明显,这段“序言”包含一个使用 EBP 作为帧指针的函数的传统“序言”的所有要素。首先保存调用方的帧指针(➊),然后为当前函数设置帧指针(➋),最后为局部变量分配空间(➌)。IDA 的问题在于,这些操作并非作为函数中的前几项操作而发生,因此 IDA 的“启发”失效。这时,要手动创建一个函数,操作起来非常简单(Edit ▶Functions ▶Create Function),但是,你应该小心监视 IDA 的行为。就像它起初无法识别该函数一样,它可能同样无法确定该函数使用 EBP 作为帧指针。你需要编辑这个函数(ALT+P ),迫使 IDA 相信该函数使用一个基于 BP 的帧,并对专门用于保存寄存器和局部变量的栈字节的数量进行调整。
和 FreeBSD 二进制文件一样,如果前面的 Linux 实例碰巧为静态链接,并且去除了符号,那么,它的起始例程将不会有任何变化,只是 __libc_start_main
函数将失去它的名称。这时,基于 gcc 的 Linux 启动例程仅调用一个函数,且该函数的第一个参数为 main
函数的地址,你仍然能够正确定位 main
函数。
在 Windows 平台上,C/C++ 编译器的数量(因而启动例程的数量)要更多一些。令人奇怪的是,在 Windows 平台上,我们可以利用分析 gcc 在其他平台上的行为所获得的知识。下面的启动例程摘自一个 gcc/Cygwin 二进制文件:
.text:00401000 start proc near .text:00401000 .text:00401000 var_28 = dword ptr -28h .text:00401000 var_24 = dword ptr -24h .text:00401000 var_20 = dword ptr -20h .text:00401000 var_2 = word ptr -2 .text:00401000 .text:00401000 push ebp .text:00401001 mov ebp, esp .text:00401003 sub esp, 28h .text:00401006 and esp, 0FFFFFFF0h .text:00401009 fnstcw [ebp+var_2] .text:0040100C movzx eax, [ebp+var_2] .text:00401010 and ax, 0F0C0h .text:00401014 mov [ebp+var_2], ax .text:00401018 movzx eax, [ebp+var_2] .text:0040101C or ax, 33Fh .text:00401020 mov [ebp+var_2], ax .text:00401024 fldcw [ebp+var_2] .text:00401027 ➋ mov [esp+28h+var_28], offset sub_4010B0 .text:0040102E ➊ call sub_401120
很明显,这段代码与前面的 Linux 示例存在一些差异。但是,有一个地方惊人地相似:只有一个函数被调用(➊),且该函数以一个函数指针作为参数(➋)。在这个例子中, sub_401120
的作用与 __libc_start_main
相同,而 sub_4010B0
则是程序的 main
函数。
使用 gcc/MinGW 构建的 Windows 二进制文件可以使用另一种形式的 start
函数,如下所示:
.text:00401280 start proc near .text:00401280 .text:00401280 var_8 = dword ptr -8 .text:00401280 .text:00401280 push ebp .text:00401281 mov ebp, esp .text:00401283 sub esp, 8 .text:00401286 mov [esp+8+var_8], 1 .text:0040128D call ds:__set_app_type .text:00401293 ➊ call sub_401150 .text:00401293 start endp
这时,IDA 同样无法识别程序的 main
函数。关于 main
函数的位置,这段代码提供了若干条线索:只有一个非库函数被调用(➊ , sub_401150
),该函数似乎并未使用任何参数(而 main
函数应包含参数)。这时,最好的办法是继续在 sub_401150
中搜索 main
函数。 sub_401150
的一部分代码如下所示:
.text:0040122A call __p__environ .text:0040122F mov eax, [eax] .text:00401231 ➍ mov [esp+8], eax .text:00401235 mov eax, ds:dword_404000 .text:0040123A ➌ mov [esp+4], eax .text:0040123E mov eax, ds:dword_404004 .text:00401243 ➋ mov [esp], eax .text:00401246 ➊ call sub_401395 .text:0040124B mov ebx, eax .text:0040124D call _cexit .text:00401252 mov [esp], ebx .text:00401255 call ExitProcess
结果我们发现,这里的函数与我们前面看到的与 FreeBSD 有关的 start
函数有许多相似之处。 sub_401395
可能就是 main
函数,因为它是唯一一个使用 3 个参数(➋、➌和➍)调用的非库函数,而且第三个参数(➍)与库函数 __p__enviro
的返回值有关,这使我们联想到一个事实,即 main 函数的第三个参数应该是一个指向环境字符串数组的指针。虽然并未显示,但这段代码在之前还调用了 getmainargs
库函数,以在真正调用 main
函数之前设置 argc
和 argv
参数,并进一步强化一个概念: main
函数即将被调用。
Visual C/C++代码的启动例程简洁明了,如下所示:
.text:0040134B start proc near .text:0040134B call ___security_init_cookie .text:00401350 jmp ___tmainCRTStartup .text:00401350 start endp
通过应用启动签名,而非因为程序链接到一个包含给定符号的动态库,IDA 识别出两条指令引用的库例程。IDA 的启动签名能够轻松确定最初调用 main
函数的位置,如下所示:
.text:004012D8 mov eax, envp .text:004012DD mov dword_40ACF4, eax .text:004012E2 push eax ; envp .text:004012E3 push argv ; argv .text:004012E9 push argc ; argc .text:004012EF ➊ call _main .text:004012F4 add esp, 0Ch .text:004012F7 mov [ebp+var_1C], eax .text:004012FA cmp [ebp+var_20], 0 .text:004012FE jnz short $LN35 .text:00401300 push eax ; uExitCode .text:00401301 call $LN27 .text:00401306 $LN35: ; CODE XREF: ___tmainCRTStartup+169↓j .text:00401306 call __cexit .text:0040130B jmp short loc_40133B
在 tmainCRTStartup
的整个代码中, _main
是唯一一个使用 3 个参数调用的函数。通过深入分析我们发现,在调用 _main
函数之前,程序还调用了 GetCommondLine
库函数,这是另一个迹象,说明程序不久将调用 main
函数。关于如何使用启动签名的最后一点提示,需要注意的是,在这个例子中,IDA 通过匹配一个启动签名,完全靠它自己生成了 _main
这个名称。而且,ASCII 字符串 main
并未出现在这个例子使用的二进制文件中。因此,即使一个二进制文件已经被去除了符号,只要 IDA 能够匹配一个启动签名,它仍然能够发现并标识 main
函数。
下面我们将要分析的最后一个 C 编译器启动例程由 Borland 的免费命令行编译器生成2 。Borland 启动例程的最后几行代码如下所示:
2. 参见 http://forms.embarcadero.com/forms/BLL32lompilerDownload/ 。
.text:00401041 ➊ push offset off_4090B8 .text:00401046 push 0 ; lpModuleName .text:00401048 call GetModuleHandleA .text:0040104D mov dword_409117, eax .text:00401052 push 0 ; fake return value .text:00401054 jmp __startup
压入到栈上的指针值(➊)引用了一个结构体,该结构体又包含一个指向 main
函数的指针。在 __startup
中,调用 main
的设置如下所示:
.text:00406997 mov edx, dword_40BBFC .text:0040699D ➍ push edx .text:0040699E mov ecx, dword_40BBF8 .text:004069A4 ➌ push ecx .text:004069A5 mov eax, dword_40BBF4 .text:004069AA ➋ push eax .text:004069AB ➊ call dword ptr [esi+18h] .text:004069AE add esp, 0Ch .text:004069B1 push eax ; status .text:004069B2 call _exit
同样,这个例子与前面的例子有许多相似之处:调用 main
函数(➊)时使用了 3 个参数➋、➌ 和➍( __startup
中唯一一个如此调用的函数),返回值被直接传递给 _exit
,以终止程序。进一步分析 __startup
后,我们发现,程序还调用了 Windows API 函数 GetEnvironmentSrtings
和 GetCommandLine
,这通常是调用 main
函数的先兆。
最后,为了证明跟踪程序的 main 函数并不是 C 程序特有的问题,我们以下面这个已编译的 Visual Basic 6.0 程序的启动代码为例:
.text:004018A4 start: .text:004018A4 ➊ push offset dword_401994 .text:004018A9 call ThunRTMain
ThunRTMain
库函数的作用与 Linux libc_start_main
函数的作用类似,它在调用程序的 main
函数之前执行任何所需的初始化任务。为了将控制权转交给 main
函数,Visual Basic 采用一种与前面例子中的 Borland 代码非常类似的机制。 ThunRTMain
仅包含一个参数(➊),它是一个指向结构体的指针,该结构体中包含进行程序初始化所需的其他信息,包括 main
函数的地址。这个结构体的内容如下所示:
.text:00401994 dword_401994 dd 21354256h, 2A1FF0h, 3 dup(0);DATA XREF:.text:start ↑ o .text:004019A8 dd 7Eh, 2 dup(0) .text:004019B4 dd 0A0000h, 409h, 0 .text:004019C0 ➊ dd offset sub_4045D0 .text:004019C4 dd offset dword_401A1C .text:004019C8 dd 30F012h, 0FFFFFF00h, 8, 2 dup(1), 0E9h,401944h, 4018ECh .text:004019C8 dd 4018B0h, 78h, 7Dh, 82h, 83h, 4 dup(0)
在这个数据结构中,只有一项(➊)引用了代码,它就是指向 sub_4045D0
的指针。事实证明, sub_4045D0
就是程序的 main
函数。
最后,要学会如何定位 main
函数,你需要了解如何构建可执行文件。如果你遇到困难,你可以利用构建你所分析的二进制文件所使用的工具,构建一些简单的可执行文件(例如,引用 main
函数中的一个易于识别的字符串)。通过分析这些简单的文件,你将逐步了解使用一组特定的工具构建的二进制文件的基本结构,并利用这些知识,进一步分析使用同一组工具构建的更加复杂的二进制文件。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论