Kealdish's Studio.

Mach-O 文件解析

字数统计: 3.6k阅读时长: 16 min
2018/11/10 Share

0x00 概要

Mach-OMach object 文件格式的缩写,它是一种用于记录可执行文件、对象代码、共享库、动态加载代码和内存转储的文件格式。作为 a.out 格式的替代品, Mach-O 提供了更好的扩展性,并提升了符号表中信息的访问速度。

每个 Mach-O 文件都包含一个 Mach-O 头,然后是载入命令(Load Commands),最后是数据块(Data)。

  • Header : 保存了 Mach-O 的一些基本信息,包括了平台、文件类型、 LoadCommands 的个数等等。
  • LoadCommands:这一段紧跟 Header ,加载 Mach-O 文件时会使用这里的数据来确定内存的分布。
  • Data:每一个 segment 的具体数据都保存在这里,这里包含了具体的代码、数据等等。

注意:如果是一个 Fat Mach-O 文件,则它的结构会稍有不同,会多出一个 Fat Header

为了更好地说明 Mach-O 的文件格式,下面会借助于 MachOView 来展示其内部的结构。

0x01 Fat Header

Fat Header 的定义如下:

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
#include <mach/machine.h>
#define FAT_MAGIC 0xcafebabe
#define FAT_CIGAM 0xbebafeca
struct fat_header {
unsigned long magic; /* FAT_MAGIC */
unsigned long nfat_arch; /* number of structs that follow */
};
struct fat_arch {
cpu_type_t cputype; /* cpu specifier (int) */
cpu_subtype_t cpusubtype; /* machine specifier (int) */
unsigned long offset; /* file offset to this object file */
unsigned long size; /* size of this object file */
unsigned long align; /* alignment as a power of 2 */
};
#ifdef KERNEL
#include <mach/mach_types.h>
struct vnode;
/* XXX return type should be load_return_t, but mach_loader.h is not in scope */
int fatfile_getarch_affinity(struct vnode *vp, vm_offset_t data_ptr,
struct fat_arch *archret, int affinity);
int fatfile_getarch(struct vnode *vp, vm_offset_t data_ptr,
struct fat_arch *archret);
int fatfile_getarch_with_bits(struct vnode *vp, integer_t archbits,
vm_offset_t data_ptr, struct fat_arch *archret);
#endif /* KERNEL */

由代码定义我们可以看出 fat_header 主要包含两部分:

  • magic : 标识当前文件是 Fat Mach-O 文件
  • nfat_arch : 当前文件中包含的架构的数目

0x02 Header

Header 的定义如下:

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 的主要作用就是帮助系统迅速的定位 Mach-O 文件的运行环境,文件类型。

由此我们可以看出 Header 主要由以下几部分组成:

  • magic number : 确定是 32 位还是 64 位平台上的文件。 0xfeedfacf 代表 64 位, 0xfeedface 代表 32 位。
  • CPU type CPU subtype : 确定文件运行的 CPU 架构,这里是 x86_64 架构。
  • File Type : 标识文件的类型。它所有的值定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
#define MH_OBJECT 0x1 /* relocatable object file */ // 编译过程中产生的*.obj文件
#define MH_EXECUTE 0x2 /* demand paged executable file */ // 可执行二进制文件
#define MH_FVMLIB 0x3 /* fixed VM shared library file */
#define MH_CORE 0x4 /* core file */ // 崩溃时的Dump文件
#define MH_PRELOAD 0x5 /* preloaded executable file */
#define MH_DYLIB 0x6 /* dynamically bound shared library */ // 动态库
#define MH_DYLINKER 0x7 /* dynamic link editor */ // 动态链接器
#define MH_BUNDLE 0x8 /* dynamically bound bundle file */ // bundle 文件
#define MH_DYLIB_STUB 0x9 /* shared library stub for static */
/* linking only, no section contents */
#define MH_DSYM 0xa /* companion file with only debug */
/* sections */
#define MH_KEXT_BUNDLE 0xb /* x86_64 kexts */ // 内核扩展文件
  • Flags : 指定了 dyld 的加载参数,其所有参数如下:
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/* Constants for the flags field of the mach_header */
#define MH_NOUNDEFS 0x1 /* the object file has no undefined
references */
#define MH_INCRLINK 0x2 /* the object file is the output of an
incremental link against a base file
and can't be link edited again */
#define MH_DYLDLINK 0x4 /* the object file is input for the
dynamic linker and can't be staticly
link edited again */
#define MH_BINDATLOAD 0x8 /* the object file's undefined
references are bound by the dynamic
linker when loaded. */
#define MH_PREBOUND 0x10 /* the file has its dynamic undefined
references prebound. */
#define MH_SPLIT_SEGS 0x20 /* the file has its read-only and
read-write segments split */
#define MH_LAZY_INIT 0x40 /* the shared library init routine is
to be run lazily via catching memory
faults to its writeable segments
(obsolete) */
#define MH_TWOLEVEL 0x80 /* the image is using two-level name
space bindings */
#define MH_FORCE_FLAT 0x100 /* the executable is forcing all images
to use flat name space bindings */
#define MH_NOMULTIDEFS 0x200 /* this umbrella guarantees no multiple
defintions of symbols in its
sub-images so the two-level namespace
hints can always be used. */
#define MH_NOFIXPREBINDING 0x400 /* do not have dyld notify the
prebinding agent about this
executable */
#define MH_PREBINDABLE 0x800 /* the binary is not prebound but can
have its prebinding redone. only used
when MH_PREBOUND is not set. */
#define MH_ALLMODSBOUND 0x1000 /* indicates that this binary binds to
all two-level namespace modules of
its dependent libraries. only used
when MH_PREBINDABLE and MH_TWOLEVEL
are both set. */
#define MH_SUBSECTIONS_VIA_SYMBOLS 0x2000/* safe to divide up the sections into
sub-sections via symbols for dead
code stripping */
#define MH_CANONICAL 0x4000 /* the binary has been canonicalized
via the unprebind operation */
#define MH_WEAK_DEFINES 0x8000 /* the final linked image contains
external weak symbols */
#define MH_BINDS_TO_WEAK 0x10000 /* the final linked image uses
weak symbols */
#define MH_ALLOW_STACK_EXECUTION 0x20000/* When this bit is set, all stacks
in the task will be given stack
execution privilege. Only used in
MH_EXECUTE filetypes. */
#define MH_ROOT_SAFE 0x40000 /* When this bit is set, the binary
declares it is safe for use in
processes with uid zero */
#define MH_SETUID_SAFE 0x80000 /* When this bit is set, the binary
declares it is safe for use in
processes when issetugid() is true */
#define MH_NO_REEXPORTED_DYLIBS 0x100000 /* When this bit is set on a dylib,
the static linker does not need to
examine dependent dylibs to see
if any are re-exported */
#define MH_PIE 0x200000 /* When this bit is set, the OS will
load the main executable at a
random address. Only used in
MH_EXECUTE filetypes. */

条目比较长,这里介绍几个常见的:

Flag Type 含义
MH_NOUNDEFS 目标没有未定义的符号,不存在链接依赖
MH_DYLDLINK 该目标文件是 dyld 的输入文件,无法被再次的静态链接
MH_PIE 允许随机的地址空间
MH_ALLOW_STACK_EXECUTION 栈内存可执行代码,一般是默认关闭的
MH_NO_HEAP_EXECUTION 堆内存无法执行代码
MH_BINDS_TO_WEAK 最终链接文件包含弱符号
MH_WEAK_DEFINES 文件包含外部弱符号
MH_TWOLEVEL 两级名称空间绑定

0x03 Load Commands

其数据结构定义如下:

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

这些加载命令在 Mach-O 文件加载解析时,被内核加载器或者动态链接器调用,指导如何设置加载对应的二进制数据段。从内部逻辑来说,它是根据 cmd 的类型去调用相对应的函数来加载。以下列出一些常见的 cmd

Command类型 处理函数或结构体 用途
LC_SEGMENT;LC_SEGMENT_64 load_segment 将 segment 中的数据加载并映射到进程的内存空间去
LC_LOAD_DYLINKER load_dylinker 启动动态加载链接器 /usr/lib/dyld 程序
LC_UUID load_uuid 加载128-bit的唯一ID,标示该二进制文件
LC_THREAD load_thread 开启一个MACH线程,但是不分配栈空间。
LC_UNIXTHREAD load_unixthread 开启一个UNIX线程
LC_CODE_SIGNATURE load_code_signature 进行数字签名
LC_ENCRYPTION_INFO set_code_unprotect 加密二进制文件
LC_SYMTAB 加载符号表和字符串表
LC_DYSYMTAB 动态符号表信息 创建导入表,包含符号表上已被加载的数据
LC_LOAD_DYLIB struct dylib_command 加载的动态库,包括动态库地址、名称、版本号等
LC_FUNCTION_STARTS 函数地址起始表
LC_MAIN struct entry_point_command 可执行文件的主函数main()的位置

至于 cmdsize 字段是表示 command 整个大小,用于计算出到下一个 command 的偏移量。

0x04 Segments

加载数据时,主要加载的就是 LC_SEGMET 或者 LC_SEGMENT_64,其数据结构定义如下:

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
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 */
};

从定义可以看出它的大部分作用是定义了一些 Mach-O 文件的数据、地址和内存保护属性,这些数据在动态链接器加载程序时被映射到了虚拟内存中。主要要关注的是 nsects 字段,标示了 Segment 中有多少 secetionsection 是具体有用的数据存放的地方。这里提一下 vmaddr 变量, vmaddr 段的虚存地址(未偏移),由于 ALSR ,程序会在进程加上一段偏移量(slide),真实的地址 = vm address + slide

LC_SEGMENT 意味着这部分文件需要映射到进程的地址空间去。一般有以下段名:

  • __PAGEZERO : 空指针陷阱段,映射到虚拟内存空间的第一页,用于捕捉对 NULL 指针的引用。
  • __TEXT : 包含了执行代码以及其他只读数据。该段数据可以 VM_PROT_READ(读)、VM_PROT_EXECUTE(执行),不能被修改。
  • __DATA : 程序数据,该段可写 VM_PROT_WRITE/READ/EXECUTE
  • __LINKEDIT : 包含需要被动态链接器使用的信息,包括符号表、字符串表、重定位项表等。

0x05 Section

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
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 */
};

除了同样有帮助内存映射的变量外,在了解 Mach-O 格式的时候,只需要知道不同的 Section 有着不同的作用就可以了。

之所以按照“段-节”的方式组织,是因为同一个段下的节,在内存的权限相同,可以不完全按照页大小进行对齐,节省内存空间。而对外整体暴露段,在装载程序的时候完整映射成一个 vma ,可以更好的做内存对齐。

Section 作用
Text.__text 主程序代码
Text.__stubs 符号桩。本质上是一小段会直接跳入 lazybinding 的表对应项指针指向的地址的代码
Text.__stub_helper 用于动态链接的存根。上述提到的 lazybinding 的表中对应项的指针在没有找到真正的符号地址的时候,都指向这。
Text.__objc_methname 方法名列表
Text.__objc_classname 类名列表
Text.__objc_methtype 方法签名列表
Text.__cstring c 字符串
Data.__const 没有初始化过的常量
Data.__objc_classlist 类列表
Data.__objc_nlclslist Objective-C+load 函数列表,比 __mod_init_func 更早执行
Data.__data 初始化可变的数据
Data._got 存储引用符号的实际地址,类似于动态符号表
Data.__cfstring Core Foundation 用到的字符串(OC字符串)
Data.__objc_imageinfo 镜像信息
Data.__la_symbol_ptr 懒加载符号指针表,通过 dyld_stub_binder 辅助链接
Data.__nl_symbol_ptr 非懒加载符号指针表
Data.__mod_init_func 初始化的全局函数地址,在 main 之前被调用
Data.__mod_term_func 终止函数,在main返回之后调用
Data.__objc_const Objective-C 的常量
Data.__objc_selrefs 所有被使用的方法的引用

0x06 二进制文件符号查找

我们可以结合 fishhook 的一张图来分析。

这张图初看很复杂,不过它演示的是寻找符号的过程,我们根据这张图来分析一下这个过程:

  1. __DATA 段中的 lazy 符号指针表中查找某个符号,获得这个符号的偏移量 1061 ,然后在每一个 section_64 中查找 reserved1 ,通过这两个值找到 Indirect Symbol Table 中符号对应的条目
  2. Indirect Symbol Table 找到符号表指针以及对应的索引 16343 之后,就需要访问符号表
  3. 然后通过符号表中的偏移量,获取字符串表中的符号 _close

从代码角度来看,整个过程大致是这样的。首先从镜像中查找 linkedit_segment symtab_commanddysymtab_command ;在开始查找之前,要先跳过 mach_header_t 长度的位置,然后将当前指针强转成 segment_command_t ,通过对比 cmd 的值,来找到需要的 segment_command_t 。在 linkedit_segment 结构体中获得其虚拟地址以及文件偏移量,然后通过一下公式来计算当前 __LINKEDIT 段的位置:

1
slide + vmaffr - fileoff

类似地,在 symtab_command 中获取符号表偏移量和字符串表偏移量,从 dysymtab_command 中获取间接符号表(indirect symbol table)偏移量,就能够获得符号表字符串表以及间接符号表的引用了。

  • 间接符号表中的元素都是 uint32_t *,指针的值是对应条目 n_list 在符号表中的位置
  • 符号表中的元素都是 nlist_t 结构体,其中包含了当前符号在字符串表中的下标
1
2
3
4
5
6
7
8
9
struct nlist_64 {
union {
uint32_t n_strx; /* index into the string table */
} n_un;
uint8_t n_type; /* type flag, see below */
uint8_t n_sect; /* section number or NO_SECT */
uint16_t n_desc; /* see <mach-o/stab.h> */
uint64_t n_value; /* value of this symbol (or stab offset) */
};
  • 字符串表中的元素是 char 字符

总结一下就是:

  1. 通过 indirect_symtab + section->reserved1 获取 indirect_symbol_indices *,也就是符号表的数组
  2. 通过 (void **)((uintptr_t)slide + section->addr) 获取函数指针列表 indirect_symbol_bindings
  3. 遍历符号表数组 indirect_symbol_indices * 中的所有符号表中,获取其中的符号表索引 symtab_index
  4. 通过符号表索引 symtab_index 获取符号表中某一个 n_list 结构体,得到字符串表中的索引 symtab[symtab_index].n_un.n_strx
  5. 最后在字符串表中获得符号的名字 char *symbol_name

以上就是查找符号的整个过程。不过,以上的过程有个限定条件,那就是该符号是存在于外部动态链接库中的。如果你所要查找的符号包含于当前的镜像中,则不需要 dyld 解决函数地址的问题,它是直接从其他代码地址跳转到了当前函数的实现中。

0x07 总结

要想继续深入 iOS 底层,Mach-O 是绕不开的一环。深入了解 Mach-O 后,为后续安装包瘦身,动态修改函数指针等打下坚实的基础。

0x08 参考

Mach-O文件格式
Apple Open Source
动态修改 C 语言函数的实现
Hook 原理之 fishhook 源码解析

CATALOG
  1. 1. 0x00 概要
  2. 2. 0x01 Fat Header
  3. 3. 0x02 Header
  4. 4. 0x03 Load Commands
  5. 5. 0x04 Segments
  6. 6. 0x05 Section
  7. 7. 0x06 二进制文件符号查找
  8. 8. 0x07 总结
  9. 9. 0x08 参考