- Mach-O文件格式源码
- Mach-O苹果官方手册
想要程序跑起来,那么这个可执行文件的格式就需要被当前的操作系统所理解,比如:
Mach-O
来到Mach-O文件所在位置,输入相关命令得到Mach-O文件信息。为了更直观点,推荐方式2查看Mach-O文件。
首先要下载一个可以查看Mach-O文件格式的工具,本来想用 MachOView ,无奈下载完后打开Mach-O文件后,会闪退,就去逆向论坛找了个大神些的替代MachOView的工具 MachOExplorer 。我这里下载使用的是MachOExplorer,下载完后,打开,点击菜单栏的 file -> Mach-O文件 (此处我用模拟器的运行,打开了Debug-iphonesimulator文件夹,找到ipa,邮件显示包内容后可以获得可执行文件)。
在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 文件了。
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(保留字段)。
现在来解释说明一下这些字段都有什么意义:
/* 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 ,当内核执行到 LC_DYLINK
(后文讲述)的时候,链接器会启动,查找进程所依赖的动态库,并加载到内存中。
进程每一次启动,地址空间都会随机化。如果采用传统的方式,程序每启动一次,启动的虚拟内存镜像一致的话,黑客很容易就重写内存来破解程序。所以,ASLR可以有效避免黑客的攻击。
打开Xcode,来到Main函数,打断点,运行程序开启lldb调试。当到达断点位置时,在控制台输入:
/** 加载模块地址*/
image list -o -f
可以发现每次运行程序,地址都在变化。
这是dyld的一个独有特性,说是符号空间中还包括所在库的信息,这样子就可以让两个不同的库导出相同的符号,与其对应的是平摊名称空间.
方式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 紧跟在header之后,说明了操作系统应当如何加载文件中的数据,对系统内核加载器和动态链接器起了指导性作用。这些加载指令告诉加载器
如何处理二进制数据,有些命令是由内核处理的,有些是由动态链接器处理的。在源码中有明显的注释说明这些是动态链接器处理的。
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
方式1: 终端 - 演示查看load commands结构
终端输入:
otool -lv + 文件
方式2: 工具查看
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位)的段映射到进程的地址(内存)空间中。
LC_SEGMENT_64
代表将文件中64位的段映射到进程的地址空间。还有LC_SEGMENT、LC_DYLD_INFO_ONLY等等类型。将段对应的文件内容加载到内存中的流程:
从file offset处 加载 file size 大小到 虚拟内存VM Address处
。如果当前段是LC_SEGMENT_64(__PAGEZERO) ,则这个段的file offset、file size 、VM Address为0,因为这个段不具备访问权限,用来处理空指针的。
具体段说明
执行代码
以及 其他只读数据
。该段数据的保护级别为:VM_PROT_READ(读)、VM_PROT_EXECUTE(执行),防止内存中被修改。otool -l 文件名 | grep cryptid
)大写的表示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) */
};
结构字段说明
主要的节说明
查看__text 节的全部内容(二进制)
otool -s __TEXT __text + 文件
查看__text 节的全部内容(汇编)
查看__text 节的内容前10条数据(汇编)
利用MachOExplorer工具
image list -o -f
查看模块加载的地址,可以看到加载可执行文件的基地址 0x000fabcd p/x 0x000fabcd + 0x100000000
得到了一个新的地址$0本文参考了的文章:
趣探 Mach-O:文件格式分析
感谢。