前言
- Mach-O文件格式源码
- Mach-O苹果官方手册
想要程序跑起来,那么这个可执行文件的格式就需要被当前的操作系统所理解,比如:
- Linux 操作系统下可执行文件格式是 ELF
- Windows 的可执行文件格式是 PE32/PE32+
- Android 的可执行文件格式是 ELF
- OSX 和 iOS 的可执行文件格式是
Mach-O
准备工具
方式1: 终端查看
来到Mach-O文件所在位置,输入相关命令得到Mach-O文件信息。为了更直观点,推荐方式2查看Mach-O文件。
方式2: 工具查看
首先要下载一个可以查看Mach-O文件格式的工具,本来想用 MachOView ,无奈下载完后打开Mach-O文件后,会闪退,就去逆向论坛找了个大神些的替代MachOView的工具 MachOExplorer 。我这里下载使用的是MachOExplorer,下载完后,打开,点击菜单栏的 file -> Mach-O文件 (此处我用模拟器的运行,打开了Debug-iphonesimulator文件夹,找到ipa,邮件显示包内容后可以获得可执行文件)。
Mach-O文件是什么
在OSX和iOS系统下,平时接触到的可执行文件、库文件、dsym文件、动态库、动态链接器(dyld)都是这种格式。Mach-O的组成结构包括:Header
(头部)、Load commands
(加载命令)、Data
(Data包含多个 Segment
(段),Segment中包含多个 Section
(节))
简单介绍dsym文件,后续开篇介绍。
- dSYM 文件是什么: Xcode编译项目后,会有一个项目同名的 dSYM 文件,dSYM 是保存 16 进制函数地址映射信息的中转文件。
- dSYM 文件的作用: release 模式打包或上线后,崩溃错误不直观,这时就需要分析 crash report 文件,iOS 设备中会有日志文件保存我们每个应用出错的函数内存地址,通过 Xcode 的 Organizer 可以将 iOS 设备中的 DeviceLog 导出成 crash 文件,这个时候我们就可以通过出错的函数地址去查询 dSYM 文件中程序对应的函数名和文件名。大前提是我们需要有软件版本对应的 dSYM 文件,这也是为什么我们很有必要保存每个发布版本的 Archives 文件了。
Header (头部)
header的数据结构
Mach-O的头部信息,可以使我们快速得到一些信息,比如
32位结构还是64位结构,比如文件类型架构类型等等。
让我们先来看看header的数据结构定义。
/** 32位架构对于header的定义*/
struct mach_header {
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 */
};
/** 64位架构对于header的定义*/
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 */
};
32位和64位架构的头文件对比,多了一个reserved(保留字段)。
现在来解释说明一下这些字段都有什么意义:
- magic : 魔数,用于快速确认该文件是64位还是32位的。若值为0xfeedfacf是64位的,值为0xfeedface则是32位的。
- cputype :CPU架构类型。比如arm,例如x86_64
- cpusubtype : 对应的架构具体类型。比如arm64、armv7等。
- filetype : 文件类型。比如可执行文件(MH_EXECUTE)、库文件、Dsym文件等。
- ncmds : 加载命令条数。
- sizeofcmds : 所有加载命令大小。
- flags : dyld加载时需要的标志位。
- reserved :保留字段。
/* filetype 类型 */
#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 */
#define MH_DSYM 0xa /* companion file with only debug */
#define MH_KEXT_BUNDLE 0xb /* x86_64 kexts */
/* flags : dyld加载时需要的标志位。*/
#define MH_NOUNDEFS 0x1 // 目前没有未定义的符号,不存在链接依赖
#define MH_DYLDLINK 0x4 // 该文件是dyld的输入文件,无法被再次静态链接
#define MH_PIE 0x200000 // 加载程序在随机的地址空间,只在 MH_EXECUTE中使用
#define MH_TWOLEVEL 0x80 // 二级名称空间
dyld
动态链接器,苹果的开源项目, 下载dyld ,当内核执行到 LC_DYLINK
(后文讲述)的时候,链接器会启动,查找进程所依赖的动态库,并加载到内存中。
flags : MH_PIE - 随机地址空间(ASLR)
进程每一次启动,地址空间都会随机化。如果采用传统的方式,程序每启动一次,启动的虚拟内存镜像一致的话,黑客很容易就重写内存来破解程序。所以,ASLR可以有效避免黑客的攻击。
打开Xcode,来到Main函数,打断点,运行程序开启lldb调试。当到达断点位置时,在控制台输入:
/** 加载模块地址*/
image list -o -f
可以发现每次运行程序,地址都在变化。
flags : MH_TWOLEVEL - 二级名称空间
这是dyld的一个独有特性,说是符号空间中还包括所在库的信息,这样子就可以让两个不同的库导出相同的符号,与其对应的是平摊名称空间.
演示查看header结构
方式1: 通过命令行来查看Mach-O文件的header结构
/* file + 可执行文件路径 : 查看文件类型 */
zhuangyuan$ file GV_VOUCHER_CN
-> Mach-O 64-bit executable x86_64
/* lipo -info + 可执行文件路径 : 查看文件架构 */
zhuangyuan$ lipo -info GV_VOUCHER_CN
-> Non-fat file: GV_VOUCHER_CN is architecture: x86_64
/* otool -h + 可执行文件路径 : 查看Mach-O文件的header信息 */
zhuangyuan$ otool -h GV_VOUCHER_CN
->
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedfacf 16777223 3 0x00 2 51 6184 0x00218085
/* otool -hv + 可执行文件路径 : 查看Mach-O文件的header信息的翻译*/
zhuangyuan$ otool -hv GV_VOUCHER_CN
->
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 X86_64 ALL 0x00 EXECUTE 51 6184 NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIE
如下图:
方式2: 利用MachOExplorer工具查看Mach-O文件的header结构
比终端看的更直观看,终端的好处就是装逼还挺成功的,哈哈。
Load commands 结构 (加载指令模块)
Load commands 紧跟在header之后,说明了操作系统应当如何加载文件中的数据,对系统内核加载器和动态链接器起了指导性作用。这些加载指令告诉加载器
如何处理二进制数据,有些命令是由内核处理的,有些是由动态链接器处理的。在源码中有明显的注释说明这些是动态链接器处理的。
Load commands
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
- cmd: 加载指令类型
- cmdsize :加载指令大小
查看Mach-O文件的所有数据
方式1: 终端 - 演示查看load commands结构
终端输入:
otool -lv + 文件
方式2: 工具查看
Segments
Mach-O文件有多个段(Segment),每个段有不同的功能,一般的段又会按不同的功能划分为几个区(Section)。
Segments数据结构
/*segment_command_64 数据结构,其他段的数据结构类似,感兴趣可以去阅读源码*/
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 */
};
Segments结构字段说明
LC_SEGMENT
和 LC_SEGMENT_64
是加载的主要命令,负责指导内核将文件中(32位或64位)的段映射到进程的地址(内存)空间中。
- cmd : 即 Load commands 的类型,比如
LC_SEGMENT_64
代表将文件中64位的段映射到进程的地址空间。还有LC_SEGMENT、LC_DYLD_INFO_ONLY等等类型。 - cmdsize : 代表当前 ‘load command’ 的大小
- VM Address :段的虚拟内存地址
- VM Size : 段的虚拟内存大小
- file offset: 段在Mach-O文件中的偏移量
- file size : 段在文件中的大小
- nsects : 标识了Segment中有多少section
- segname[16] : 段的名称。例如__PAGEZERO、__TEXT等等。
将段对应的文件内容加载到内存中的流程:
从file offset处 加载 file size 大小到 虚拟内存VM Address处
。如果当前段是LC_SEGMENT_64(__PAGEZERO) ,则这个段的file offset、file size 、VM Address为0,因为这个段不具备访问权限,用来处理空指针的。
具体段说明
- LC_SEGMENT_64(__PAGEZERO) : 空指针陷阱段,映射到虚拟内存空间的第一页,用于捕捉对NULL指针的引用。
- LC_SEGMENT_64(__TEXT) : 包含了
执行代码
以及其他只读数据
。该段数据的保护级别为:VM_PROT_READ(读)、VM_PROT_EXECUTE(执行),防止内存中被修改。 - LC_SEGMENT_64(__DATA) : 包含了程序数据,可读/可写数据存放段。
- LC_SEGMENT_64(__LINKEDIT) : 链接器使用的符号以及其他表,链接的部分,支持dyld,包含了一些符号表等数据。
- LC_SYMTAB : 符号表信息
- LC_DYSYMTAB :动态符号表信息
- LC_LOAD_DYLINKER : 加载动态链接器(/usr/lib/dyld),使用Mach-O文件的时候链接器,可以看到name为 /usr/lib/dyld 的链接器来加载Mach-O文件。
- LC_UUID : 对每个Mach-O文件都是唯一标识,crash解析中也会有,去匹配dysm文件和crash文件。
- LC_VERSION_MIN_IPHONEOS : 二进制文件要求的最低操作系统版本(iOS Deployment Target) ,打开xcode ,输入iOS Development Target 可以查看版本
- LC_MAIN : 设置程序主线程的入口地址和栈大小
- LC_ENCRYPTION_INFO : 加密信息,查看文件是否加密(在终端输入
otool -l 文件名 | grep cryptid
) - LC_LOAD_DYLIB : 加载的动态库,包括动态库地址和名称,当前版本号,兼容版本号(终端输入`otool -l 文件名)
- LC_RPATH : 环境变量路径
- LC_FUNCTION_STARTS : 函数起始地址表
- LC_CODE_SIGNATURE : 记录可执行文件的代码是否签名
Section
大写的表示Segment,小写的表示section。
例如 __TEXT 和 __text.
Section数据结构
struct section { /* for 32-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint32_t addr; /* memory address of this section */
uint32_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) */
};
结构字段说明
- sectname:比如_text、stubs
- segname :该section所属的segment,比如__TEXT
- addr : 该section在内存的起始位置
- size: 该section的大小
- offset: 该section的文件偏移
- align :字节大小对齐
- reloff :重定位入口的文件偏移
- nreloc: 需要重定位的入口数量
- flags:包含section的type和attributes
主要的节说明
- __text:主程序代码
- __stub_helper :用于动态链接器的存根
- __symbolstub1 : 用于动态链接器的存根
- __objc_methname :OC的方法名
- __objc_classname : OC的类名
- __cstring : 字符串
查看__text 节的全部内容(二进制)
otool -s __TEXT __text + 文件
查看__text 节的全部内容(汇编)
查看__text 节的内容前10条数据(汇编)
利用MachOExplorer工具
演示部分
Mach-O文件开始加载的地方
- 打开MachOExplorer查看__TEXT部分的虚拟地址0x100000000
- 打开xcode 来到main函数打断点 ,输入
image list -o -f
查看模块加载的地址,可以看到加载可执行文件的基地址 0x000fabcd - 在控制台输入
p/x 0x000fabcd + 0x100000000
得到了一个新的地址$0 - 选择xcode菜单的Debug —> debug workflow —> view memory 之后再Address里面输入$0地址,回车
- 可以看到Mach-O文件格式的一些数据 ,所以0x100000000就是Mach-O文件加载开始的地方.
本文参考了的文章:
趣探 Mach-O:文件格式分析
感谢。
更多推荐
iOS逆向与安全 - 5. Mach-O文件格式
发布评论