简介
Mach-O 是Mach object的缩写,常见的Mach-O文件类型有目标文件、可执行文件、Dsym文件等。了解Mach-O文件的格式,对于静态分析、动态调试、自动化测试及安全都很有意义。可以使用可视化工具来查看 Mach-O 文件,推荐使用 MachOView 软件
文件结构
Mach-O主要由三部分组成,分别是 Header、Load Command、Data。
Header
Mach-O 头部(Header)保存了CPU架构、大小端序、文件类型、加载命令数量等一些基本信息。Header的定义:
1 | struct mach_header_64 { |
- magic:魔数,用于标识当前设备是大端还是小端,iOS是小端序。对于64位架构的程序,它被定义为常量 MH_MAGIC_64,值为 0xfeedfacf
- cputype: 标识 CPU 的架构,如ARM、ARM64、X86_64等。
- cpusubtype: 标识具体的CPU类型,区别不同版本处理器
- filetype: 表示 Mach-O 的文件类型,如可执行文件、动态库文件、符号文件等
- ncmds: Load Commands(加载命令)的数量
- flags: 标志信息,例如是否启动 ASLR
- reserved: 保留字段
Load Commands
Load Commands 紧跟在 Header 之后,描述文件在虚拟内存中的逻辑结构、布局。告诉加载器如何处理二进制数据,有些命令是由内核处理的,有些是由动态库链接器(dyld)处理的。定义如下:
1 | struct load_command { |
- cmd: 表示当前加载命令的类型
- cmdsize: 表示当前加载命令的大小
根据不同的加载命令类型,内核会使用不同的函数来解析,cmd 包含以下部分:
- LC_SEGMENT_64:段加载命令,定义一个段,加载后被映射到内存中,段中包含节
- LC_DYLD_INFO_ONLY:记录了动态链接的重要信息,动态链接库要根据它来进行地址重定向。
- LC_SYMTAB:文件所使用的符号表
- LC_DYSYMTAB:动态库所使用的符号表
- LC_LOAD_DYLINKER:默认的加载器路径
- LC_UUID:Mach-O文件的唯一标识,dSYM文件都存在这个值
- LC_CODE_SIGNATURE:代码签名信息
DATA
表示数据,由结构图可知,Data 是由 Segment 和 Section 组成,示例图包含4个段:
- __PAGEZERO:作为第一个段,它的大小为 4GB。这 4GB 并不是文件的真实大小,但是规定了进程地址空间的前 4GB 被映射为不可执行、不可写和不可读。这就是为什么当读写一个
NULL
指针或更小的值时会得到一个EXC_BAD_ACCESS
错误。 - __TEXT:包含了被执行的代码。它被以只读和可执行的方式映射。进程被允许执行这些代码,但是不能修改。在这个段中,__text 节包含了主程序代码,__stubs 和 __stub_helper 是给 dyld 用的,用于帮助动态链接器绑定符号
- __DATA:包含了可读写数据,nl_symbol_ptr 和 __la_symbol_ptr,它们分别是 non-lazy 和 lazy 符号指针。延迟符号指针用于可执行文件中调用未定义的函数,例如不包含在可执行文件中的函数,它们将会延迟加载。而针对非延迟符号指针,当可执行文件被加载同时,也会被加载。
- __LINKEDIT:包含了动态链接库的原始数据。
Segment 的数据结构:
1 | struct segment_command_64 { |
一个段可以包含0个 Section(节)或者多个 Section, Section 的数据结构:
1 | struct section_64 { |
大写代表的是段,小写代表的是节。下面列举一些常见的节
__TEXT 段:
- __text: 主程序代码
- __stubs: 占位代码
- __stubs_helper:用于帮助动态链接器绑定符号
- __const:const 关键字修饰的变量
- __cstring:C语言字符串
- __objc_classname: OC类名
- __objc_methname:OC方法名
- __objc_methtype:OC方法类型
__DATA 段:
- __data: 初始化数据
- __common: 未初始化的符号声明
- __bss: 未初始化的全局变量
- __cfstring: Core Foundation 字符串
- __got: 非懒绑定符号指针表
- __la_symbol_ptr: 懒绑定符号指针表
- __objc_classlist: OC类列表
- __objc_catlist: OC 分类列表
- __objc_protolist: OC 协议列表
- __objc_selrefs: OC SEL 列表
- __objc_ivar: OC 类的实例变量
ASLR
ASLR 是 iOS4.3开始引入的技术,全称是 Address space layout randomization
,翻译过来就是“地址空间布局随机化”,是一种针对缓冲区溢出的安全保护技术,如果未开启 ASLR,__PAGEZERO 段的起始地址就是固定的0x0,那么黑客就很容易就可以找到函数的地址。ASLR 技术就使得这个起始地址是随机的。
可以通过 LLDB 指令:image list -o -f
来查看起始地址。
PIC
PIC 的全称是 Position Independ code(地址无关代码),是 Mach-O 动态链接使用的技术,如果是第一次使用延迟绑定符号(也就是在 __la_symbol_ptr
区的符号),会先跳转到 stub
区,然后通过 _stub_helper
来调用 dyld_stub_binder
去找到符号真正的地址。
下面来验证这个过程:
1 | int main(int argc, char * argv[]) { |
这段代码对应的 ARM64 汇编代码:
1 | MachO`main: |
断点在12行,bl
指令可以理解为函数调用。用 MachOView 打开可执行文件, 选中 stubs
stub
区的地址和汇编的对不上,是因为 MachOView 是没有 ASLR 的,我们可以试下查看开启了 ASLR 的随机初始地址。
计算得出 280000 + 1000064FC = 0x1002864fc
可以用 x/8i 0x1002864fc
指令来查看这个它到底做了什么事:
br
也是一个跳转指令,跳到 x16
寄存器的地址, 值是 0x100286574, 这个值就是对应 __la_symbol_ptr
区的值。
继续跟踪 0x100286574 的实现:
b
也是跳转指令,首先直接跳转到 0x100286508
这个地址,这个其实就是
会发现最终会跳到 dyld_stub_binder
这个函数,这个函数会找到 printf
的真实地址。
当我们过掉这个断点,也就是 first print
打印了之后,再试一下 x/8i 0x1002864fc
第二次调用 printf
会直接跳转到它的真实地址,不会再调用 dyld_stub_binder
。
以上分析,可以得出所以的外部函数引用都会在 DATA
段 la_symbol_ptr
节中产生一个占位符,设置这个占位符主要是做 lazy binding,当第一调用时,就会调用 dyld_stub_binder
进入动态链接过程,一旦找到地址后,la_symbol_ptr
节的值就会被改成方法的真实地址。
Symbol
Symbol Table
首先理解符号是什么,符号是一个数据结构,包含了名称,类型, 地址等信息。符号表就存储了目标文件的符号,还包含了重定位信息,也就是说静态链接器和动态链接器在链接的过程中都会读取符号表,符号表也会提供信息给调试器。
符号的数据结构:
1 | struct nlist_64 { |
String Table
符号的 n_strx
字段是存储了符号的名字在 Sting Table 的索引。String Table 的格式很简单,就是一个个字符串。
Dynamic Symbol Table
Dynamic Symbol Table 是动态链接器(dyld)需要的符号表,在对象被加载的时候映射到进程的地址空间,DST 只存储了符号位于Symbol Table 的下标,可以说 DST 是 Symbol Table 的子集。
寻找符号过程
前面提过,如果section是 __la_symbol_ptr
,那么 reserved1
属性的值就是 DST 的下标,所以寻找符号的过程就是:
- 从
__la_symbol_ptr
section 读取reserved1
,即 DST 的下标 - 因为 Dynamic Symbol Table 存储的是 Symbol 的下标,Symbol 的
n_strx
字段是 String Table 的索引,根据索引可以找到符号名
在 facebook 开源的 fishhook 的 README中有一张图就是说明这个过程,现在看的话会容易理解