Mach-O二进制格式
Mach-Object,简称Mach-O,是苹果自己独有的二进制文件格式,运行在OSX和iOS上。
一个Mach-O文件由3个模块组成,文件头(Header)、加载命令(Load Commands)、数据块(Data)。
- Header : 保存了Mach-O的一些基本信息,包括cpu类型、文件类型、loadCommands等。
- LoadCommands : 加载命令,紧跟在Header后面。
- Data : 保存每一个segment的具体数据,包含了具体的代码、数据等。
1、Mach-O文件头
我们可以在dyld源码中找到对应的定义,在<mach-o/loader.h>头文件中:
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 */
};
/* Constant for the magic field of the mach_header (32-bit architectures) */
#define MH_MAGIC 0xfeedface /* the mach magic number */
#define MH_CIGAM 0xcefaedfe /* NXSwapInt(MH_MAGIC) */
/*
* The 64-bit mach header appears at the very beginning of object files for
* 64-bit architectures.
*/
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 */
};
/* Constant for the magic field of the mach_header_64 (64-bit architectures) */
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
#define MH_CIGAM_64 0xcffaedfe /* NXSwapInt(MH_MAGIC_64) */
不论是32位还是64位,都可以用下图表示:
- magic :魔数值,也可以说是特征值,加载器可以通过这个魔数值快速判断二进制文件运行在32位还是64位系统上。在源码中可以看出都是定义为固定数值的宏。一些可执行文件的魔数参见表1-1。
- cputype/cpusubtype:CPU类型和子类型,定义在<mach/machine.h>头文件中。
- filetype :文件类型。可执行文件、库文件、核心转储文件、内核扩展等,详见表1-2。
- ncmds/sizeofcmds:用于加载器的加载命令,数量和大小。
- flags:动态链接器(dyld)的标志,常见的详见表1-3。
- Reserved:仅64位有,额外的预留字段,目前还没有使用。
表 1-1 文件类型和魔数:
文件类型 | 魔数 | 用途 |
---|---|---|
Mach-O文件 | 0xfeedface(32位)/0xfeedfacf(64位) | OS X原生二进制格式 |
通用二进制文件 | 0xcafebabe(小尾顺序)/0xbebafeca(大尾顺序) | 包含多种架构支持的二进制格式,只在OS X上支持 |
解释器脚本 | #! | unix脚本和一些解释器脚本使用的格式:主要用于shell脚本,也常用于其他解释器,例如Perl、AWK、PHP等。内核寻找#!后面跟着的字符串,,然后执行这个字符串表示的命令。文件剩下的部分通过标准输入(stdin)传递给这个命令 |
ELF | \x7FELF | 可执行文件和库文件格式:Linux和大部分Unix的原生格式,OS X不支持ELF |
PE32/PE32+ | MZ | 可移植的可执行格式:Windows和Intel的EFI(Extensible Firmware Interface)二进制文件的原生格式。尽管OS X不支持这个格式,但是引导器支持这个格式,可以加载boot.efi文件 |
- 大端小端模式
字节存储有大尾顺序和小尾顺序之分,也就是大端小端模式。
例如数值0x1234使用两个字节储存:高位字节是0x12,低位字节是0x34。
所谓的大端模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中。高位字节在前,低位字节在后。
高位在低地址,低位在高地址(0x1234)
所谓的小端模式,是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中。低位字节在前,高位字节在后。
高位在高地址,低位在低地址(0x3412)
- 解释器脚本
解释器脚本格式实际上只是一种特殊形式的二进制文件格式,因为这些文件只不过是指向"真正"二进制的脚本,而这些被指向的文件才是真正得到执行的文件。
表 1-2 Mach-O文件类型:
文件类型 | 定义值 | 用途 | 备注 |
---|---|---|---|
MH_OBJECT | 0x1 | 可重定位的目标文件,编译器对源代码变异得到的(中间)结果,也可以是32位的内核扩展 | 通过gcc -c xxx.c 生成xxx.o文件 |
MH_EXECUTE | 0x2 | 可执行二进制文件 | 应用程序的二进制文件 |
MH_FVMLIB | 0x3 | VM共享库文件 | xxx |
MH_CORE | 0x4 | 核心转储文件 | 核心转储时生成 |
MH_PRELOAD | 0x5 | 预加载执行文件 | xxx |
MH_DYLIB | 0x6 | 动态库 | /usr/lib中的库文件 |
MH_DYLINKER | 0x7 | 动态链接器 | dyld |
MH_BUNDLE | 0x8 | 插件:非独立的二进制文件,要加载至其他二进制文件才能发挥作用 | 通过gcc -bindle生成,Xcode里面可以创建bundle的Target |
MH_DYLIB_STUB | 0x9 | 静态共享库桩 | xxx |
MH_DSYM | 0xa | 辅助的符号文件以及调试信息 | 通过gcc -g生成,Xcode->Product->Archive打包之后,里面就会生成一个叫DSYM后缀名的文件夹 |
MH_KEXT_BUNDLE | 0xb | 内核扩展 | 64位的内核扩展,常见于/System/Library/Extensions下 |
表1-3 Mach-O头文件常见标志(flags):
标志 | 定义值 | 用途 |
---|---|---|
MH_NOUNDEFS | 0x1 | 表示目标文件没有带有未定义的符号。这些目标文件大部分都是静态二进制文件,没有进一步的链接依赖关系 |
MH_DYLDLINK | 0x4 | 该目标文件是dyld的输入文件,无法被再次的静态链接 |
MH_TWOLEVEL | 0x80 | 该镜像使用的是两级名称空间绑定 |
MH_WEAK_DEFINES | 0x8000 | 二进制文件包含了弱符号 |
MH_BINDS_TO_WEAK | 0x10000 | 二进制文件链接了弱符号 |
MH_ALLOW_STACK_EXECUTION | 0x20000 | 允许栈执行。只有可执行文件(MH_EXECUTE)可以使用这个标志,但是通常不建议使用,当发生缓冲区溢出时,可执行的栈会给代码注入带来方便 |
MH_PIE | 0x200000 | 对可执行文件启用地址空间随机化分布,仅MH_EXECUTE可用 |
MH_NO_HEAP_EXECUTION | 0x1000000 | 将堆标记为不可执行。可以防止"堆喷"的攻击技术,使用这种技术的黑客盲目地用shellcode覆盖大量的堆空间,然后投机地跳转到这些地址中,企图能够跳转到自己的代码并且执行 |
代码注入常见方法是使用栈变量(即自动变量),因此默认情况下栈都标记为不可执行。而MH_ALLOW_STACK_EXECUTION(允许栈执行)这个标志可以覆盖这种行为,但是非常危险。堆则默认可执行,尽管完全可能被注入,但是通过堆注入代码相对困难一些。
我们可以用MachOView打开一个可执行文件,如下:
otool工具是可以查看Mach-O文件的原生工具。
$ otool -hV /bin/ls
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 X86_64 ALL LIB64 EXECUTE 18 1816 NOUNDEFS DYLDLINK TWOLEVEL PIE
Mach-O文件头的主要功能在于加载命令(Load Commands),加载命令紧跟在文件头之后。从文件头中可以知道加载命令的数量和大小(ncmds/sizeofcmds)。
2、Load Commands
dyld源码中定义了50多条Load Commands,这些指令在被调用时清晰地指导了如何设置并加载二进制数据。
所有加载命令的前两个字段必须是cmd和cmdsize:
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
cmd表示类型,cmdsize表示大小,32位体系架构的cmdsize必须是4字节的倍数,对于64位体系结构,必须是8字节的倍数(这永远是任何加载命令的最大对齐)。填充的字节必须为零,目标文件中的所有表也必须遵循这些规则,这样目标文件才能进行内存映射。
表1-4 LoadCommands类型:
命令 | 定义值 | 用途 |
---|---|---|
LC_SEGMENT | 0x1 | 将文件中(32位)的段映射到内存地址空间中 |
LC_SEGMENT_64 | 0x19 | 将文件中(64位的段映射到内存地址空间中 |
LC_LOAD_DYLINKER | 0xe | 加载一个动态链接器,即dyld |
LC_LOAD_DYLIB | 0xc | 加载动态链接库,大部分可执行文件都是动态链接的,包括系统提供和第三方的库 |
LC_UUID | 0x1b | 一个唯一的128位ID,这个ID匹配一个二进制文件及其对应的符号 |
LC_THREAD | 0x4 | 开启一个Mach线程,但是不分配栈(很少在核心转储文件之外使用) |
LC_UNIXTHREAD | 0x5 | 开启一个UNIX线程(初始化栈布局和寄存器)。设置程序主线程的入口点地址和栈大小,通常情况下,除了指令指针/程序计数器之外,所有的寄存器值都为0,Lion之后使用LC_MAIN |
LC_CODE_SIGNATURE | 0x1d | 代码签名 |
LC_ENCRYPTION_INFO | 0x21 | 加密的二进制文件 |
LC_DYLD_INFO_ONLY | 0x22 | 压缩dyld信息 |
LC_SYMTAB | 0x2 | 链接器桩的符号表信息 |
LC_DYSYMTAB | 0xb | 动态链接器符号表信息 |
LC_VERSION_MIN_IPHONEOS | 0x25 | 手机的最低支持版本 |
LC_FUNCTION_STARTS | 0x26 | 压缩表函数起始地址 |
LC_SEGMENT(或LC_SEGMENT_64)命令是最主要的加载命令,这条命令指导内核如何设置新运行的进程的内存空间。这些 segment直接从Mach-O二进制文件加载到内存中。
LC_SEGMENT、LC_SEGMENT_64结构定义:
struct segment_command { /* for 32-bit architectures */
uint32_t cmd; /* LC_SEGMENT */
uint32_t cmdsize; /* includes sizeof section structs */
char segname[16]; /* segment name */
uint32_t vmaddr; /* memory address of this segment */
uint32_t vmsize; /* memory size of this segment */
uint32_t fileoff; /* file offset of this segment */
uint32_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 */
};
/*
* The 64-bit segment load command indicates that a part of this file is to be
* mapped into a 64-bit task's address space. If the 64-bit segment has
* sections then section_64 structures directly follow the 64-bit segment
* command and their size is reflected in cmdsize.
*/
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 */
};
参数 | 用途 |
---|---|
segname | segment的名字 |
vmaddr | segname的虚拟物理地址 |
vmsize | segname分配的虚拟内存大小 |
fileoff | segname在文件中的偏移量 |
filesize | segname在文件中占用的字节数 |
maxprot | segname的页面所需要的最高内存保护 |
initprot | segname的页面最初始的内存保护 |
nsects | segname中区(section)数量 |
flags | 标志位 |
通过LC_SEGMENT命令,设置进程虚拟内存的过程就变成遵循LC_SEGMENT命令的简单操作。对于每一个段,将文件中相应的内容加载到内存中:从偏移量为fileoff处加载filesize字节到虚拟内存地址vmaddr处的vmsize字节。每一个段的页面都根据initprot进行初始化,initprot指定了如何通过读/写/执行位初始化页面的保护级别。段的保护设置可以动态改变,但是不能超过maxprot中指定的值。
_PAGEZERO段(空指针陷阱)、_TEXT段(程序代码)、_DATA段(程序数据)和_LINKEDIT(链接器使用的符号和其他表)段提供了LC_SEGMENT命令。
从图2-1中可以看出,_PAGEZERO段的fileoff=0,filesize=0,vmaddr=0,vmsize=16384=0x4000,那么该段就是从偏移量为0处加载0个字节到虚拟地址为0处的16384字节,那么下一段的vmaddr地址应该就是从0x4000开始向后分配。如下图所示:
_TEXT段则是从偏移量为0处加载15335424个字节到虚拟地址为0x4000处的15335424字节。因此这一段的虚拟地址分配为0x4000+15335424=0xEA4000,也就是占用15335424字节大小,内存地址为0x4000到0xEA4000。这里刚好filesize和vmsize的大小相等。同理下一段的vmaddr起始位置为0xEA4000。如下所示:
图2-3中可以看到filesize和vmsize的大小是不相等的,vmsize应该比filesize大,一个是文件实际大小,一个是分配的内存空间。
我们再看一看fileoff,_PAGEZERO的fileoff是0,filesize也是0,在_TEXT段中fileoff还是0,_TEXT中filesize是15335424,然后在_DATA段中fileoff是15335424。这样看来fileoff对应的就是字节的实际大小偏移量,_DATA段中filesize是3375104,那么在下一段中
fileoff就应该是15335424+3375104=18710528。vmaddr可以理解为虚拟内存地址偏移量,也就是每一个段的起始地址。
3. Sections
段有时候也可以进一步分解为区(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) */
};
struct section_64 { /* for 64-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint64_t addr; /* memory address of this section */
uint64_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) */
uint32_t reserved3; /* reserved */
};
区名称 | 用途 |
---|---|
__text | 主程序代码 |
__stub_helper、__stubs | 用于动态链接的桩 |
__cstring | 程序中硬编码的C语言字符串 |
__const | 用const修饰的常量变量以及硬编码的常量 |
__objc_methname | Objective-C方法名 |
__objc_methtype | Objective-C方法类型 |
__objc_classname | Objective-C类名称 |
__objc_classlist | Objective-C类列表 |
__objc_protolist | Objective-C原型 |
__objc_imginfo | Objective-C镜像信息 |
__objc_const | Objective-C常量 |
__objc_catlist | Objective-C类别列表 |
__objc_selrefs | Objective-C自引用(this) |
__objc_protorefs | Objective-C原型引用 |
__objc_classrefs | Objective-C类引用 |
__objc_superrefs | Objective-C超类引用 |
__nl_symbol_ptr | 非懒加载符号指针表 |
__la_symbol_ptr | 懒加载符号指针表 |
__mod_init_func | 模块初始化函数 |
__objc_ivar | 属性 |
__objc_data | Objective-C数据 |
__cfstring | 程序中使用的Core Foundation字符串(CFStrings) |
segment_command flags类型定义:
/* Constants for the flags field of the segment_command */
#define SG_HIGHVM 0x1 /* the file contents for this segment is for
the high part of the VM space, the low part
is zero filled (for stacks in core files) */
#define SG_FVMLIB 0x2 /* this segment is the VM that is allocated by
a fixed VM library, for overlap checking in
the link editor */
#define SG_NORELOC 0x4 /* this segment has nothing that was relocated
in it and nothing relocated to it, that is
it maybe safely replaced without relocation*/
#define SG_PROTECTED_VERSION_1 0x8 /* This segment is protected. If the
segment starts at file offset 0, the
first page of the segment is not
protected. All other pages of the
segment are protected. */
#define SG_READ_ONLY 0x10 /* This segment is made read-only after fixups */
这里主要有一个标志需要注意,SG_PROTECTED_VERSION_1表示这个段的页面是加密的,Finder就是使用这种加密方式。
$ otool -lV /System/Library/CoreServices/Finder.app/Contents/MacOS/Finder
/System/Library/CoreServices/Finder.app/Contents/MacOS/Finder:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 X86_64 ALL LIB64 EXECUTE 80 9928 NOUNDEFS DYLDLINK TWOLEVEL BINDS_TO_WEAK PIE MH_HAS_TLV_DESCRIPTORS
Load command 0
cmd LC_SEGMENT_64
cmdsize 72
segname __PAGEZERO
vmaddr 0x0000000000000000
vmsize 0x0000000100000000
fileoff 0
filesize 0
maxprot ---
initprot ---
nsects 0
flags (none)
Load command 1
cmd LC_SEGMENT_64
cmdsize 1112
segname __TEXT
vmaddr 0x0000000100000000
vmsize 0x0000000000757000
fileoff 0
filesize 7696384
maxprot rwx
initprot r-x
nsects 13
flags PROTECTED_VERSION_1
Xcode包含了一个名为segedit的特殊工具,可以用于提取或替换Mach-O文件中的段。
https://samhuri.net/posts/2010/01/basics-of-the-mach-o-file-format