4、Mach-O头结构:mach_header与mach_header_64的字段解析

Mach-O(Mach Object)文件格式是macOS、iOS、tvOS等Apple平台的可执行文件、目标代码、动态库和核心转储文件的标准容器。当我们使用Ghidra、Hopper或LLDB对iOS应用进行逆向分析时,第一步接触的二进制数据就是Mach-O文件头。文件头不仅定义了文件的“身份信息”(CPU架构、文件类型),还指明了后续加载命令(Load Commands)的起始位置和总大小。理解mach_header与mach_header_64的每一个字段,是后续解析Load Commands、Segment、Symbol Table的基石。本章将深入拆解这些字段的位级含义,并通过十六进制工具(如xxd、hexdump)实际查看一个iOS二进制文件的头部数据,建立从字节到结构的映射直觉。

Mach-O 文件头结构解析(64位) mach_header_64 (32 bytes) magic 0xFEEDFACF cputype 0x0100000C (ARM64) cpusubtype 0x00000000 filetype 0x00000002 (EXECUTE) ncmds 0x0000001F (31) sizeofcmds 0x00000D68 flags 0x00200085 偏移 32 字节后 Load Commands (ncmds 个,总大小 sizeofcmds 字节) LC_SEGMENT_64 LC_SYMTAB LC_DYLD_INFO_ONLY LC_CODE_SIGNATURE Data 区域(__TEXT, __DATA, __LINKEDIT 等 Segment 内容) 由 Load Commands 中的 LC_SEGMENT_64 描述文件偏移与内存地址的映射关系 Ghidra 通过解析头部字段自动加载并重定位所有 Segment

上图展示了mach_header_64在二进制文件中的位置及其与后续Load Commands、Data区域的关系。在实际逆向工程中,我们通常先通过otool -h或Ghidra的导入视图快速获取这些字段的值,但若要深入理解Ghidra的自动分析结果,或者手动修复损坏的二进制文件,就必须掌握每个字段的原始字节含义。

4.1 核心原理:mach_header与mach_header_64的字段定义

Apple在<mach-o/loader.h>中定义了两种头部结构。32位架构(如armv7)使用mach_header,64位架构(如arm64)使用mach_header_64。两者的字段名称完全相同,但mach_header_64在flags之后多了一个reserved字段(4字节),用于将来的扩展,因此头部总大小从28字节变为32字节。

字段名类型(32位)类型(64位)偏移(64位)说明
magicuint32_tuint32_t0x00魔数,用于识别文件格式和字节序
cputypecpu_type_tcpu_type_t0x04CPU类型,如ARM64、X86_64
cpusubtypecpu_subtype_tcpu_subtype_t0x08CPU子类型,如ARM64E、ARM64_V8
filetypeuint32_tuint32_t0x0C文件类型,如MH_EXECUTE、MH_DYLIB
ncmdsuint32_tuint32_t0x10Load Commands的数量
sizeofcmdsuint32_tuint32_t0x14所有Load Commands的总字节数
flagsuint32_tuint32_t0x18动态链接器标志位
reserved—uint32_t0x1C保留字段,64位架构专用

其中magic字段是最先被读取的4个字节。对于64位Mach-O文件,其值为0xFEEDFACF(小端序存储为CF FA ED FE);对于32位文件,值为0xFEEDFACE(小端序CE FA ED FE)。如果读取到的字节序相反(CF FA ED FE),则说明文件采用大端序,但Apple平台几乎全部使用小端序,因此逆向时通常直接按小端解析。

4.2 判断方法:通过十六进制工具实际查看头部

在iOS逆向中,我们经常需要从越狱设备或IPA包中提取Mach-O二进制文件。以下操作流程演示如何使用xxd和otool查看头部字段。

  1. 提取二进制:从越狱设备的/Applications/目录或IPA解压后的Payload/*.app/中获取主可执行文件。
  2. 查看原始字节:执行xxd -l 64 -g 4 WeChat,输出前64字节,每4字节一组显示。
  3. 对照解析:将输出的十六进制与mach_header_64字段逐一对应。
  4. 交叉验证:使用otool -h WeChat查看系统解析后的字段值,与手动解析结果比对。
# 实际输出示例(节选)
$ xxd -l 64 -g 4 WeChat
00000000: cffaedfe 0c000001 00000000 02000000  ................
00000010: 1f000000 680d0000 85002000 00000000  ....h....... .....
00000020: 4c434e55 00000000 00000000 00000000  LCNU............
00000030: 00000000 00000000 00000000 00000000  ................

对照解析:cffaedfe为magic(小端序0xFEEDFACF);0c000001为cputype(0x0100000C,即ARM64);00000000为cpusubtype(0);02000000为filetype(0x2,MH_EXECUTE);1f000000为ncmds(0x1F,31个命令);680d0000为sizeofcmds(0xD68,3448字节);85002000为flags(0x00200085)。

4.3 案例拆解:解析一个iOS应用二进制头部

假设我们正在逆向一个名为“SampleApp”的iOS应用,目标是从其二进制中提取关键信息以定位入口函数。我们使用Ghidra导入该文件,但为了验证Ghidra的解析是否正确,我们手动检查头部。

背景与约束:SampleApp为arm64架构,未加密(或已通过frida-ios-dump脱壳)。文件大小约50MB,我们只关心头部32字节。

操作步骤:

  1. 使用otool -h SampleApp得到系统解析结果:MH_MAGIC_64 ARM64 MH_EXECUTE 31 commands 3448 bytes。
  2. 使用xxd -l 32 -g 4 SampleApp查看原始字节,手动计算每个字段。
  3. 在Ghidra的“Symbol Tree”中查看__mh_execute_header符号,其地址指向文件头在内存中的映射位置。Ghidra会自动将头部字段映射为结构体,我们可以在“Data Type Manager”中展开mach_header_64查看每个字段的值。
  4. 对比三者结果,确认一致后,继续分析Load Commands中的LC_SEGMENT_64,找到__TEXT段的vmaddr和fileoff,从而定位入口点start函数。

结果与复盘:通过手动解析头部,我们不仅验证了Ghidra的自动分析,还理解了ncmds和sizeofcmds如何决定Load Commands的遍历边界。如果ncmds被篡改(例如恶意软件混淆),Ghidra可能无法正确解析后续结构,此时手动修复头部字段是恢复分析的关键。

4.4 常见误区与总结复盘

误区1:混淆32位与64位头部大小。许多初学者在解析arm64二进制时仍按28字节读取头部,导致后续所有偏移错位。务必检查magic值:0xFEEDFACE为32位,0xFEEDFACF为64位。

误区2:忽略字节序。虽然Apple平台几乎都是小端序,但在处理从网络或嵌入式设备获取的Mach-O文件时,应检查magic的字节序。若xxd输出为fe ed fa ce,则说明是大端序,需要交换字节序解析。

误区3:将filetype与magic混淆。filetype字段决定文件是主可执行文件(MH_EXECUTE=2)、动态库(MH_DYLIB=6)还是bundle(MH_BUNDLE=8)。在逆向中,动态库的入口分析方式与主程序不同。

经验总结:在Ghidra中,可以通过Window → Memory Map查看文件头映射到内存的起始地址。通常__TEXT段的vmaddr为0x100000000(arm64),文件头偏移0处对应内存地址0x100000000。利用这一映射关系,可以直接在Ghidra的Listing视图中跳转到0x100000000,以结构体形式查看头部字段。

掌握mach_header_64的字段解析,是后续学习Load Commands(第5章)、Segment布局(第6章)和符号表(第7章)的前提。在实际逆向中,我们通常不会手动逐字节解析,但理解这些字段的底层含义,能帮助我们在Ghidra自动分析失败时快速定位问题,也能让我们更深刻地理解iOS二进制的加载过程。