返回介绍

18.2 手动加载一个 Windows PE 文件

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

如果能够找到与某个文件所使用的格式有关的文档资源,那么,当你将这个文件与一个 IDA 数据库关联起来时,你面临的困难会大大降低。代码清单 18-1 显示了一个以二进制文件加载到 IDA 中的 PE 文件的前几行代码。既然无法从 IDA 获得帮助,我们求助于 PE 规范1 ,该规范指出:一个有效的 PE 文件应以一个有效的 MS-DOS 头部结构开头,而一个有效的 MS-DOS 头部结构则以 2 字节签名 4Dh 5Ah(MZ ) 开头,如代码清单 18-1 的前两行所示。

1. 参见 http://www.microsoft.com/whdc/system/platform/firmware/PECOFF.mspx (需接受 EULA )。

这时,我们需要了解 MS-DOS 头部的布局。PE 规范表明:文件中偏移量为 0x3C 的位置的 4 字节值是我们需要找到的下一个头部(PE 头部)的偏移量。你可以采用两种方法细分 MS-DOS 头部的字段:为 MS-DOS 头部中的每个字段定义适当大小的数据值,或者利用 IDA 的结构体创建功能,定义和应用一个符合 PE 文件规范的 IMAGE_DOS_HEADER 结构体。使用后一种方法可以得到下面的代码段(有所修改):

seg000:00000000                 dw 5A4Dh                 ; e_magic  
seg000:00000000                 dw 90h                   ; e_cblp  
seg000:00000000                 dw 3                     ; e_cp  
seg000:00000000                 dw 0                     ; e_crlc  
seg000:00000000                 dw 4                     ; e_cparhdr  
seg000:00000000                 dw 0                     ; e_minalloc  
seg000:00000000                 dw 0FFFFh                ; e_maxalloc  
seg000:00000000                 dw 0                     ; e_ss  
seg000:00000000                 dw 0B8h                  ; e_sp  
seg000:00000000                 dw 0                     ; e_csum  
seg000:00000000                 dw 0                     ; e_ip  
seg000:00000000                 dw 0                     ; e_cs  
seg000:00000000                 dw 40h                   ; e_lfarlc  
seg000:00000000                 dw 0                     ; e_ovno  
seg000:00000000                 dw 4 dup(0)              ; e_res  
seg000:00000000                 dw 0                     ; e_oemid  
seg000:00000000                 dw 0                     ; e_oeminfo  
seg000:00000000                 dw 0Ah dup(0)            ; e_res2  
seg000:00000000                 dd 80h                ➊ ; e_lfanew

e_lfanew 字段 (➊)的值为 80h ,表示你应该可以在数据库的偏移量为 80h (128 字节)的位置找到一个 PE 头部。分析偏移量为 80h 的位置的字节,可以确定这个 PE 头部的幻数 50h 45h(PE) ,并在数据库的偏移量为 80h 的位置构建并应用一个 IMAGE_NT_HEADERS 结构体(基于 PE 规范)。得到的 IDA 列表的一部分如下所示:

seg000:00000080        dd 4550h          ; Signature
seg000:00000080        dw 14Ch        ➋ ; FileHeader.Machine
seg000:00000080        dw 4           ➎ ; FileHeader.NumberOfSections
seg000:00000080        dd 47826AB4h      ; FileHeader.TimeDateStamp
seg000:00000080        dd 0E00h          ; FileHeader.PointerToSymbolTable
seg000:00000080        dd 0FBh           ; FileHeader.NumberOfSymbols
seg000:00000080        dw 0E0h           ; FileHeader.SizeOfOptionalHeader
seg000:00000080        dw 307h           ; FileHeader.Characteristics
seg000:00000080        dw 10Bh           ; OptionalHeader.Magic
seg000:00000080        db 2              ; OptionalHeader.MajorLinkerVersion
seg000:00000080        db 38h            ; OptionalHeader.MinorLinkerVersion
seg000:00000080        dd 600h           ; OptionalHeader.SizeOfCode
seg000:00000080        dd 400h           ; OptionalHeader.SizeOfInitializedData
seg000:00000080        dd 200h           ;  OptionalHeader.SizeOfUninitializedData
seg000:00000080        dd 1000h       ➍ ; OptionalHeader.AddressOfEntryPoint
seg000:00000080        dd 1000h          ; OptionalHeader.BaseOfCode
seg000:00000080        dd 0              ; OptionalHeader.BaseOfData
seg000:00000080        dd 400000h     ➌ ; OptionalHeader.ImageBase
seg000:00000080        dd 1000h       ➐ ; OptionalHeader.SectionAlignment
seg000:00000080        dd 200h        ➏ ; OptionalHeader.FileAlignment

前面的代码和讨论与第 8 章中介绍的 MS-DOS 和 PE 头部结构有许多相似之处。但是,在这里,我们加载文件时没有使用 PE 加载器,而且,与第 8 章不同的是,这里的头部结构对于我们成功了解数据库的其他部分非常重要。

现在,我们已经揭示了大量有用的信息,它们将帮助我们进一步了解数据库布局。首先,PE 头部中的 Machine (➋)字段指出了与构建该文件有关的目标 CPU 类型。在这个例子中,值 14Ch 表示该文件供 x86 处理器类型使用。如果这里的机器类型是其他值,如 1C0h (ARM),那么,需要关闭数据库并重新开始分析,并确保在最初的加载对话框中选择正确的处理器类型。加载数据库后,你将无法更改该数据库所使用的处理器的类型。

ImageBase (➌)字段显示已加载文件映像的基本虚拟地址。使用这个信息,我们可以将一些虚拟地址信息合并到数据库中。使用 Edit ▶Segments ▶Rebase Program 菜单项,我们可以为程序的第一段指定一个新的基址,如图 18-2 所示。

enter image description here

图 18-2 为程序指定一个新的基址

因为当一个文件以二进制模式加载时,IDA 仅创建一个段来保存整个文件,所以在当前的例子中,只有一个段存在。该对话框中的两个复选框决定在段被移动时,IDA 如何重新定位,以及 IDA 是否应移动数据库中的每一个段。对于以二进制模式加载的文件,IDA 将无法获知任何重定位信息。同样,由于程序中只有一个段,默认情况下,IDA 将重新设置整个映像的基址。

AddressOfEntryPoint (➍)字段指定程序进入点的相对虚拟地址(RAV )。RAV 是一个相对于程序基本虚拟地址的偏移量,而程序进入点表示程序中即将执行的第一条指令的地址。在这个例子中,进入点 RAV 1000h 表示程序将在虚拟地址 401000h400000h+1000h )处开始运行。这是一条非常重要的信息,因为,对于该在数据库的什么地方开始寻找代码,这是我们获得的第一个提示。但是,在查找代码之前,需要将数据库的剩余部分与相应的虚拟地址对应起来。

PE 格式利用“节”(section )来描述文件内容与内存范围之间的对应关系。通过解析文件中每节的头部,我们可以确定数据库的基本虚拟内存布局。 NumberOfSections (➎)字段指出一个 PE 文件所包含的节的数量,这里是 4。再次查阅 PE 规范可知,在 IMAGE_NT_HEADERS 结构体后面,紧跟着一个节头部结构体数组。这个数组中的每个元素都是 IMAGE_SECTION_HEADER 结构体,我们可以在 IDA 的“结构体”窗口中定义这些结构体,并将它应用于 IMAGE_NT_HEADERS 结构体后面的字节(这里共应用了 4 次)。

在讨论如何创建段之前,我们还需要注意 FileAlignment (➏)和 SectionAlignment (➐)这两个字段。这两个字段说明如何对齐2 文件中每节的数据,以及将数据映射到内存中时,如何对齐相同的数据。在我们的例子中,每节与文件中的一个 200h 字节偏移量对齐。但是,在加载到内存中时,这些节将与能够被 1000h 整除的地址对齐。在将一个可执行映像存储到文件中时,使用更小的 FileAlignment 有利于节省存储空间,而较大的 SectionAlignment 值通常对应于操作系统的虚拟内存页面大小。在数据库中手动创建节时,了解节如何对齐可帮助我们避免错误。

2. 对齐体现了一个数据块的起始地址或偏移量。这个地址或偏移量必须是对齐值的偶数倍。例如,如果数据与 200h512 )字节边界对齐,它必须以一个能够被 200h 偶数倍整除的地址(或偏移量)为起点。

创建每节头部后,我们有了足够的信息,可以开始创建数据库中的其他段。对紧跟在 IMAGE_NT_HEADERS 结构体后面的字节应用一个 IMAGE_SECTION_HEADER 模板,将生成第一个节头部,并使以下数据在示例数据库中显示出来:

seg000:00400178                 db '.text',0,0,0       ➊; Name  
seg000:00400178                 dd 440h                  ; VirtualSize  
seg000:00400178                 dd 1000h               ➌; VirtualAddress  
seg000:00400178                 dd 600h                ➍; SizeOfRawData  
seg000:00400178                 dd 400h                ➋; PointerToRawData  
seg000:00400178                 dd 0                     ; PointerToRelocations  
seg000:00400178                 dd 0                     ; PointerToLinenumbers  
seg000:00400178                 dw 0                     ; NumberOfRelocations  
seg000:00400178                 dw 0                     ; NumberOfLinenumbers  
seg000:00400178                 dd 60000020h             ; Characteristics

Name (➊)字段表明这个头部描述的是 .text 节。所有其他字段都可用于格式化数据库,但这里我们重点讨论 3 个描述节布局的字段。 PointerToRawData (➋)字段( 400h )指出可以找到节内容的位置的文件偏移量。需要注意的是,这个值是文件对齐值 200h 的整数倍。PE 文件中的节按文件偏移量(和虚拟地址)升序排列。由于这个节以文件偏移量 400h 为起点,我们可以得出结论:文件的第一个 400h 字节包含文件头部数据。因此,虽然严格来说,它们并不构成节,但是,我们可以把它们划分到数据库的一节中,以强调它们之间的逻辑关系。

Edit ▶Segments ▶Create Segment 命令用于在数据库中手动创建一个段。段创建对话框如 图 18-3 所示。

enter image description here

图 18-3 段创建对话框

在创建段时,你可以为段指定任何名称。这里我们选择 .headers ,因为它不可能被用作文件中真正的节名称,它充分地描述了节的内容。你可以手动输入段的起始(包括)和结束(不包括)地址,如果你在打开对话框之前已经指定了地址范围,IDA 将自动填写这些地址。SDK 的 segment.hpp 文件描述了段的基值。对于 x86 二进制文件,IDA 通过将段的基值向左移 4 个位,然后在字节上加上偏移量,从而计算出字节的虚拟地址( virtual=(base<<4)+offset )。如果不使用分段,则应使用基值零。段类别可以用于描述段的内容。IDA 能识别几个预定义的类别名称,如 CODEDATABSS 。segment.hpp 也描述了预定义的段类别。

遗憾的是,创建新的段会产生一个“副作用”,即被定义到段边界中的任何数据(如我们前面格式化的头部)将被取消定义。重新应用前面讨论的所有头部结构体后,我们返回到 .text 节的头部,注意 VirtualAddress (➌)字段( 1000h )是一个 RAV ,它指定应加载段内容的位置的内存地址, SizeOfRawData (➍)字段( 600h )指出文件中有多少字节的数据。换句话说,这个特殊的节头部告诉我们, .text 节是通过将文件偏移量 400h9FFh 之间的 600h 个字节映射到虚拟地址 401000h4015FFh 之间创建而成。

示例文件以二进制模式加载,因此, .text 节的所有字节出现在数据库中,我们只需要将它们移动到正确的位置即可。创建 .headers 节后,在 .headers 节的末尾部分,我们可以看到类似于下面的代码:

.headers:004003FF               db    0  
.headers:004003FF _headers      ends  
.headers:004003FF  
seg001:00400400 ; ===========================================================  
seg001:00400400  
seg001:00400400 ; Segment type: Pure code  
seg001:00400400 seg001          segment byte public 'CODE' use32  
seg001:00400400                 assume cs:seg001  
seg001:00400400                 ;org 400400h  
seg001:00400400                 assume es:_headers, ss:_headers, ds:_headers  
seg001:00400400                 db  55h ; U

在创建 .headers 节时,IDA 拆分最初的 seg000 节,构成我们指定的 .headers 节和一个新的 seg001 节,以保存 seg000 中的剩余字节。在数据库中, .text 节的内容为 seg001 节的前 600h 个字节。我们只需要将 seg001 节移动到正确的位置,并确定 .text 节的正确大小即可。

创建 .text 节的第一步是将 seg001 移动到虚拟地址 401000h 处。使用 Edit ▶ Segments▶ Move Current Segment 命令为 seg001 指定一个新的起始地址,如图 18-4 所示。

enter image description here

图 18-4 移动一个段

下一步,我们将通过 Edit ▶ Segments ▶ Create Segment 从新移动的 seg001 节的前 600h 字节中分离出.text 节。用于创建新节的参数如图 18-5 所示,它们取自节的头部值。

enter image description here

图 18-5 手动创建.text 节

记住,结束地址并不包含在地址范围内。创建 .text 节将 seg001 分割成新的 .text 节,初始文件的所有剩余字节则构成一个名为 seg002 的新节,它紧跟在 .text 节的后面。

回到节头部,可以看到第二个节,构建成一个 IMAGE_SECTION_HEADER 结构体后,它的代码如下所示:

.headers:004001A0                 db '.rdata',0,0         ; Name  
.headers:004001A0                 dd 60h                  ; VirtualSize  
.headers:004001A0                 dd 2000h                ; VirtualAddress  
.headers:004001A0                 dd 200h                 ; SizeOfRawData  
.headers:004001A0                 dd 0A00h                ; PointerToRawData  
.headers:004001A0                 dd 0                    ; PointerToRelocations  
.headers:004001A0                 dd 0                    ; PointerToLinenumbers  
.headers:004001A0                 dw 0                    ; NumberOfRelocations  
.headers:004001A0                 dw 0                    ; NumberOfLinenumbers
.headers:004001A0                 dd 40000040h            ; Characteristics

使用我们创建 .text 节时分析的数据字段,我们注意到,这个节名为 .rdata ,在以文件偏移量 0A00h 为起始地址的文件中占用 200h 个字节,并与 RVA 2000h (虚拟地址 402000h )对应。值得注意的是,现在,由于已经移走了 .text 段,我们不能再轻易地将 PointerToRawData 字段映射到数据库中的一个偏移量。我们需要以这样一个事实为依据: .rdata 节的内容紧跟在 .text 节的内容之后。换言之, .rdata 节当前位于 seg002 的前 200h 个字节中。或者可以逆向创建这些节:首先创建在头部定义的最后一个节,最后创建 .text 节。这种方法先将节放置在其正确的文件偏移量位置,然后将它们移到对应的虚拟地址。

创建 .rdata 节的方法与创建 .text 节的方法类似。第一步,将 seg002 移到 402000h ;第二步,创建 .rdata 节,其地址范围为 402000h~402200h

在这个特殊的二进制文件中定义的下一个节称为 .bss 节。 .bss 节通常由编译器生成,用于放置在程序启动时需要初始化为零的静态分配的变量(如全局变量)。具有非零初始值的静态变量通常被分配到 .data (非常量)或 .rdata (常量)节中。 .bss 节的优势在于,通常它不会占用磁盘镜像的空间,因为操作系统加载器创建可执行文件的内存镜像时,会为该节分配空间。本示例指定的 .bss 节如下所示:

.headers:004001C8                 db '.bss',0,0,0      ; Name  
.headers:004001C8                 dd 40h  ➋           ; VirtualSize  
.headers:004001C8                 dd 3000h             ; VirtualAddress  
.headers:004001C8                 dd 0  ➊             ; SizeOfRawData  
.headers:004001C8                 dd 0                 ; PointerToRawData  
.headers:004001C8                 dd 0                 ; PointerToRelocations  
.headers:004001C8                 dd 0                 ; PointerToLinenumbers  
.headers:004001C8                 dw 0                 ; NumberOfRelocations  
.headers:004001C8                 dw 0                 ; NumberOfLinenumbers  
.headers:004001C8                 dd 0C0000080h        ; Characteristics

其中节头部指出了该节在文件中的大小, SizeOfRawData ➊为零,而该节的 Virtual- Size ➋为 0x40 (64)字节。要在 IDA 中创建这个节,首先需要在以地址 0x403000 开头的地址空间中创建一个间隙(因为我们没有文件内容来填充该节),然后定义 .bss 来填补这个间隙。创建这个间隙的最简单方法是将二进制文件的剩余的节移到它们正确的位置。整个过程完成后,“段”窗口如下所示:

Name     Start    End      R W X D L Align Base Type   Class  
.headers 00400000 00400400 ? ? ? . . byte  0000 public DATA   ...  
.text    00401000 00401600 ? ? ? . . byte  0000 public CODE   ...  
.rdata   00402000 00402200 ? ? ? . . byte  0000 public DATA   ...  
.bss     00403000 00403040 ? ? ? . . byte  0000 public BSS    ...  
.idata   00404000 00404200 ? ? ? . . byte  0000 public IMPORT ...  
seg005   00404200 004058DE ? ? ? . L byte  0001 public CODE   ...

为了简单,我们省略了代码清单右侧部分。你可能已经注意到,段的结束地址与随后段的起始地址并不相邻。这是因为,在创建这些段时,使用的是它们的文件大小,而没有考虑它们的虚拟大小及所需的任何节对齐。为了使我们的段反映可执行映像的真正布局,我们可以对每个结束地址进行编辑,以填补段之间的任何间隙。

问号表示每个节的权限位的值未知。对于 PE 文件,这些值通过每个节头部的 Characteristics 字段中的位来指定。除了通过 IDC 编程或使用插件外,你没有办法为手动创建的节指定权限。下面的 IDC 语句对前面代码清单中的 .text 节设置执行权限:

SetSegmentAttr(0x401000, SEGATTR_PERM, 1);

遗憾的是,IDC 并没有为每一个被允许的权限定义符号常量。Unix 用户可以轻易记住节的权限位,它们正好与 Unix 文件系统所使用的权限位完全对应。因此,读为 4,写为 2,执行为 1。你可以使用按位 OR 在一个操作中设置几个权限来组合值。

手动加载过程的最后一步是让 x86 处理器模块为我们工作。一旦二进制文件与各种 IDA 节正确对应起来,我们就可以返回我们在头部中发现的程序入口点(RVA 1000h ,虚拟地址 401000h ),并要求 IDA 将那个位置的字节转换为代码。如果我们想要 IDA 在“Exports”(导出)窗口中将该地址列为入口点,我们必须以编程方式指定它这样做。下面是一行 Python 代码,可用于实现这一目的:

AddEntryPoint(0x401000, 0x401000, 'start', 1);

如果以这种方式调用该入口点,IDA 会将其命名为 start ,然后将其作为导出符号添加,在指定的地址创建代码,并启动递归下降以尽可能详细地分解相关代码。有关 AddEntryPoint 函数的详细信息,请参阅 IDA 的内置帮助。

如果一个文件以二进制模式加载,IDA 不会自动分析这个文件的内容。此外,IDA 也不会设法确定创建该文件所使用的编译器和该文件导入的库和函数,也不会自动在数据库中加载类型库或签名信息。你很可能需要做大量的工作,才能生成一个和 IDA 自动生成的反汇编代码清单类似的代码清单。实际上,我们甚至还没有触及 PE 头部的其他方面,以及如何将这些额外的信息合并到我们的手动加载过程中。

在结束关于手动加载的讨论之前,想象一下,每次你打开 IDA 无法识别的同一类格式的二进制文件时,你都需要重复本节讨论的每一个步骤。以后你可能会选择编写 IDC 脚本,帮助你执行一些头部解析和段创建任务,对你的一些操作进行自动化。这正是我们创建 IDA 加载器模块的动机所在,也是加载器模块的用途所在,我们将在下一节中讨论 IDA 加载器模块。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

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

发布评论

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