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 基本结构
MachOView 和 010editor 这两款工具可以帮我们更直观的分析 MachO 格式。
MachOView:可以看出,Mach-O 主要由三部分组成: - 首先是文件头,表明该文件是 Mach-O 格式,指定目标架构,还有一些其他的文件属性信息,文件头信息影响后续的文件结构安排 - 紧随文件头的是可变大小的 Load Commands 区,这里以多组结构指定文件数据布局和关系,比如可以指定文件在虚拟内存中初始加载地址,符号表的位置,代码块的位置等。Load Commands 不是固定的,通过多种组合,Mach-O 就可以用于不同场景下,比如作为可执行文件,作为动态链接库,调试文件,目标文件,内核转储文件等。 - 接下来是 Data 区,以上两部分实际上是结构化组织,不承载具体的二进制代码和数据,Data 区主要就是负责代码和数据记录的。Mach-O 是以 Segment 这种结构来组织数据的,一个 Segment 可以包含 0 个或多个 Section。根据 Segment 是映射的哪一个 Load Command,Segment 中 section 就可以被解读为是是代码,常量或者一些其他的数据类型。在装载在内存中时,也是根据 Segment 做内存映射的。
header
MachO64 文件头结构详情可参考 loader.h
,共占用 0x20 个
byte。
1 | /* |
magic
表示魔数标识,MACHO-64 的魔数值为 0xfeedfacf
cputype
和 cpusubtype
表示 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 ...
ncmds
和 sizeofcmds
表示 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 | struct load_command { |
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
13struct 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
。
vmaddr
和 vmsize
指定当前 segment
在映射到内存后,虚拟内存开始地址和占据的内存大小
fileoff
和 filesize
指定当前 segment
数据在偏移 fileoff 的长度后映射到 vmaddr 处,注意不是指在 MachO
文件中的偏移,filesize 指定要拷贝的数据大小
maxprot
和 initprot
指定当前 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
14struct 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 | /* |
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 | /* |
dataoff
数据在 MachO 文件中偏移
datasize
数据大小
LC_MAIN
1 | /* |
entryoff
main 函数在 MachO 文件中偏移
stacksize
一般情况下为0, 非 0 表示初始的栈大小
LC_DYLD_INFO LC_DYLD_INFO_ONLY
这两个命令主要是给动态链接器 dyld 做 link 使用的。动态 linker 的主要作用是二进制加载到内存后,将动态链接库的信息链接起来,找到使用的外部函数信息,导出提供给外部使用的函数信息。
1 | struct dyld_info_command { |
rebase
地址重定位
bind
地址绑定, 找到使用的外部符号信息
weak bind
弱引用绑定,C++ 程序需要的
lazy bind
地址懒绑定,延迟绑定,有的外部函数,不需要在加载时就完成地址绑定,可以在运行到的时候再去查找外部函数地址
export
要导出的函数信息
问题:iOS 的函数 hook 应该在这里操作吧?
LC_LOAD_DYLINKER
这个命令指定动态链接器 linker 路径
1 | union lc_str { |
offset
链接器路径字符串在 struct dylinker_command
这个结构中的偏移
比如这里使用的 dyld 链接器 linker,OSX 和 iOS 都使用 dyld 做动态链接器。
LC_SYMTAB
符号表
1 | /* |
symoff
符号表在 MachO 文件中的偏移
nsyms
符号数量
stroff
字符串表在 MachO 文件中的偏移
strsize
字符串表大小
LC_LOAD_DYLIB
描述当前 MachO 需要哪些动态链接库信息
1 | union lc_str { |
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
// 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 MachO 文件从虚拟地址 0x10352f000 开始加载,
使用 lldb 工具来调试:1 |
|
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
12struct 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 | $ file /usr/lib/libSystem.B.dylib |