Mach-O二进制格式
UNIX基本上标准化了一个通用的可移植的二进制格式,这个格式成为Executable and Library Format(简称ELF)。这个格式具有良好的文档,而且还有一整套binutils工具用于维护和调试这个格式的文件,甚至同样的CPU架构上的不同UNIX之间还允许二进制级别的可移植性(例如Linux和Solaris--这是真的,x86版本的Solaris可以原生地执行某些Linux二进制文件)。然而OS X却维护了一个自己独有的二进制格式:Mach-Object(简写为Mach-O),这是另一个源于NextSTEP的遗产。
Mach-O和苹果的一些文档对Mach-O的格式进行了解释。Mach-O格式具有一个固定的文件头。这个文件头的详细信息在<mach-o/loader.h>头文件中,如图4-3所示。
文件头一开始是魔数值,加载器可以通过这个魔数值快速判断这个二机制文件用于32位(MH_MAGIC, #define为0xFEEDFACE)还是64位(MH_MAGIC_64, #define为0xFEEDFACF)。在魔数值之后跟着的是cpu类型以及子类型字段,这两个字段和通用二进制文件中的相同字段作用是一样的---------用于确保二进制文件适合并且可以在当前架构下运行。除此之外,32位架构和64位架构的文件头结构就没有实质差别了:除了64位的头文件还包含一个额外的预留字段之外,这个字段目前没有使用。
image.png
由于这样的同一种二进制格式用于多种目标文件类型(可执行文件,库文件,核心转储文件以及内核扩展文件等),那么下一个int类型的字段filetype就用于表示目标文件的类型,这个字段的可取值以宏的形式定义在<mach-o/loader.h>头文件中。在我们使用的系统中常见的值如表4-3所示。
image.png
文件头中还包含了重要的标志,这些标志也定义在<mach-o/loader.h>文件中,如图:
image.png
从上图中可以看出,有两个标志和“执行” 相关:MH_ALLOW_STACK_EXECUTION和MH_NO_HEAP_EXECTION.这两个标志都用于防止某些数据的执行,通常称为NX(Non-eXecutable,可参阅内存页面中的一个同名标志位)。通过将数据所在的内存页面标记为不可执行,(一般情况下)可以防止黑客进行代码注入,因为黑客不能方便地执行数据段中的代码。如果试图执行数据段中的代码,则会引发一个硬件异常,进程会终止---------让进程崩溃,从而避免执行注入的代码。
由于代码注入的常见方法是使用栈变量(即自动变量),因此默认情况下栈都标记为不可执行,而这个标志可以用于覆盖这种行为(非常危险)。堆则默认可执行。尽管完全可能,但是通过堆注入代码相对困难一些。
这两个设置可以在系统级别进行:通过sysctl(8)修改vm.allow_stack_exec和vm.allow_heap_exec变量。在发生冲突时,以更宽松的设置为准(即false优先于true)。在iOS中,没有暴露sysctl接口,堆和栈都默认不可执行。
Mach-O文件头的主要功能在于加载命令(load command)。加载命令紧跟在文件头之后,文件头中的两个字段--------ncmds和sizeofincmds---------用于解析加载命令。之后会详细讨论。
image.png
otool工具善于分析加载命令和文本段,但是不合适分析数据段和其他区域。本书的支持网站上有一个名为jtool的工具,这个工具的目的是增强otool的功能。这个工具可以处理iOS 5.1和Mountain Lion之前(含)的所有类型的二进制文件。这个工具将nm和strings、segedit、size和otool的功能整合在一个二进制文件中,不仅特别适合脚本化操作,还支持一些新的特性。
Mach-O文件头中包含了非常详细的指令,这些指令在被调用时清晰地指导了如何设置并加载二进制数据。这些指令,或称为“加载命令”,紧跟在基本的mach_header之后,每一条指令都采用“类型-长度-值”的格式:32位的cmd值(表示类型),32位的cmdsize值(32位二进制位4的倍数,64位二进制为8的倍数),以及命令本身(由cmdsize指定的任意长度)。有一些命令是由内核加载器(定义在bsd/kern/mach_loader.c文件中)直接使用的,其他命令是由动态链接器处理的。
加载命令总共有30多条。如图列出了内核使用的那些命令(之后在讨论链接编辑器时会讨论剩下的命令):
image.png
加载过程在内核的部分负责新进程的基本设置--------分配虚拟内存,创建主线程,以及处理任何可能的代码签名/加密的工作。然而对于动态链接的可执行文件(大部分可执行文件都是动态链接的)来说,真正的库加载和符号解析的工作都是通过LC_LOAD_DYLINKER命令指定的动态链接器在用户态完成的。控制权会转交给链接器,链接器进而接着处理头文件中的其他加载命令(本章稍后会讨论库的加载)。
下面详细讨论这些加载命令。
LC_SEGMENT 以及进程虚拟内存设置
LC_SEGMENT(或LC_SEGMENT_64)命令是最主要的加载命令,这条命令指导内核如何设置新运行的进程的内存空间。这些“段”直接从Mach-O二进制文件加载到内存中。
每一条LC_SEGMETN[_64]命令都提供了段布局的所有必要细节信息,如图
image.png
image.png
有了LC_SEGMENT命令,设置进程虚拟内存的过程就变成遵循LC_SEGMENT命令的简单操作。对于每一个段,将文件中相应的内容加载到内存中:从偏移量为fileoff处加载filesize字节到虚拟内存地址vmaddr处的vmsize字节。每一个段的页面根据iniprot进行初始化,initprot指定了如何通过读/写/执行位初始化页面的保护级别。段的保护设置可以动态改变,但是不能超过maxprot中指定的值(在iOS中,+x和+w是互斥的)。
_PAGEZERO段(空指针陷阱)、_TEXT段(程序代码)、_DATA段(程序数据)和_LINKEDIT(链接器使用的符号和其他表)段提供了LC_SEGMENT命令。段有时候也可以进一步分解为区(section)。如图列出了一些常见的区:
image.png
段也可以设置一些<mach/loader.h>头文件中定义的标志。苹果使用的一个标志是SG_PROTECTED_VERSION_1(0x08),表示这个段的页面是“受保护的”,即加密的。苹果通过这个种技术加密一些二进制文件,例如Finder。如图所示:
image.png
为了支持这种代码加密,XNU内核包含了一个特殊的自定义(外部)虚拟内存管理器,其名称为“Apple protect”
在创建Mach-O对象时,可以通过-segcreate开关让Xcode的ld创建段。Xcode还包含一个名为segedit的特殊工具,可以用于提取或替换Mach-O文件中的段。这个工具可以用于提取内嵌的文本信息,例如内核的PRELINK_INFO区,此外,本书的伴随工具jtool也提供了这个功能。jtool还提供了另一个Xcode工具size的功能,能够打印出每一个段的大小和地址。
LC_UNIXTHREAD
当所有的库都完成加载之后,dyld的工作也完成了,之后由LC_UNIXTHREAD命令负责启动二进制程序的主线程(因此主线程总是在可执行文件中,而不会在其他二进制文件中,例如库文件)。根据架构的不同,这条命令列出所有初始化寄存器的状态,不同架构的寄存器状态不同,这些不同的架构包括i386_THREAD_STATE、x86 _THREAD_STATE64以及iOS中的ARM_THREAD_STATE。在任何一种架构中,大部分寄存器应该都会初始化为0,其中指令指针(Intel的IP)或程序计数器(ARM的r15)是例外,这些寄存器保存了程序入口点的地址。
在苹果完成抛弃PPC平台之前,在Lion中,还有一个PPC_THREAD_STATE。在某些包含了PPC代码的胖二进制文件中还能看到这个类型(例如在Snow Leopard中尝试运行otool -arch ppc -l/mach_kernel)。在这种情况下,寄存器srr0保存代码的入口点。
LC_THREAD
和LC_UNIXTHREAD的功能类似,LC_THREAD用于核心转储文件。Mach-O核心转储文件实际上是一个组LC_SEGMENT(或LC_SEGMENT_64)命令的集合,这些命令负责建立起进程的内存镜像(只不过现在进程已经无效了),然后就是最后一条LC_THREAD命令。LC_THREAD命令也包含几种不同的类型,每一种类型对应不同的机器状态(即线程、浮点和异常)。只要创建一个核心转储(这太简单了!)并通过otool -l查看这个核心转储文件就可以轻松找到这条命令了。
LC_MAIN
从Mountain Lion开始,一条新的加载命令LC_MAIN替代了LC_UNIXTHREAD命令。这条命令的作用是设置程序主线程的入口点地址和大小。这条命令比LC_UNIXTHREAD命令更实用一个些,
LC_CODE_SIGNATURE
Mach-O二进制文件有一个重要特性就是可以进行数字签名。尽管在OS X中仍然没怎么实用数字签名,不过由于代码签名和新改进的沙盒机制绑定在一起,所以签名的使用率也越来越高。在iOS中,代码签名是强制要求的,这也是苹果尽可能对系统封锁的另一种尝试:在iOS中只有苹果自己的签名才会被认可。在OS X中,codesign工具可以用于操纵和显示代码签名。man手册页,以及Apples code signing guide和Mac OS X Code Signing In Depth文档都从系统管理的角度详细解释了代码签名机制。
LC_CODE_SIGNATURE包含了Mach-O二进制文件的代码签名,如果这个签名和代码本身不匹配(或者如果在iOS上这条命令不存在),那么内核会立即给进程发送一个SIGKILL信号将进程杀掉,没有商量的余地,毫不留情。在ios4之前,还可以通过两条sysctl命令覆盖负责强制执行(利用内核的MAC,即Mandatory Access Control)的内核变量,从而实现禁用代码签名检查:
sysctl -w security.mac.pro_enforce=0 // 禁用进程的MAC
sysctl -w security.mac.vnode_enforce=0 //禁用VNode的MAC
而在之后版本的iOS中,苹果意识到只要能够获得root权限,越狱者就可以覆盖内核变量。因此这些变量成了读变量。untelthered越狱(即完美越狱)因为利用了一个内核漏洞所以可以修改这个些变量。由于这些变量的默认值都是启用签名检查,所以不完美越狱会导致非苹果签名的应用程序崩溃-------除非i设备以完美越狱的方式引导。
此外,通过Saurik的ldid这类工具可以在Mach-O中嵌入伪代码签名。这个工具可以替代OS X的codesign,允许生成自我签署的伪签名。这在iOS中尤为重要,因为签名和沙盒模型的应用程序“entitlement”绑定一起,而后者在iOS中是强制要求的。entitlement是声明式的许可(以plist的形式保存),必须内嵌在Mach-O中并且通过签名盖章,从而允许执行安全敏感的操作时具有运行时权限。
OS X和iOS都有一个特殊的系统调用csops用于代码签名的操作。
网友评论