Damon

宏愿纵未了 奋斗总不太晚

0%

Mach-O 文件探索

简介

Mach-O 是Mach object的缩写,常见的Mach-O文件类型有目标文件、可执行文件、Dsym文件等。了解Mach-O文件的格式,对于静态分析、动态调试、自动化测试及安全都很有意义。可以使用可视化工具来查看 Mach-O 文件,推荐使用 MachOView 软件

文件结构

Mach-O主要由三部分组成,分别是 Header、Load Command、Data。

Mach-O 头部(Header)保存了CPU架构、大小端序、文件类型、加载命令数量等一些基本信息。Header的定义:

1
2
3
4
5
6
7
8
9
struct mach_header_64 {
uint32_t magic;
cpu_type_t cputype;
cpu_subtype_t cpusubtype;
uint32_t filetype;
uint32_t ncmds;
uint32_t flags;
uint32_t reserved;
}
  • 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
2
3
4
struct load_command {
uint32_t cmd;
uint32_t cmdsize;
}
  • 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-lazylazy 符号指针。延迟符号指针用于可执行文件中调用未定义的函数,例如不包含在可执行文件中的函数,它们将会延迟加载。而针对非延迟符号指针,当可执行文件被加载同时,也会被加载。
  • __LINKEDIT:包含了动态链接库的原始数据。

Segment 的数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct segment_command_64 { 
uint32_t cmd; // 表示当前加载命令的类型
uint32_t cmdsize; // 表示当前加载命令的大小
char segname[16]; // 段名称
uint64_t vmaddr; // 段的虚拟内存地址
uint64_t vmsize; // 段的虚拟内存大小
uint64_t fileoff; // 段在文件中的偏移量
uint64_t filesize; // 段在文件中的大小
vm_prot_t maxprot; // 段所在页面的最高内存保护,八进制表示
vm_prot_t initprot; // 段所在页面的初始内存保护,八进制表示
uint32_t nsects; // 段中包含节(section)的数量
uint32_t flags; // 标识符
};

一个段可以包含0个 Section(节)或者多个 Section, Section 的数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct section_64 { 
char sectname[16]; // 节名字
char segname[16]; // 节所在的段名字
uint64_t addr; // 节的内存地址
uint64_t size; // 节的内存的大小
uint32_t offset; // 节所在的文件偏移
uint32_t align; // 节的字节对齐大小
uint32_t reloff; // 重定位入口的文件偏移
uint32_t nreloc; // 需要重定位的入口数量
uint32_t flags; // 节的类型和属性
uint32_t reserved1; // 保留字段,如果这个 section 是 __la_symbol_ptr,那么这个值就是 DST 的下标
uint32_t reserved2; // 保留字段
uint32_t reserved3; // 保留字段
};

大写代表的是段,小写代表的是节。下面列举一些常见的节

__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
2
3
4
5
int main(int argc, char * argv[]) {
printf("first print");
printf("second print");
return 0;
}

这段代码对应的 ARM64 汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
MachO`main:
0x100286170 <+0>: sub sp, sp, #0x30 ; =0x30
0x100286174 <+4>: stp x29, x30, [sp, #0x20]
0x100286178 <+8>: add x29, sp, #0x20 ; =0x20
0x10028617c <+12>: mov w8, #0x0
0x100286180 <+16>: stur wzr, [x29, #-0x4]
0x100286184 <+20>: stur w0, [x29, #-0x8]
0x100286188 <+24>: str x1, [sp, #0x10]
0x10028618c <+28>: adrp x0, 1
0x100286190 <+32>: add x0, x0, #0xf75 ; =0xf75
0x100286194 <+36>: str w8, [sp, #0xc]
-> 0x100286198 <+40>: bl 0x1002864fc ; symbol stub for: printf
0x10028619c <+44>: adrp x9, 1
0x1002861a0 <+48>: add x9, x9, #0xf81 ; =0xf81
0x1002861a4 <+52>: mov x0, x9
0x1002861a8 <+56>: bl 0x1002864fc ; symbol stub for: printf
0x1002861ac <+60>: ldr w8, [sp, #0xc]
0x1002861b0 <+64>: mov x0, x8
0x1002861b4 <+68>: ldp x29, x30, [sp, #0x20]
0x1002861b8 <+72>: add sp, sp, #0x30 ; =0x30
0x1002861bc <+76>: ret

断点在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

以上分析,可以得出所以的外部函数引用都会在 DATAla_symbol_ptr 节中产生一个占位符,设置这个占位符主要是做 lazy binding,当第一调用时,就会调用 dyld_stub_binder 进入动态链接过程,一旦找到地址后,la_symbol_ptr节的值就会被改成方法的真实地址。

Symbol

Symbol Table

首先理解符号是什么,符号是一个数据结构,包含了名称,类型, 地址等信息。符号表就存储了目标文件的符号,还包含了重定位信息,也就是说静态链接器和动态链接器在链接的过程中都会读取符号表,符号表也会提供信息给调试器。

符号的数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
struct nlist_64 {

union {
uint_32_t n_strx; // 联合体,字符串表索引
} n_un;

uint8_t n_type; // 符号类型
uint8_t n_sect; // 整数,指定这个符号可以在哪个 section 找到,如果找不到,则为 NO_SECT。
uint16_t n_desc; // 提供符号性质的附加信息。
uint64_t n_value; // 符号值,如果符号是变量或者函数,符号值就是地址。

}

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 的下标,所以寻找符号的过程就是:

  1. __la_symbol_ptr section 读取 reserved1,即 DST 的下标
  2. 因为 Dynamic Symbol Table 存储的是 Symbol 的下标,Symbol 的 n_strx 字段是 String Table 的索引,根据索引可以找到符号名

在 facebook 开源的 fishhook 的 README中有一张图就是说明这个过程,现在看的话会容易理解