第三章 可执行文件格式:PE、ELF、Mach-O的结构解析
可执行文件格式是二进制安全与逆向工程的基石。无论是分析恶意软件、挖掘漏洞还是进行软件破解,第一步都是理解目标文件在磁盘上如何组织、加载器如何将其映射到内存、以及系统如何解析其依赖与符号。Windows的PE(Portable Executable)、Linux/Android的ELF(Executable and Linkable Format)以及macOS/iOS的Mach-O,构成了当今三大主流操作系统下的二进制事实标准。本章将深入解析这三种格式的节区(Section)、段(Segment)、导入导出表与重定位表,帮助读者建立从文件字节到内存映像的完整映射思维。
上图展示了三种格式在宏观结构上的对应关系。尽管它们的头部长相各异,但核心思想一致:通过头部描述文件布局,通过节区/段划分权限与内容,通过表结构管理符号与重定位。下面我们将逐一拆解。
3.1 PE格式:Windows下的可移植可执行文件
核心原理:从DOS头到节区的层层封装
PE格式起源于Unix的COFF(Common Object File Format),但微软对其进行了扩展。一个完整的PE文件由以下部分组成:DOS头(64字节)、DOS Stub、NT头(包含Signature、FileHeader、OptionalHeader)、节表(Section Table)以及节区数据。DOS头中的e_lfanew字段(偏移0x3C)指向NT头的起始位置,这是解析PE的入口点。
NT头中的OptionalHeader虽然名为“可选”,但在PE文件中是必需的。它定义了入口点地址(AddressOfEntryPoint)、镜像基址(ImageBase,通常为0x00400000)、节区对齐(SectionAlignment)和文件对齐(FileAlignment)。节区对齐决定了节区在内存中的布局粒度(通常为0x1000即4KB),而文件对齐决定了节区在磁盘上的存储粒度(通常为0x200即512字节)。两者不同,意味着磁盘上的文件字节与内存中的映像并非一一对应,加载器需要根据对齐值进行展开。
IMAGE_OPTIONAL_HEADER结构体(如代码段、数据段、栈段等属性描述),这与ELF中的“段”概念不同。在逆向分析时,我们更关注节区,因为节区承载了实际的代码与数据。
判断方法:使用工具解析PE结构
在Windows环境下,我们可以使用dumpbin(Visual Studio自带)、PE-bear、CFF Explorer或010 Editor等工具。以dumpbin /headers命令为例,可以查看文件头、节区表、导入表等关键信息。对于自动化分析,Python的pefile库是最常用的选择。
import pefile
pe = pefile.PE('C:\\Windows\\System32\\notepad.exe')
print(pe.FILE_HEADER.Machine) # 0x8664 表示x64
print(pe.OPTIONAL_HEADER.ImageBase) # 0x140000000
for section in pe.sections:
print(section.Name, hex(section.VirtualAddress), hex(section.Misc_VirtualSize), section.Characteristics)
# 遍历导入表
if hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
for entry in pe.DIRECTORY_ENTRY_IMPORT:
print(entry.dll.decode())
for imp in entry.imports:
print(' ', imp.name.decode() if imp.name else hex(imp.ordinal))
通过上述代码,我们可以快速获取节区名称、虚拟地址、虚拟大小以及节区属性(如可读、可写、可执行)。节区属性是逆向分析的重要线索:一个同时具有可写和可执行属性的节区(如旧的UPX壳),往往暗示着代码自修改或加壳行为。
案例拆解:分析一个加壳PE的导入表
假设我们拿到一个疑似加壳的样本sample.exe。初始现象是:使用pefile加载后,发现导入表只有LoadLibraryA和GetProcAddress两个函数,且节区名称被修改为UPX0、UPX1。这强烈提示使用了UPX加壳。分析判断过程如下:
- 确认壳类型:检查节区名称和入口点代码。UPX壳的入口点通常是一条
pushad指令,随后是call指令跳转到解压代码。 - 脱壳操作:使用
upx -d sample.exe尝试脱壳。如果成功,再次解析PE,导入表将恢复为完整的系统API列表(如CreateFileW、WriteFile等)。 - 结果对比:脱壳前,文件大小为100KB;脱壳后为300KB。脱壳前节区数为2,脱壳后为5(.text、.rdata、.data、.rsrc、.reloc)。
这个案例说明,导入表是判断加壳与否的重要指标。加壳程序为了减小体积或隐藏逻辑,会压缩原始导入表,仅保留最少的加载函数。
常见误区与复盘
- 误区1:认为
ImageBase就是程序加载后的实际基址。实际上,由于ASLR(地址空间布局随机化),系统可能将PE加载到其他地址。此时,重定位表(.reloc)就发挥作用了。如果程序禁用了ASLR(如某些老旧恶意软件),则基址固定。 - 误区2:忽略
SizeOfRawData与VirtualSize的区别。在磁盘上,节区数据按FileAlignment对齐;在内存中,按SectionAlignment对齐。如果VirtualSize大于SizeOfRawData,则多出的部分在内存中被填充为0(通常为.bss节区)。 - 复盘:在分析PE时,务必先检查
Characteristics字段。例如,0xE0000020表示代码节区可读、可执行、不可写;0xC0000040表示数据节区可读、可写、不可执行。若发现代码节区可写,需警惕堆喷射或代码注入。
3.2 ELF格式:Linux与Android的通用格式
核心原理:节区与段的双重视角
ELF格式最独特的设计在于它同时提供了节区视图(Section View)和段视图(Segment View)。节区视图用于链接和调试(由节头表描述),段视图用于加载和执行(由程序头表描述)。一个段(Segment)可以包含多个节区(Section),例如PT_LOAD段通常包含.text、.rodata、.data等节区。这种设计使得加载器无需关心节区的具体划分,只需按段进行内存映射即可。
ELF头部的e_ident数组包含魔数(0x7F 0x45 0x4C 0x46)、位数(32/64)、字节序(小端/大端)等信息。e_type字段区分文件类型:ET_REL(可重定位文件,如.o)、ET_EXEC(可执行文件)、ET_DYN(共享对象,如.so,也用于PIE可执行文件)。
判断方法:readelf与objdump实战
在Linux下,readelf是解析ELF的利器。以下命令展示了如何查看节区与段:
# 查看ELF头
readelf -h /bin/ls
# 查看程序头(段)
readelf -l /bin/ls
# 查看节头(节区)
readelf -S /bin/ls
# 查看动态符号表
readelf --dyn-syms /bin/ls
# 查看重定位表
readelf -r /bin/ls
重点关注readelf -l的输出,其中Type列为LOAD的段是实际映射到内存的段。每个LOAD段都有Offset(文件偏移)、VirtAddr(虚拟地址)、PhysAddr(物理地址,通常与虚拟地址相同)、FileSiz(文件大小)和MemSiz(内存大小)。MemSiz大于FileSiz的部分会被清零,这通常对应.bss节区。
案例拆解:分析一个Linux恶意软件的动态链接与重定位
假设我们分析一个名为evil.so的共享库。初始现象:该库被一个游戏外挂程序加载,但行为异常。我们使用readelf -r evil.so查看其重定位表,发现存在大量R_X86_64_JUMP_SLOT类型的重定位,指向system和execve等危险函数。分析判断过程:
- 确认导入函数:通过
.dynsym(动态符号表)和.rela.plt(重定位表)确认该库导入了system函数。 - 追踪调用路径:在IDA中加载
evil.so,定位到引用system@plt的代码地址。发现其通过strcpy拼接用户输入,然后调用system执行命令。 - 关键输入数据:重定位表显示
system的GOT条目位于0x3fc8,PLT桩代码位于0x1050。通过断点监控GOT地址,可以捕获实际解析后的system函数地址。
这个案例展示了重定位表在逆向工程中的核心价值:它揭示了程序运行时将要绑定哪些外部函数,以及这些函数在GOT中的位置。攻击者常通过修改GOT条目来劫持控制流(GOT覆写攻击),因此分析重定位表也是漏洞挖掘的起点。
常见误区与总结
- 误区1:混淆
ET_EXEC与ET_DYN。现代Linux发行版默认编译PIE(位置无关可执行文件),其类型为ET_DYN。这意味着它的基址不固定,需要依赖重定位表在加载时修正地址。 - 误区2:忽略
.bss节区。该节区不占用文件空间,但占用内存空间。在计算恶意软件的内存占用或进行内存取证时,必须考虑.bss的大小。 - 总结:ELF的延迟绑定机制(通过PLT/GOT)是性能与安全的折中。在分析时,第一次调用外部函数会触发动态链接器解析,后续调用直接使用GOT中的地址。逆向时,可以通过修改
LD_BIND_NOW=1环境变量来强制立即绑定,便于调试。
3.3 Mach-O格式:Apple生态的二进制标准
核心原理:基于段的内存偏好设计
Mach-O格式的设计哲学与ELF类似,但更强调虚拟内存的友好性。其结构分为三部分:Header、Load Commands和Data。Header包含魔数(0xFEEDFACE(32位)或0xFEEDFACF(64位))、CPU类型(如CPU_TYPE_X86_64)、文件类型(MH_EXECUTE、MH_DYLIB等)。Load Commands是一系列变长命令,描述了文件在内存中的布局、依赖库、符号表位置等。Data区域则包含实际的节区数据。
Mach-O的段(Segment)以__开头命名,如__TEXT、__DATA、__LINKEDIT。每个段包含多个节区(Section),节区以__开头,如__text(代码)、__cstring(字符串)、__const(常量)。__LINKEDIT段包含了符号表、字符串表和动态链接信息,是逆向分析的重点。
判断方法:使用otool与MachOView
macOS自带的otool命令可以查看Mach-O结构。以下命令展示了基本用法:
# 查看头部
otool -h /bin/ls
# 查看Load Commands
otool -l /bin/ls
# 查看__TEXT段中的__text节区(反汇编)
otool -tv /bin/ls
# 查看动态依赖库
otool -L /bin/ls
对于图形化分析,MachOView是社区常用的工具。它能够直观地展示段、节区、符号表、重定位项等。在iOS逆向中,class-dump工具可以导出Objective-C的类信息,其原理就是解析__objc_classlist等节区。
案例拆解:分析一个iOS应用的Dyld绑定信息
假设我们分析一个越狱环境下的iOS应用App.ipa。初始现象:应用启动时崩溃,提示“Symbol not found: _OBJC_CLASS_$_SomeClass”。分析判断过程:
- 检查依赖库:使用
otool -L App查看其依赖的动态库列表,确认是否链接了包含SomeClass的私有框架。 - 查看绑定信息:使用
dyldinfo -bind App查看所有绑定符号。发现_OBJC_CLASS_$_SomeClass被标记为从PrivateFramework导入。 - 关键操作:在越狱设备上,使用
nm -g PrivateFramework检查该框架是否真的导出了这个符号。结果发现该符号在框架的新版本中已被移除。 - 结果对比:应用是针对旧版本SDK编译的,新系统移除了该私有API,导致dyld无法完成绑定。
这个案例说明了Mach-O的绑定(Bind)信息在排查启动崩溃中的关键作用。与ELF的PLT/GOT不同,Mach-O在较新的系统中使用Chained Fixups机制,将重定位信息以链式结构存储在__LINKEDIT段中,减少了启动时的计算开销。
常见误区与复盘
- 误区1:认为Mach-O的
__TEXT段只包含代码。实际上,__TEXT段还包含__cstring(字符串常量)、__objc_methname(Objective-C方法名)等。这些节区是提取字符串和类信息的重要来源。 - 误区2:忽略
__LINKEDIT段。该段在文件末尾,但包含了符号表(__symbol_table)、字符串表(__string_table)和代码签名。在iOS逆向中,去除代码签名或修改二进制后,需要重新计算__LINKEDIT中的哈希值,否则应用无法启动。 - 复盘:在分析Mach-O时,务必注意其通用二进制(Universal Binary)格式。一个文件可能包含多个架构(如arm64和x86_64),每个架构有独立的Header和Load Commands。使用
lipo -info可以查看包含的架构,使用lipo -thin arm64可以提取指定架构。
3.4 三种格式的对比与实战要点
| 维度 | PE | ELF | Mach-O |
|---|---|---|---|
| 头部魔数 | MZ (0x4D5A) | 0x7F 45 4C 46 | 0xFEEDFACE / 0xFEEDFACF |
| 核心结构 | DOS头 + NT头 + 节表 | ELF头 + 程序头 + 节头 | Header + Load Commands + Data |
| 段/节区关系 | 节区是基本单元,段是属性描述 | 段包含节区,段用于加载 | 段包含节区,段以页对齐 |
| 导入机制 | IAT(导入地址表) | PLT/GOT(延迟绑定) | Dyld Bind / Chained Fixups |
| 重定位表 | .reloc(基址重定位) | .rel.dyn / .rel.plt | __LINKEDIT中的Fixups |
| 常见工具 | dumpbin, PE-bear, CFF | readelf, objdump, IDA | otool, MachOView, class-dump |
在实战中,无论面对哪种格式,建议遵循以下分析流程:第一步,解析头部,确认架构、位数、入口点;第二步,查看节区/段,重点关注权限属性(可写可执行需警惕);第三步,分析导入导出表,了解程序的外部依赖;第四步,检查重定位表,理解地址修正逻辑;第五步,结合反汇编工具,定位关键代码逻辑。
.reloc节区进行数据混淆。务必在沙箱环境中进行动态分析,并校验文件的数字签名(PE)或代码签名(Mach-O)。
pefile库文档丰富。掌握PE后,再对比学习ELF和Mach-O,会发现三者概念高度相似,只是名称和布局不同。核心能力在于理解“文件字节”与“内存映像”之间的映射关系,以及“符号解析”与“重定位”的机制。