框架图
Mach-O 是 Apple 系统上(包括 MacOS 以及 iOS)的可执行文件格式,类似于 windows 上的 PE 文件以及 linux 上的 ELF 文件。上图左边为官方图,右边为用 MachOView 软件打开的 Mach-O 文件图。可以非常清晰的看到,这种文件格式由文件头(Header)、加载命令(Load Commands)以及具体数据(Segment&Section)组成。下面一一介绍。
Header
|
|
以上是 Header 在代码中的定义,它在文件中的作用主要是:使系统能够快速定位其运行环境以及文件类型等等。
分析文件头的 otool 命令为: otool \-h 可执行文件
,或者可视化强一点的 otool \-hv 可执行文件
。
Fat 格式的文件(既包含有32位的二进制文件,又包含有64位的二进制文件),会在两个架构的二进制文件之前(也就是最开始的部分)有一个 Fat Header,其中 magic 为 0xCAFEBABE
,然后是包含有的架构的个数,以及每个架构在文件中的偏移和大小等。
filetype 以及 flags 只列举了几个比较常见的定义,还有其他的详见EXTERNAL_HEADERS/mach-o/x86_64/loader.h
。
Load Commands
Load Commands 是跟在 Header 后面的加载命令区,所有 commands 的大小总和即为 Header->sizeofcmds 字段,共有 Header->ncmds 条加载命令。
|
|
Command 以 LC 开头,不同的加载命令有不同的专有的结构体,cmd 和 cmdsize 是都有的,分别为命令类型(即命令名称),这条命令的长度。这些加载命令告诉系统应该如何处理后面的二进制数据,对系统内核加载器和动态链接器起指导作用。如果当前 LC_SEGMENT 包含 section,那么 section 的结构体紧跟在 LC_SEGMENT 的结构体之后,所占字节数由 SEGMENT 的 cmdsize 字段给出。
Cmd | 作用 |
---|---|
LC_SEGMENT/LC_SEGMENT_64 | 将对应的段中的数据加载并映射到进程的内存空间去 |
LC_SYMTAB | 符号表信息 |
LC_DYSYMTAB | 动态符号表信息 |
LC_LOAD_DYLINKER | 启动动态加载连接器/usr/lib/dyld程序 |
LC_UUID | 唯一的 UUID,标示该二进制文件,128bit |
LC_VERSION_MIN_IPHONEOS/MACOSX | 要求的最低系统版本(Xcode中的Deployment Target) |
LC_MAIN | 设置程序主线程的入口地址和栈大小 |
LC_ENCRYPTION_INFO | 加密信息 |
LC_LOAD_DYLIB | 加载的动态库,包括动态库地址、名称、版本号等 |
LC_FUNCTION_STARTS | 函数地址起始表 |
LC_CODE_SIGNATURE | 代码签名信息 |
使用命令 otool \-l 可执行文件
可以查看加载命令区,使用 otool \-l 可执行文件 | grep cryptid
可以查看是否加密。
Segment
Mach-O 文件有多个段(Segment),每个段有不同的功能。然后每个段又分为很多小的 Section。 LC_SEGMENT 意味着这部分文件需要映射到进程的地址空间去。一般有以下段名:
__PAGEZERO: 空指针陷阱段,映射到虚拟内存空间的第一页,用于捕捉对 NULL 指针的引用。
__TEXT: 包含了执行代码以及其他只读数据。该段数据可以 VM_PROT_READ(读)、VM_PROT_EXECUTE(执行),不能被修改。
__DATA: 程序数据,该段可写 VM_PROT_WRITE/READ/EXECUTE。
__LINKEDIT: 链接器使用的符号以及其他表。
段的结构体定义为:
|
|
其中 nsects 字段就是表明该段中有多少个 section。文件映射的起始位置是由 fileoff 给出,映射到地址空间的 vmaddr 处。
Section
Section 是具体有用的数据存放的地方。它的结构体跟随在 LC_SEGMENT 结构体之后,LC_SEGMENT 又在 Load Commands 中,但是 segment 的数据内容是跟在 Load Commands 之后的。它的结构体为:
|
|
其中 flag 字段分为两个部分,一个是区域类型(section type),一个是区域属性(section attributes)。其中 type 是互斥的,即只能有一个类型,而 attributes 不是互斥的,可以有多个属性。如果段(segment)中的任何一个 section 拥有属性 S_ATTR_DEBUG,那么该段所有的 section 都必须拥有这个属性。具体的flag字段内容以及意义请参考 /usr/include/mach-o/loader.h
。
段名为大写,节名为小写。各节的作用主要有:
__text: 主程序代码
__stub_helper: 用于动态链接的存根
__symbolstub1: 用于动态链接的存根
__objc_methname: Objective-C 的方法名
__objc_classname: Objective-C 的类名
__cstring: 硬编码的字符串
__lazy_symbol: 懒加载,延迟加载节,通过 dyld_stub_binder 辅助链接
_got: 存储引用符号的实际地址,类似于动态符号表
__nl_symbol_ptr: 非延迟加载节
__mod_init_func: 初始化的全局函数地址,在 main 之前被调用
__mod_term_func: 结束函数地址
__cfstring: Core Foundation 用到的字符串(OC字符串)
__objc_clsslist: Objective-C 的类列表
__objc_nlclslist: Objective-C 的 +load 函数列表,比 __mod_init_func 更早执行
__objc_const: Objective-C 的常量
__data: 初始化的可变的变量
__bss: 未初始化的静态变量
查看某段中某节的命令为: otool \-s __TEXT __text 可执行文件
。
与 IDA 的对应地址
如果用 MachOView 来查看的话,界面左上角有一个 RAW、RVA 的选项。RAW 就是指该字节相对于文件开始部分的绝对偏移,文件头部的地址是从0x000开始的。RVA 是相对于某个基地址的偏移,也就是整体的绝对偏移值再加上某个基地址,文件头部的地址是从某个值(基地址)开始的。
这个所谓的基地址其实是 LC_SEGMENT_64(_PAGEZERO) 中的 VM_Size 字段的值,因为留出这段空白页面就是为了捕获程序的空指针,以及考虑到页面对齐。IDA 中就是使用的 RVA 地址。这个地址在 armv7 中是0x4000,arm64 中是0x10000 0000。
Section(__TEXT,__text) 所在的 RVA 地址,对应的就是 IDA 解析的函数开始地址。 IDA 解析的 Mach-O 文件中的函数都位于 Section(__TEXT) 段,然后还会接着解析 Section(__DATA) 段,即 IDA 中的数据区。
LC_MAIN 加载命令中的 Entry Offset 字段 + 基地址(RVA 选项下的文件头部地址) = IDA 中左侧函数 _main 的地址。
Reference
[1] mach-o格式分析
http://turingh.github.io/2016/03/07/mach-o%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F%E5%88%86%E6%9E%90/#
[2] 趣探 Mach-O:文件格式分析 http://www.jianshu.com/p/54d842db3f69
[3] 网易云课堂《iOS逆向与安全》