返回介绍

20.3 定位 main 函数

发布于 2024-10-11 21:05:48 字数 14214 浏览 0 评论 0 收藏 0

如果你足够幸运,拥有你想要分析的 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_80483C0sub_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 函数之前设置 argcargv 参数,并进一步强化一个概念: 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 函数 GetEnvironmentSrtingsGetCommandLine ,这通常是调用 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文