0x00 概要
Mach-O
是 Mach 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
的定义如下:
|
|
由代码定义我们可以看出 fat_header
主要包含两部分:
magic
: 标识当前文件是 FatMach-O
文件nfat_arch
: 当前文件中包含的架构的数目
0x02 Header
Header
的定义如下:
|
|
由源码可以看出 Header
的主要作用就是帮助系统迅速的定位 Mach-O
文件的运行环境,文件类型。
由此我们可以看出 Header
主要由以下几部分组成:
magic number
: 确定是 32 位还是 64 位平台上的文件。0xfeedfacf
代表 64 位,0xfeedface
代表 32 位。CPU type
CPU subtype
: 确定文件运行的 CPU 架构,这里是 x86_64 架构。File Type
: 标识文件的类型。它所有的值定义如下:
|
|
Flags
: 指定了 dyld 的加载参数,其所有参数如下:
|
|
条目比较长,这里介绍几个常见的:
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
其数据结构定义如下:
|
|
这些加载命令在 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
,其数据结构定义如下:
|
|
从定义可以看出它的大部分作用是定义了一些 Mach-O
文件的数据、地址和内存保护属性,这些数据在动态链接器加载程序时被映射到了虚拟内存中。主要要关注的是 nsects
字段,标示了 Segment
中有多少 secetion
。 section
是具体有用的数据存放的地方。这里提一下 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
的数据结构如下:
|
|
除了同样有帮助内存映射的变量外,在了解 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
的一张图来分析。
这张图初看很复杂,不过它演示的是寻找符号的过程,我们根据这张图来分析一下这个过程:
- 从
__DATA
段中的lazy
符号指针表中查找某个符号,获得这个符号的偏移量 1061 ,然后在每一个section_64
中查找reserved1
,通过这两个值找到Indirect Symbol Table
中符号对应的条目 - 在
Indirect Symbol Table
找到符号表指针以及对应的索引 16343 之后,就需要访问符号表 - 然后通过符号表中的偏移量,获取字符串表中的符号
_close
从代码角度来看,整个过程大致是这样的。首先从镜像中查找 linkedit_segment
symtab_command
和 dysymtab_command
;在开始查找之前,要先跳过 mach_header_t
长度的位置,然后将当前指针强转成 segment_command_t
,通过对比 cmd
的值,来找到需要的 segment_command_t
。在 linkedit_segment
结构体中获得其虚拟地址以及文件偏移量,然后通过一下公式来计算当前 __LINKEDIT
段的位置:
|
|
类似地,在 symtab_command
中获取符号表偏移量和字符串表偏移量,从 dysymtab_command
中获取间接符号表(indirect symbol table
)偏移量,就能够获得符号表、字符串表以及间接符号表的引用了。
- 间接符号表中的元素都是
uint32_t *
,指针的值是对应条目n_list
在符号表中的位置 - 符号表中的元素都是
nlist_t
结构体,其中包含了当前符号在字符串表中的下标
|
|
- 字符串表中的元素是
char
字符
总结一下就是:
- 通过 i
ndirect_symtab + section->reserved1
获取indirect_symbol_indices *
,也就是符号表的数组 - 通过
(void **)((uintptr_t)slide + section->addr)
获取函数指针列表indirect_symbol_bindings
- 遍历符号表数组
indirect_symbol_indices *
中的所有符号表中,获取其中的符号表索引symtab_index
- 通过符号表索引
symtab_index
获取符号表中某一个n_list
结构体,得到字符串表中的索引symtab[symtab_index].n_un.n_strx
- 最后在字符串表中获得符号的名字
char *symbol_name
以上就是查找符号的整个过程。不过,以上的过程有个限定条件,那就是该符号是存在于外部动态链接库中的。如果你所要查找的符号包含于当前的镜像中,则不需要 dyld
解决函数地址的问题,它是直接从其他代码地址跳转到了当前函数的实现中。
0x07 总结
要想继续深入 iOS 底层,Mach-O
是绕不开的一环。深入了解 Mach-O
后,为后续安装包瘦身,动态修改函数指针等打下坚实的基础。
0x08 参考
Mach-O文件格式
Apple Open Source
动态修改 C 语言函数的实现
Hook 原理之 fishhook 源码解析