The quiter you become,the more you are able to hear!

聊聊 Mach-O 文件格式

Author: geneblue

Blog: https://geneblue.github.io/

MachO (Mach Object)文件格式是苹果 OSX 和 iOS 系统使用的可执行,可链接的 ABI(二进制)文件格式。类比 ELF 文件之于 Linux 平台,PE 文件之于 Windows 平台。同样的,MachO 文件是代码与数据的集合,体现了在苹果定义的一套规则下,程序文件是如何构成的,程序的链接,装载是如何发生的。

ABI 文件是操作系统的基石。学习认识一个新的 OS 平台,理解它的 ABI 文件是非常好的切入点,对于 OSX/iOS 系统同样如此。

官方文档永远是学习的第一手资料。这里有一份苹果出的《OSX ABI Mach-O File Format Reference》文档,奇怪的是,这份文档在苹果开发者官网竟然没找到。另外,XNU 源码中可以找到 MachO 相关代码 darwin-xnu/EXTERNAL_HEADERS/mach-o/fat.h, loader.h, nlist.h, reloc.h 这四个文件主要描述 Mach-O 结构。本文内容主要基于这两份资料。

MachO 基本结构

MachOView010editor 这两款工具可以帮我们更直观的分析 MachO 格式。

MachOView:

MachO-structure

010Editor:

010editor

Mach-O 文件基本结构如下:

mach-o_format_basic_structure

可以看出,Mach-O 主要由三部分组成: - 首先是文件头,表明该文件是 Mach-O 格式,指定目标架构,还有一些其他的文件属性信息,文件头信息影响后续的文件结构安排 - 紧随文件头的是可变大小的 Load Commands 区,这里以多组结构指定文件数据布局和关系,比如可以指定文件在虚拟内存中初始加载地址,符号表的位置,代码块的位置等。Load Commands 不是固定的,通过多种组合,Mach-O 就可以用于不同场景下,比如作为可执行文件,作为动态链接库,调试文件,目标文件,内核转储文件等。 - 接下来是 Data 区,以上两部分实际上是结构化组织,不承载具体的二进制代码和数据,Data 区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load Command,Segment 中 section 就可以被解读为是是代码,常量或者一些其他的数据类型。在装载在内存中时,也是根据 Segment 做内存映射的。

MachO64 文件头结构详情可参考 loader.h,共占用 0x20 个 byte。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* 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 */
};

macho-file-header

magic 表示魔数标识,MACHO-64 的魔数值为 0xfeedfacf

cputypecpusubtype 表示 cpu 类别信息,具体参考 osfmk/mach/machine.h

filetype 表明该 MachO 具体的文件类型,类似于 ELF 文件,是可执行文件,还是动态链接库之类的,filetype 常见类型有如下: - 0x1 MH_OBJECT: 可重定向目标文件,编译过程的之间产物,一般是 .o,静态库就是这些 .o 的归档文件 - 0x2 MH_EXECUTE: 可执行文件,有执行入口 - 0x6 MH_DYLIB: 动态库文件,常见的 .dylib 文件,还有一些动态的 framework 文件 - 0xa MH_DSYM:存储文件符号信息,用于debug ...

ncmdssizeofcmds 表示 LOAD_COMMAND 结构数量,和所有 LOAD_COMMAND 结构大小总和。

flags 字段标识文件的各种属性,可以组合多个属性的 bit 位信息,常见的 flags 有: - 0x1 MH_NOUNDEFS: 目标文件在编译时没有未定义的符号引用,目标文件是静态编译的,不会依赖其他动态库的实现 - 0x4 MH_DYLDLINK: 该文件用于动态链接,不可用在静态链接中 - 0x200000 MH_PIE: 位置无关代码,针对 MH_EXECUTE 可执行文件,os 会将可执行文件加载到随机的内存地址处 ...

reserved 保留字段,未使用

Load Commands(装载命令)

Load Command 指定了文件逻辑结构还有文件在虚拟内存中的布局,每个 Load Command 结构都会以命令类型和当前 Load Command 结构大小开始:

1
2
3
4
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
  • cmd 32位,指定命令类型,
  • cmdsize 32位,用来表示 Load Command 结构的大小。Load Command 根据命令类型,是由不同的对应的数据结构构成的,这样占据的大小也不一样。64位系统 size 大小是 8 的倍数,结尾多余空间用 0 填充。

这里介绍一些常见的装载命令,未介绍到的参考官方文档

LC_SEGMENT_64

segment 是 MachO 中最重要的 Command,这里主要包含了 MachO 中代码,常量数据等信息。segment 负责将这些代码块,数据块等内容以指定方式映射到虚拟内存的对应位置。

segment_command_64 结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 */
};

cmd 命令类型,LC_SEGMENT_64 值为 0x19

cmdsize 命令结构大小,LC_SEGMENT_64 也要根据数据类型的不同,填充对应的结构大小

segname 指定当前 segment 的数据类型,主要类型包括 __PAGEZERO, __TEXT, __DATA, __OBJC, __IMPORT, __LINKEDIT

vmaddrvmsize 指定当前 segment 在映射到内存后,虚拟内存开始地址和占据的内存大小

fileofffilesize 指定当前 segment 数据在偏移 fileoff 的长度后映射到 vmaddr 处,注意不是指在 MachO 文件中的偏移,filesize 指定要拷贝的数据大小

maxprotinitprot 指定当前 segment 映射后的初始内存属性和最大支持的内存属性,常用属性 VM_PROT_NONE, VM_PROT_READ, VM_PROT_WRITE, VM_PROT_EXECUTE

nsects 当前 segment 后紧跟多少个 section 结构

flags 影响 segment 内存加载方式,flag 值有 SG_HIGHVM, SG_FVMLIB, SG_NORELOC, SG_PROTECTED_VERSION_1

介绍一些常用的 segment 数据类型: - __PAGEZERO 一般作为 MachO 可执行文件的第一个 segment, __PAGEZERO 从虚拟地址 0 开始加载,内存属性 VM_PROT_NONE,在 LC_SEGMENT_64 中 vmsize 为 0x100000000。__PAGEZERO 只是指明了 MachO 要加载的内存地址,并无数据要填充,所以在 segment_command_64 结构中 fileoff filesize 均为 0 - __TEXT segment 包含可执行代码块和只读数据,代码和只读数据在映射后都不具备内存写属性。__TEXT 中可以包含多 section,比如 __text, __cstring, __picsymbol_stub, __symbol_stub, __const, __literal4, __literal8 - __DATA segment 包含写属性的数据,也常包含多个 section,比如 __data, __la_symbol_ptr, __nl_symbol_ptr, __dyld, __const, __mod_init_func, __mod_term_func, __bss, __common - __OBJC 提供 Objective-C 语言运行时所需要的支持库信息 - __IMPORT 只为 IA-32 架构服务 - __LINKEDIT segment 提供动态链接器需要的数据,比如符号,字符串,还有一些可重定位表信息

上面说到 __TEXT __DATA segment 中可能会包含多个 section 用于记录具体的代码和数据,section 的结构如下, section 一般 4 字节对齐:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 */
};

sectname 表明当前 section 名,section 名都是小写字母,比如 __text, __cstring, __bss, __common, __data 等

segname 当前 section 所属的 segment

addr 当前 section 映射的虚拟内存起始地址

size 映射到虚拟内存时,要拷贝的数据大小,指 section 结构所指定的具体数据(在 DATA 区)大小

offset 指这个 section 结构所指定的具体数据(在 DATA 区)在 MachO 文件中偏移,这样 loader 就可以找到具体数据在哪里了

align 指定 section 要按照多少字节对齐

reloff 重定位相关,指定首个重定位入口在 MachO 文件中偏移

nreloc 指重定位入口数量

flags 用来表示 section 属性,32bit 大小,分成两个部分,低 8 bit 表示 section 类型信息,高 24 位表示其他属性信息,这些属性信息主要提供给链接器和其他分析工具使用,详情参见文档。

**问题, __TEXT segment 中有一些常量数据,这些常量数据所在的区域是不是也具备代码执行权限?**

LC_UUID

UUID(universally unique identifier) 通用唯一标识

1
2
3
4
5
6
7
8
9
/*
* The uuid load command contains a single 128-bit unique random number that
* identifies an object produced by the static link editor.
*/
struct uuid_command {
uint32_t cmd; /* LC_UUID */
uint32_t cmdsize; /* sizeof(struct uuid_command) */
uint8_t uuid[16]; /* the 128-bit uuid */
};

uuid 存储了一个 128bit 长的具备唯一随机字符串ID,可以通过这个 ID 来标识二进制文件。如果有 UUID 相同的 dSYM 文件,就可以符号调试或者追溯崩溃时的堆栈信息。

LC_CODE_SIGNATURE LC_SEGMENT_SPLIT_INFO LC_FUNCTION_STARTS LC_DATA_IN_CODE LC_DYLIB_CODE_SIGN_DRS LC_LINKER_OPTIMIZATION_HINT

这几个装载命令使用同一个数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* The linkedit_data_command contains the offsets and sizes of a blob
* of data in the __LINKEDIT segment.
*/
struct linkedit_data_command {
uint32_t cmd; /* LC_CODE_SIGNATURE, LC_SEGMENT_SPLIT_INFO,
LC_FUNCTION_STARTS, LC_DATA_IN_CODE,
LC_DYLIB_CODE_SIGN_DRS or
LC_LINKER_OPTIMIZATION_HINT. */
uint32_t cmdsize; /* sizeof(struct linkedit_data_command) */
uint32_t dataoff; /* file offset of data in __LINKEDIT segment */
uint32_t datasize; /* file size of data in __LINKEDIT segment */
};

dataoff 数据在 MachO 文件中偏移

datasize 数据大小

LC_MAIN

1
2
3
4
5
6
7
8
9
10
11
12
/*
* The entry_point_command is a replacement for thread_command.
* It is used for main executables to specify the location (file offset)
* of main(). If -stack_size was used at link time, the stacksize
* field will contain the stack size need for the main thread.
*/
struct entry_point_command {
uint32_t cmd; /* LC_MAIN only used in MH_EXECUTE filetypes */
uint32_t cmdsize; /* 24 */
uint64_t entryoff; /* file (__TEXT) offset of main() */
uint64_t stacksize;/* if not zero, initial stack size */
};

entryoff main 函数在 MachO 文件中偏移

stacksize 一般情况下为0, 非 0 表示初始的栈大小

LC_DYLD_INFO LC_DYLD_INFO_ONLY

这两个命令主要是给动态链接器 dyld 做 link 使用的。动态 linker 的主要作用是二进制加载到内存后,将动态链接库的信息链接起来,找到使用的外部函数信息,导出提供给外部使用的函数信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct dyld_info_command {
uint32_t cmd; /* LC_DYLD_INFO or LC_DYLD_INFO_ONLY */
uint32_t cmdsize; /* sizeof(struct dyld_info_command) */

uint32_t rebase_off; /* file offset to rebase info */
uint32_t rebase_size; /* size of rebase info */

uint32_t bind_off; /* file offset to binding info */
uint32_t bind_size; /* size of binding info */

uint32_t weak_bind_off; /* file offset to weak binding info */
uint32_t weak_bind_size; /* size of weak binding info */

uint32_t lazy_bind_off; /* file offset to lazy binding info */
uint32_t lazy_bind_size; /* size of lazy binding infs */

uint32_t export_off; /* file offset to lazy binding info */
uint32_t export_size; /* size of lazy binding infs */
};

rebase 地址重定位

bind 地址绑定, 找到使用的外部符号信息

weak bind 弱引用绑定,C++ 程序需要的

lazy bind 地址懒绑定,延迟绑定,有的外部函数,不需要在加载时就完成地址绑定,可以在运行到的时候再去查找外部函数地址

export 要导出的函数信息

问题:iOS 的函数 hook 应该在这里操作吧?

LC_LOAD_DYLINKER

这个命令指定动态链接器 linker 路径

1
2
3
4
5
6
7
8
9
10
11
12
13
union lc_str {
uint32_t offset; /* offset to the string */
#ifndef __LP64__
char *ptr; /* pointer to the string */
#endif
};

struct dylinker_command {
uint32_t cmd; /* LC_ID_DYLINKER, LC_LOAD_DYLINKER or
LC_DYLD_ENVIRONMENT */
uint32_t cmdsize; /* includes pathname string */
union lc_str name; /* dynamic linker's path name */
};

offset 链接器路径字符串在 struct dylinker_command 这个结构中的偏移

loader

比如这里使用的 dyld 链接器 linker,OSX 和 iOS 都使用 dyld 做动态链接器。

LC_SYMTAB

符号表

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* The symtab_command contains the offsets and sizes of the link-edit 4.3BSD
* "stab" style symbol table information as described in the header files
* <nlist.h> and <stab.h>.
*/
struct symtab_command {
uint32_t cmd; /* LC_SYMTAB */
uint32_t cmdsize; /* sizeof(struct symtab_command) */
uint32_t symoff; /* symbol table offset */
uint32_t nsyms; /* number of symbol table entries */
uint32_t stroff; /* string table offset */
uint32_t strsize; /* string table size in bytes */
};

symoff 符号表在 MachO 文件中的偏移

nsyms 符号数量

stroff 字符串表在 MachO 文件中的偏移

strsize 字符串表大小

LC_LOAD_DYLIB

描述当前 MachO 需要哪些动态链接库信息

lc-load-dylib

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
union lc_str {
uint32_t offset; /* offset to the string */
#ifndef __LP64__
char *ptr; /* pointer to the string */
#endif
};

struct dylib {
union lc_str name; /* library's path name */
uint32_t timestamp; /* library's build time stamp */
uint32_t current_version; /* library's current version number */
uint32_t compatibility_version; /* library's compatibility vers number*/
};

struct dylib_command {
uint32_t cmd; /* LC_ID_DYLIB, LC_LOAD_{,WEAK_}DYLIB,
LC_REEXPORT_DYLIB */
uint32_t cmdsize; /* includes pathname string */
struct dylib dylib; /* the library identification */
};

offset 指动态链接库字符串在 struct dylib_command 这个结构中的偏移

问题,更改 LC_LOAD_DYLIB 数据,是不是就可以加载我们自定义的 lib 了?

Data

Data 区存放的是 MachO 中实际的二进制代码指令数据和使用到的常量,符号信息等。Header 和 Load Commands 就是为了有序的组织这些数据。这里的数据大部分都是对应 Load Commands 中不同的 Command 结构。

查看 MachO 文件内存布局

我们现在 macos 下编译一个简单的命令行程序 main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <string.h>
#include <unistd.h>

// clang main.c -o main
int main(int argc, char **argv) {
pid_t pid = getpid();
printf("[+] current pid = %d\n", pid);
sleep(10);

if (argc < 2) {
printf("[-] please enter the password\n");
return -1;
}

if (strcmp(argv[1], "thisiskey") != 0) {
printf("[-] access denied\n");
} else {
printf("[+] access ok\n");
}

return 0;
}

将 main 程序运行,获取 pid

可以使用 vmmap 工具查看进程内存信息:

1
✗ vmmap <pid>

如下是 main 程序的虚拟内存信息:

vmmap

可以看到 main MachO 文件从虚拟地址 0x10352f000 开始加载,

使用 lldb 工具来调试:
1
2
$lldb -p <pid>
(lldb) x/100xw 0x10352f000 --force

lldb-macho

010-macho

000000010352f000-0000000103530000 区域均具备代码执行权限,在 Load Commands 和 Data 区之前,还有很多未使用的空白区域,这些区域是否可以用来隐藏我们的某些代码呢?

FAT 胖二进制格式 (通用二进制格式)

在其他 OS 平台,如果我们想编译跨平台的二进制文件,我们需要单独编译出来,然后在独立输出出去。

在 OSX/iOS 中,可以使用通用二进制格式(Universal Binary)又叫 FAT 胖格式,把这些不同平台的 MachO 文件组织在一起,输出一个独立的二进制文件。系统在执行这个独立的二进制文件时,会选择与当前架构匹配的 MachO 执行。

FAT 格式定义在 fat.h 文件中。

1
2
3
4
5
6
7
8
9
10
11
12
struct fat_header {
uint32_t magic; /* FAT_MAGIC */
uint32_t nfat_arch; /* number of structs that follow */
};

struct fat_arch {
cpu_type_t cputype; /* cpu specifier (int) */
cpu_subtype_t cpusubtype; /* machine specifier (int) */
uint32_t offset; /* file offset to this object file */
uint32_t size; /* size of this object file */
uint32_t align; /* alignment as a power of 2 */
};

OSX 中很多系统文件都是 FAT 格式,比如 /usr/lib/libSystem.B.dylib

1
2
3
4
$ file /usr/lib/libSystem.B.dylib
/usr/lib/libSystem.B.dylib: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dynamically linked shared library x86_64] [i386:Mach-O dynamically linked shared library i386]
/usr/lib/libSystem.B.dylib (for architecture x86_64): Mach-O 64-bit dynamically linked shared library x86_64
/usr/lib/libSystem.B.dylib (for architecture i386): Mach-O dynamically linked shared library i386

fat-format

参考