Mach-O 文件探索

之前在项目中使用 fishhook 来替换系统的 C 函数,其中涉及到很多和 iOS 系统相关的编译、链接等方面的知识,由于内容比较多,所以打算分几篇文章来进行讲解,本文主要是分析 Mach—O 文件。

在 macOS 以及 iOS 系统中,可执行文件的格式为 Mach-O,理解 Mach-O 文件格式对于我们探究操作系统的运作机制起着关键的作用。Mach-O 文件格式如下图所示,它由 Header、Load commands 以及 Data 三部分组成:

Header:记录 Mach-O 文件的基本信息,包括文件类型、支持的 CPU 类型以及加载命令的个数、大小等。

Load commands: 位于 Header 之后,向操作系统描述如何解析文件。

Data: 用于保存程序的 TEXT、DATA、LINKEDIT 等 segment 数据。

以下是人民群众喜闻乐见的一段代码:

使用 clang main.c -o MachOExplore 命令编译上述代码,我们得到名为 MachOExplore 的目标文件,也就是本文将要探究的 Mach-O 文件。otool 是用来查看 Mach-O 文件的常用工具,但是本文会使用另一种工具 MachOView 来完成任务。

Header

MachOView 查看 Header 的内容如下:

Header 中记录了 Mach-O 文件的属性信息,相关数据结构定义在 loader.h 中,分为32位和64位两种:

下表描述了各个字段的含义,mach_header_64 除了比 mach_header 多出一个 reserved 字段外,其他方面并无区别。

字段 说明
magic 表明文件适用于64位操作系统还是32位操作系统,因为大小端的存在,所以有  MH_MAGIC_64 和 MH_CIGAM_64 两种形式 。
cputype 描述文件所支持的 CPU 架构,包括 ARM64、X86_64、i386等。
cpusubtype 描述文件对应的具体 CPU 架构,例如 ARM_V7S、ARM64_V8、X86_ARCH1 等。
filetype 描述文件的类型,常见的类型有可执行文件、可重定位文件、共享库文件等。
ncmds 记录加载命令的个数。
sizeofcmds 记录所有加载命令的大小。
flags 描述文件在编译、链接等过程中的信息,示例中的 MH_NOUNDEFS 表示文件中不存在未定义的符号,MH_DYLDLINK 表示文件要交由 DYLD 进一步处理,MH_TWOLEVEL 表示文件使用两级命名空间,MH_PIE 表示启用地址空间布局随机化。

Load commands

Load commands 紧随在 Header 后,它包含了一系列的加载命令,目的是向操作系统描述如何处理 Mach-O 文件。示例中包含的加载命令如下:

LC_SEGMENT_64 命令表示将相应的 segment 映射到虚拟地址空间中,以 LC_SEGMENT_64(__PAGEZERO) 为例:

  • Command: 表示加载命令;

  • Command Size: 表示加载命令的大小;

  • Segment Name: 被加载的段的名字;

  • VM Address: 段所在的虚拟空间地址;

  • VM Size: 段所占用的虚拟空间的大小;

  • File Offset: 段在文件中的偏移量;

  • File Size: 段在文件中的大小;

  • Maximum VM protection: 表示与段相对应的最大操作权限;

  • Initial VM protection: 表示段的初始操作权限;

  • Number of Sections: 段包含多少个 Section;

  • Flags: 描述与段相关的加载信息,具体解释请参考 loader.h 文件;

所以上述命令是将 __PAGEZERO 段映射到虚拟地址 0x0 处,占用虚拟空间大小为 4GB,但是这4GB并不是真实的文件大小,它仅表明将虚拟地址空间的前4GB映射为不可读、不可写、不可执行,与 NULL 指针相对应。如果程序试图访问 __PAGEZERO 段,那么将会引起系统的崩溃。

接下来我们来看 LC_SEGMENT_64(__TEXT):


它将 __TEXT 段映射到虚拟地址空间 0x100000000 处,也就是紧随 __PAGEZERO 段,占用虚拟空间大小为 4096B,所对应的权限是可读、可执行、不可写入。我们的 __TEXT 段包含以下5个 Section:

  • __text: 包含程序的机器码;

  • __stubs 和 __stub_helper: 用来帮助 DYLD 绑定符号;

  • __cstring: 记录了文件中的常量字符串信息(包含在双引号中),我们可以依据此信息找到字符串的地址;

  • __unwind_info: 用于确定异常发生时栈所对应的信息,包括栈指针、返回地址、寄存器信息等,它同样包含相应的处理函数来支持像 catch、final 等特性;

LC_SEGMENT_64(__DATA) 的作用是将 __DATA 段映射到紧随 __TEXT 段的虚拟地址空间上,它包含两个 Section:

__nl_symbol_ptr Section 包含的符号指针需要在加载时绑定,而 __la_symbol_ptr Section 包含的符号指针则是在其第一次被程序使用时绑定。

LC_SEGMENT_64(__LINKEDIT) 则是将与动态链接相关的信息映射到虚拟地址空间,__LINKEDIT 段包括 rebase、bind、lazy bind 等信息。

LC_DYLD_INFO_ONLY 记录了有关链接的重要信息,它的数据结构如下:

根据它所记录的偏移量,我们便可以找到在 Dynamic Loader Info 中的相关信息。它的 ONLY 后缀表明这是程序运行所必须的,如果链接器不支持,那么加载过程就会终止。

LC_SYMTAB 记录了程序的符号表以及字符串表的偏移量及大小,符号表中记录了程序用到的函数以及全局变量的信息,符号表条目的数据结构定义在 nlist.h 中:

数据结构中相关字段的含义都可以在 nlist.h 中找到,这里值得一说的是 n_un 字段,它用来记录符号的名字,但为什么是 uint32_t 类型呢?又为什么在注释中标明是 string table 的 索引呢?

这是因为在程序中,字符串的长度是不固定的,所以会将其放在 string table 中,然后存储它在 string table 中的偏移。如果其他部分想要引用某个字符串,那么他首先需要找到 string table 的起始地址,然后根据偏移量找到相应字符串的起始位置并向后读取字符,直到遇见 \0 才会停止读取过程,最后返回读到的字符串。

这也是 LC_SYMTAB 额外记录 string table 地址的原因,string table 通常用于记录 section 名、符号名等信息。

其他的加载命令如下表所示:

加载命令 描述
LC_DYSYMTAB 包括动态链接过程中所需要的信息
LC_LOAD_DYLINKER 指定动态链接器的地址
LC_LOAD_DYLIB 记录了程序所需要的动态库的相关信息
LC_UUID 静态链接器为其生成的文件所提供的唯一标识符
LC_MAIN 指定 main 函数的地址
LC_FUNCTION_STARTS 记录文件中每个函数的起始地址
LC_DATA_IN_CODE 记录那些写在程序二进制执行指令中的数据
LC_CODE_SIGNATURE 代码签名

Data

Data 包括文件所需的 segment 数据,除了 MachOView 工具,你也可以通过 size 工具来查看,运行 size -x -l -m MachOExplore 命令后可得到以下内容:

我们还可以通过输入 otool -s __DATA __la_symbol_ptr MachOExplore 命令来查看 __la_symbol_ptr Section 的数据:

也可以使用 otool -V -s __TEXT __text MachOExplore 命令来查看 __text Section 的反汇编代码:

同时 otool 也为一些常见的命令设置了缩写,例如 -t 就是 -s __TEXT __text 简称,而 -d 就是 -s __DATA __data 的简称,相关信息都可以通过 man otool 命令来查看。这些数据都可以在 MachOView 中看得很清楚,但是这些命令行工具记一下也无妨。

总结

以上便是 Mach-O 文件的探究(其实总结部分就是强行凑的(๑•̀ㅂ•́)و✧)。

发表评论

Close Menu