什么是Mach-O文件?
Mach-O是Mach object的缩写,是Mac\iOS上用来存储程序、库的标准格式。
Mach-O文件类型
- 可以在xnu源码中,查看到Mach-O格式的详细定义(opensource.apple.com/tarballs/xn… EXTERNAL_HEADERS/mach-o/fat.h EXTERNAL_HEADERS/mach-o/loader.h
#define MH_OBJECT 0x1 /* relocatable object file */
#define MH_EXECUTE 0x2 /* demand paged executable file */
#define MH_FVMLIB 0x3 /* fixed VM shared library file */
#define MH_CORE 0x4 /* core file */
#define MH_PRELOAD 0x5 /* preloaded executable file */
#define MH_DYLIB 0x6 /* dynamically bound shared library */
#define MH_DYLINKER 0x7 /* dynamic link editor */
#define MH_BUNDLE 0x8 /* dynamically bound bundle file */
#define MH_DYLIB_STUB 0x9 /* shared library stub for static */
/* linking only, no section contents */
#define MH_DSYM 0xa /* companion file with only debug */
/* sections */
#define MH_KEXT_BUNDLE 0xb /* x86_64 kexts */
- xnu是苹果MacOS\iOS等操作系统的内核
- 常见的Mach-O文件类型
| Mach-O类型 | 示例文件 |
|---|---|
| MH_OBJECT | 目标文件(.o) 静态库文件(.a)注:静态库其实就是多个目标文件合并在一起 |
| MH_EXECUTE | 可执行文件,存放App的所有源码信息,在.app/xx |
| MH_DYLIB | 动态库文件.dylib 或者 .framework/xx |
| MH_DYLINKER | 动态链接编辑器,也就是之前所说的/usr/lib/dyld工具 |
| MH_DSYM | 此文件中存储这二进制文件符号信息(.dSYM/Contents/Resources/DWARF/xx),在开发中,我们经常使用此文件来分析App的崩溃信息 |
Mach-O的基本结构
可以点击官网查看Mach-O的介绍。
Mach-O组成
Mach-O由3个部分组成
- Header,包含文件类型、目标架构类型等等
- Load commands,是描述文件在虚拟内存中的逻辑结构和布局,相当于一份目录索引
- Raw segment data,在Load commands中所定义的Segment,在这里都能找到原始数据。
Raw segment data存放了所有的原始数据,而Load commands相当于Raw segment data的索引目录
查看Mach-O的结构
命令行工具,通过file命令查看Mach-O文件的基本信息
- file:查看Mach-O的文件类型
file 文件路径
- otool,查看Mach-O特定部分和段的内容
#查看Mach-O文件的header信息
otool -h 文件路径
#查看Mach-O文件的load commands信息
otool -l 文件路径
更多使用方法,终端输入otool -help查看
- lipo,用来处理多架构Mach-O文件,常用命令如下
#查看架构信息
lipo -info 文件路径
#导出某种类型的架构
lipo 文件路径 -thin 架构类型 -output 输出文件路径
#合并多种架构类型
lipo 文件路径1 文件路径2 -output 输出文件路径
GUI工具,MachOView的使用
- 点击查看MachOView官网
Universal Binary(通用二进制文件)
通用二进制文件就是同时适用于多种架构的二进制文件,它包含了多种不同架构的独立的二进制文件,它有以下特点
- 因为需要存储多种架构的代码,所以通用二进制文件要比单架构二进制文件要大
- 因为两种种架构之间可以共用一些资源,所以两种架构的通用二进制文件大小不会达到单一架构版本的两倍。
- 运行过程中只会调用其中的部分代码,所以运行起来不会占用额外的内存
- 通用二进制文件通常也被称为“胖二进制文件(Fat binary)”
dyld和Mach-O
dyld是iOS中用来加载可执行文件、动态库的工具,其实它本身也是一个Mach-O文件。
什么是dyld?
- dyld 动态加载器(又叫做动态链接编辑器)
- dyld的源码可以点击此处下载
dyld的作用。
dyld可以用来加载以下三种类型的Mach-O文件
- MH_EXECUTE
- MH_DYLIB
- MH_BUNDLE
通过查看dyld的源码可以看到加载文件时的类型校验
Mach-O基本结构回顾
在深入学习Mach-O文件之前,先来回顾一下之前学习的Mach-O的基本结构,可以到官网查看Mach-O文件的介绍
Mach-O深入探究
Header
在Mach-O文件中,Header部分存放了文件的基本描述信息,如下:
-
Magic Number代表当前Mach-O文件的架构是MH_MAGIC_64,所支持的架构是arm64架构
-
CPU Type、CPU SubType代表CPU的类型和子类型,在源码<mach/machine.h>中够可以看到具体的定义
-
File Type代表文件的类型,图中的文件类型表示可执行文件类型
-
Number of Load Commands 和 Size of Load Commands 表示Load Commands的数量和大小
-
Flags 代表动态链接器(dyld)的标志
-
Reserved 保留字段
Load Commands
Load Commands指定了文件在虚拟内存中的逻辑结构和布局,如下
在Load Commands中存储了各种段的基本信息,下面以LC_SEMENT_64(__PAGEZERO)中的信息为例
- 最顶部的Command代表Load Command的类型是LC_SEMENT_64,具体含义是将文件中的段映射到进程地址空间
- Command Size 表示当前Load Command本身的大小
- Segment Name 是Load Command的名称,当前的Load Command名称为__PAGEZERO
- VM Address 表示__PAGEZERO段加载到虚拟内存中的地址,从0x000000000开始
- VM Size 表示__PAGEZERO段在虚拟内存中所占据的空间大小
- File Offset 表示当前__PAGEZERO段在Mach-O文件中的位置。
- File Size 表示__PAGEZERO段在Mach-O文件中的大小,此处File Size为0表示在Mach-O文件中并没有__PAGEZERO段,在Mach-O文件被加载进虚拟内存中,才会附加上__PAGEZERO段。
- Maxinum VM Protection 表示当前段在虚拟内存中所需要的最高内存保护
- Initial VM Protection 表示当前段的初始内存保护
- Number of Sections 表示当前段中所包含的Section的数量
- Flag 标志位
__PAGEZERO是Mach-O加载进内存之后附加的一块区域,它不可读,不可写,主要用来捕捉NULL指针的引用。如果访问__PAGEZERO段,会引起程序崩溃
Raw Segment Data
在Raw Segment Data中就存放了所有段的原始数据
- __TEXT段中存放了所有函数代码
- __DATA段中存放了所有全局变量信息
使用size 指令查看Mach-O内存分布
size -l -m -x Mach-O文件路径
ASLR
ASLR其实就是Address Space Layout Randomization,地址空间布局随机化。它是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的的一种技术。在iOS 4.3开始引入ASLR技术
未使用ASLR技术时,Mach-O文件加载进内存后如何布局?
在未使用ASLR技术时,Mach-O被加载进内存后,是从地址0x000000000开始存放,前文说到,Mach-O文件本身是不存在__PAGEZERO的,在Mach-O文件被加载到虚拟内存之后,系统会给Mach-O文件分配一个__PAGEZERO,它的开始位置是0x000000000,结束位置是0x100000000。并且它的大小是固定的。
Mach-O本身的内容在虚拟内存中存放的开始位置从0x100000000开始,也就是紧接着__PAGEZERO的结束地址存放。而且在下图中,__TEXT段的File Offset为0,File Size为133740320,这代表着在Mach-O文件中,从0x000000000位置开始到0x7CB0000为止存放的都是__TEXT段的内容。
而__TEXT段在虚拟内存中存放的开始位置是0x100000000,终止位置是0x17CB0000,这说明__TEXT段是原封不动的从Mach-O文件加载进虚拟内存中,紧接着__PAGEZERO存放的。
通过分析剩下的__DATA段、__LinkEDIT段等等可以得出以下结论
PS:在arm64架构中,__PAGEZERO段的终止位置是从0x100000000(8个0)而在非arm64架构中,__PAGEZERO段的终止位置是从0x4000(3个0)开始
使用了ASLR技术后,Mach-O文件加载进内存后如何布局?
在使用了ASLR技术之后,在Mach-O文件加载进内存之后,__PAGEZERO的开始位置就不是从0x000000000开始存放了,ASLR会随机产生一个地址偏移Offset,而__PAGEZERO的开始位置需要在0x000000000的基础上加上偏移量Offset的值,才是真正的存放地址。 假设随机偏移量Offset是0x000005000,那么__PAGEZERO的开始位置就是0x000005000,结束位置就是0x100005000。剩下的__TEXT段、__DATA段和__LINKEDIT段则依次偏移Offset即可,如下:
获取函数在虚拟内存中的真实内存地址
Mach-O文件被加载进虚拟内存中时,由于使用了ASLR技术,导致内存地址产生Offset,所以要想获取函数的准确的内存地址,就需要知道当前具体的偏移量。然后使用以下公式就可得出函数在虚拟内存中的内存地址
函数的内存地址(VM Address) = File Offset + ASLR Offset + __PAGEZERO Size
- File Offset 表示当前函数在Mach-O文件中的存放位置
- ASLR Offset 表示随机地址偏移量
- __PAGEZERO Size 表示__PAGEZERO段的size
通常我们使用Hopper、IDA等工具查看Mach-O文件所看到的地址都是未使用ASLR的VM Address,要想获取函数的真实虚拟内存地址,就需要找到Mach-O加载进虚拟内存后的随机偏移量Offset
上图中函数test的起始地址是0x5e92e0,也就是说它的File Offset为0x5e92e0。这个是它在Mach-O文件中的地址偏移。
动态调试,获取程序ASLR的偏移量
通过动态调试,我们来一步一步获取ASLR的偏移量
- 首先在Mac上使用tcprelay.py开启Mac端口号映射
python tcprelay.py -t 22:10010 10011:10011
- 然后通过SSH连接iPhone
ssh root@localhost -p 10010
- 在iPhone上使用启动Debugserver,将要动态调试的App附加到Debugserver上,此处以听云App为例
debugserver *:10011 -a ting
- 在Mac上启动LLDB,然后通过Mac的10089端口连接Debugserver服务
➜ ~ lldb
(lldb) process connect connect://localhost:10011
- 使用image list命令得到App可执行文件的路径
(lldb) image list -o -f grep | ting
[ 0] /var/containers/Bundle/Application/3705B8A0-0B47-4B66-9E71-D0A511E19563/ting.app/ting 0x00000000000ec000(0x00000001000ec000)
- 可以看出,0x00000000000ec000就是该可执行文件的起始地址,也就是ASLR的偏移量,然后,使用Hopper Disassmbler可以获取到未使用ASLR的地址,加上0x00000000000ec000,就可以得到加载进内存之后的真实地址。