Mach-O文件

766 阅读9分钟

什么是Mach-O文件?

Mach-O是Mach object的缩写,是Mac\iOS上用来存储程序、库的标准格式。

Mach-O文件类型

#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文件

查看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的使用

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,就可以得到加载进内存之后的真实地址。