- 献词
- 致谢
- 前言
- 第一部分 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 交叉引用
18.2 手动加载一个 Windows PE 文件
如果能够找到与某个文件所使用的格式有关的文档资源,那么,当你将这个文件与一个 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 所示。
图 18-2 为程序指定一个新的基址
因为当一个文件以二进制模式加载时,IDA 仅创建一个段来保存整个文件,所以在当前的例子中,只有一个段存在。该对话框中的两个复选框决定在段被移动时,IDA 如何重新定位,以及 IDA 是否应移动数据库中的每一个段。对于以二进制模式加载的文件,IDA 将无法获知任何重定位信息。同样,由于程序中只有一个段,默认情况下,IDA 将重新设置整个映像的基址。
AddressOfEntryPoint
(➍)字段指定程序进入点的相对虚拟地址(RAV )。RAV 是一个相对于程序基本虚拟地址的偏移量,而程序进入点表示程序中即将执行的第一条指令的地址。在这个例子中,进入点 RAV 1000h
表示程序将在虚拟地址 401000h
( 400000h+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. 对齐体现了一个数据块的起始地址或偏移量。这个地址或偏移量必须是对齐值的偶数倍。例如,如果数据与 200h
( 512
)字节边界对齐,它必须以一个能够被 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 所示。
图 18-3 段创建对话框
在创建段时,你可以为段指定任何名称。这里我们选择 .headers
,因为它不可能被用作文件中真正的节名称,它充分地描述了节的内容。你可以手动输入段的起始(包括)和结束(不包括)地址,如果你在打开对话框之前已经指定了地址范围,IDA 将自动填写这些地址。SDK 的 segment.hpp 文件描述了段的基值。对于 x86 二进制文件,IDA 通过将段的基值向左移 4 个位,然后在字节上加上偏移量,从而计算出字节的虚拟地址( virtual=(base<<4)+offset
)。如果不使用分段,则应使用基值零。段类别可以用于描述段的内容。IDA 能识别几个预定义的类别名称,如 CODE
、 DATA
和 BSS
。segment.hpp 也描述了预定义的段类别。
遗憾的是,创建新的段会产生一个“副作用”,即被定义到段边界中的任何数据(如我们前面格式化的头部)将被取消定义。重新应用前面讨论的所有头部结构体后,我们返回到 .text
节的头部,注意 VirtualAddress
(➌)字段( 1000h
)是一个 RAV ,它指定应加载段内容的位置的内存地址, SizeOfRawData
(➍)字段( 600h
)指出文件中有多少字节的数据。换句话说,这个特殊的节头部告诉我们, .text
节是通过将文件偏移量 400h
与 9FFh
之间的 600h
个字节映射到虚拟地址 401000h
与 4015FFh
之间创建而成。
示例文件以二进制模式加载,因此, .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 所示。
图 18-4 移动一个段
下一步,我们将通过 Edit ▶ Segments ▶ Create Segment 从新移动的 seg001
节的前 600h
字节中分离出.text 节。用于创建新节的参数如图 18-5 所示,它们取自节的头部值。
图 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论