0%

解读 Mach-O 文件格式

框架图

Mach-O 是 Apple 系统上(包括 MacOS 以及 iOS)的可执行文件格式,类似于 windows 上的 PE 文件以及 linux 上的 ELF 文件。上图左边为官方图,右边为用 MachOView 软件打开的 Mach-O 文件图。可以非常清晰的看到,这种文件格式由文件头(Header)、加载命令(Load Commands)以及具体数据(Segment&Section)组成。下面一一介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*
* The 32-bit mach header appears at the very beginning of the object file for
* 32-bit architectures.
*/
struct mach_header {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
};
/* Constant for the magic field of the mach_header (32-bit architectures) */
#define MH_MAGIC 0xfeedface /* the mach magic number */
#define MH_CIGAM 0xcefaedfe /* NXSwapInt(MH_MAGIC) */
/*
* The 64-bit mach header appears at the very beginning of object files for
* 64-bit architectures.
*/
struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
uint32_t reserved; /* reserved */
};
/* Constant for the magic field of the mach_header_64 (64-bit architectures) */
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
#define MH_CIGAM_64 0xcffaedfe /* NXSwapInt(MH_MAGIC_64) */

以上是 Header 在代码中的定义,它在文件中的作用主要是:使系统能够快速定位其运行环境以及文件类型等等。

分析文件头的 otool 命令为: otool \-h 可执行文件 ,或者可视化强一点的 otool \-hv 可执行文件

Fat 格式的文件(既包含有32位的二进制文件,又包含有64位的二进制文件),会在两个架构的二进制文件之前(也就是最开始的部分)有一个 Fat Header,其中 magic 为 0xCAFEBABE,然后是包含有的架构的个数,以及每个架构在文件中的偏移和大小等。

filetype 以及 flags 只列举了几个比较常见的定义,还有其他的详见EXTERNAL_HEADERS/mach-o/x86_64/loader.h

Load Commands

Load Commands 是跟在 Header 后面的加载命令区,所有 commands 的大小总和即为 Header->sizeofcmds 字段,共有 Header->ncmds 条加载命令。

1
2
3
4
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};

Command 以 LC 开头,不同的加载命令有不同的专有的结构体,cmd 和 cmdsize 是都有的,分别为命令类型(即命令名称),这条命令的长度。这些加载命令告诉系统应该如何处理后面的二进制数据,对系统内核加载器和动态链接器起指导作用。如果当前 LC_SEGMENT 包含 section,那么 section 的结构体紧跟在 LC_SEGMENT 的结构体之后,所占字节数由 SEGMENT 的 cmdsize 字段给出。

Cmd 作用
LC_SEGMENT/LC_SEGMENT_64 将对应的段中的数据加载并映射到进程的内存空间去
LC_SYMTAB 符号表信息
LC_DYSYMTAB 动态符号表信息
LC_LOAD_DYLINKER 启动动态加载连接器/usr/lib/dyld程序
LC_UUID 唯一的 UUID,标示该二进制文件,128bit
LC_VERSION_MIN_IPHONEOS/MACOSX 要求的最低系统版本(Xcode中的Deployment Target)
LC_MAIN 设置程序主线程的入口地址和栈大小
LC_ENCRYPTION_INFO 加密信息
LC_LOAD_DYLIB 加载的动态库,包括动态库地址、名称、版本号等
LC_FUNCTION_STARTS 函数地址起始表
LC_CODE_SIGNATURE 代码签名信息

使用命令 otool \-l 可执行文件 可以查看加载命令区,使用 otool \-l 可执行文件 | grep cryptid 可以查看是否加密。

Segment

Mach-O 文件有多个段(Segment),每个段有不同的功能。然后每个段又分为很多小的 Section。 LC_SEGMENT 意味着这部分文件需要映射到进程的地址空间去。一般有以下段名:

__PAGEZERO: 空指针陷阱段,映射到虚拟内存空间的第一页,用于捕捉对 NULL 指针的引用。
__TEXT: 包含了执行代码以及其他只读数据。该段数据可以 VM_PROT_READ(读)、VM_PROT_EXECUTE(执行),不能被修改。

__DATA: 程序数据,该段可写 VM_PROT_WRITE/READ/EXECUTE。
__LINKEDIT: 链接器使用的符号以及其他表。

段的结构体定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct segment_command { /* for 32-bit architectures */
uint32_t cmd; /* LC_SEGMENT */
uint32_t cmdsize; /* includes sizeof section structs */
char segname[16]; /* segment name */
uint32_t vmaddr; /* memory address of this segment 段的虚拟内存地址*/
uint32_t vmsize; /* memory size of this segment 段的虚拟内存大小*/
uint32_t fileoff; /* file offset of this segment 段在文件中的偏移量*/
uint32_t filesize; /* amount to map from the file 段在文件中的大小*/
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */
uint64_t vmsize; /* memory size of this segment */
uint64_t fileoff; /* file offset of this segment */
uint64_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};

其中 nsects 字段就是表明该段中有多少个 section。文件映射的起始位置是由 fileoff 给出,映射到地址空间的 vmaddr 处。

Section

Section 是具体有用的数据存放的地方。它的结构体跟随在 LC_SEGMENT 结构体之后,LC_SEGMENT 又在 Load Commands 中,但是 segment 的数据内容是跟在 Load Commands 之后的。它的结构体为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct section { /* for 32-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint32_t addr; /* memory address of this section 该节在内存中的起始位置*/
uint32_t size; /* size in bytes of this section 该节的大小*/
uint32_t offset; /* file offset of this section 该节的文件偏移*/
uint32_t align; /* section alignment (power of 2) 字节大小对齐*/
uint32_t reloff; /* file offset of relocation entries 重定位入口的文件偏移*/
uint32_t nreloc; /* number of relocation entries 需要重定位的入口数量*/
uint32_t flags; /* flags (section type and attributes) */
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
};
struct section_64 { /* for 64-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint64_t addr; /* memory address of this section */
uint64_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
uint32_t reserved3; /* reserved */
};

其中 flag 字段分为两个部分,一个是区域类型(section type),一个是区域属性(section attributes)。其中 type 是互斥的,即只能有一个类型,而 attributes 不是互斥的,可以有多个属性。如果段(segment)中的任何一个 section 拥有属性 S_ATTR_DEBUG,那么该段所有的 section 都必须拥有这个属性。具体的flag字段内容以及意义请参考 /usr/include/mach-o/loader.h

段名为大写,节名为小写。各节的作用主要有:

__text: 主程序代码
__stub_helper: 用于动态链接的存根
__symbolstub1: 用于动态链接的存根
__objc_methname: Objective-C 的方法名
__objc_classname: Objective-C 的类名
__cstring: 硬编码的字符串

__lazy_symbol: 懒加载,延迟加载节,通过 dyld_stub_binder 辅助链接
_got: 存储引用符号的实际地址,类似于动态符号表
__nl_symbol_ptr: 非延迟加载节
__mod_init_func: 初始化的全局函数地址,在 main 之前被调用
__mod_term_func: 结束函数地址
__cfstring: Core Foundation 用到的字符串(OC字符串)

__objc_clsslist: Objective-C 的类列表
__objc_nlclslist: Objective-C 的 +load 函数列表,比 __mod_init_func 更早执行
__objc_const: Objective-C 的常量
__data: 初始化的可变的变量
__bss: 未初始化的静态变量

查看某段中某节的命令为: otool \-s __TEXT __text 可执行文件

与 IDA 的对应地址

如果用 MachOView 来查看的话,界面左上角有一个 RAW、RVA 的选项。RAW 就是指该字节相对于文件开始部分的绝对偏移,文件头部的地址是从0x000开始的。RVA 是相对于某个基地址的偏移,也就是整体的绝对偏移值再加上某个基地址,文件头部的地址是从某个值(基地址)开始的。

这个所谓的基地址其实是 LC_SEGMENT_64(_PAGEZERO) 中的 VM_Size 字段的值,因为留出这段空白页面就是为了捕获程序的空指针,以及考虑到页面对齐。IDA 中就是使用的 RVA 地址。这个地址在 armv7 中是0x4000,arm64 中是0x10000 0000。

Section(__TEXT,__text) 所在的 RVA 地址,对应的就是 IDA 解析的函数开始地址。 IDA 解析的 Mach-O 文件中的函数都位于 Section(__TEXT) 段,然后还会接着解析 Section(__DATA) 段,即 IDA 中的数据区。

LC_MAIN 加载命令中的 Entry Offset 字段 + 基地址(RVA 选项下的文件头部地址) = IDA 中左侧函数 _main 的地址。


Reference

[1] mach-o格式分析  
http://turingh.github.io/2016/03/07/mach-o%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F%E5%88%86%E6%9E%90/#
[2] 趣探 Mach-O:文件格式分析 http://www.jianshu.com/p/54d842db3f69
[3] 网易云课堂《iOS逆向与安全》

----------------------END END----------------------