天天看点

iOS系统分析(二)Mach-O二进制文件解析

0x01  mach-o格式简单介绍

mach-o文件格式是 os x 与 ios 系统上的可执行文件格式,类似于windows的 pe 文件 与 linux(其他 unix like)的 elf 文件,如果不彻底搞清楚mach-o的格式与相关内容,那么深入研究 xnu 内核就无从谈起。

mach-o文件的格式如下图所示:

iOS系统分析(二)Mach-O二进制文件解析

有如下几个部分组成:

1. header:保存了mach-o的一些基本信息,包括了平台、文件类型、loadcommands的个数等等。

2. loadcommands:这一段紧跟header,加载mach-o文件时会使用这里的数据来确定内存的分布。

3. data:每一个segment的具体数据都保存在这里,这里包含了具体的代码、数据等等。

0x02 fat二进制数据 ,数据结构定义在 \<mach-o/fat.h\>

iOS系统分析(二)Mach-O二进制文件解析
iOS系统分析(二)Mach-O二进制文件解析

1. 第一段为magic 魔数,这里注意大小端,读出来之后需要看下是0xcafebabe还是 0xbebafeca(否则即为thin),需要根据这个来转后续读取的字节的字节序。  可以看出来 前4byte 为 0xbebafeca ,说明为fat。

2. 第二段为arch count,也就是该app或dsym中包含哪些cpu架构,比如armv7、arm64等,这个例子中为2(后4byte  0x 00 00 00 02),表示包含了两种cpu架构。  

3. 后续段中包含cputype(0x  0c 00  00 01)、cpusubtype (0x 00 00 00 00)、offset (0x 00 10 00  00)、size(0x 00  f0 27 00)等数据,根据fat中的结构定义,依次读取,这里需要说明的是,如果只包含一种cpu架构的话,是没有这段fat头定义的,可以跳过这部分,直接读取arch数据。

4. 根据fat头中读取的offset数据,我们可以跳到文件对应的arch数据的位置,当然如果只有一种架构的话就不需要计算偏移量了。 下图给出解析的函数

iOS系统分析(二)Mach-O二进制文件解析

0x03 mach header二进制数据

通过magic我们可以区分出是32-bit还是64-bit,64-bit多了4个字节的保留字段,这里同样需要注意字节序的问题,也就是判断magic,来确定是否需要转换字节序。  

iOS系统分析(二)Mach-O二进制文件解析

根据mach-header与mach-header_64的定义,很明显可以看出,headers的主要作用就是帮助系统迅速的定位mach-o文件的运行环境,文件类型。

iOS系统分析(二)Mach-O二进制文件解析

filetype 

因为mach-o文件不仅仅用来实现可执行文件,同时还用来实现了其他内容

1. 内核扩展

2. 库文件

3. coredump

4.  其它

iOS系统分析(二)Mach-O二进制文件解析

下面是一些精彩用到的文件类型

1. mh-object    编译过程中产生的  obj文件 (gcc -c xxx.c 生成xxx.o文件)

2. mh-executable  可执行二进制文件 (/usr/bin/ls)

3. mh-core      coredump (崩溃时的dump文件)

4. mh-dylib  动态库(/usr/lib/里面的那些共享库文件)

5. mh-dylinker  连接器linker(/usr/lib/dyld文件)

6. mh-kext-bundle   内核扩展文件 (自己开发的简单内核模块)

flags

mach-o headers还包含了一些很重要的dyld的加载参数。

iOS系统分析(二)Mach-O二进制文件解析

1. mh-noundefs   目标没有未定义的符号,不存在链接依赖

2. mh-dyldlink     该目标文件是dyld的输入文件,无法被再次的静态链接

3. mh-pie      允许随机的地址空间(开启aslr  -\>address space layout randomization)

4. mh-allow-stack-execution   栈内存可执行代码,一般是默认关闭的。

5. mh-no-heap-execution   堆内存无法执行代码

iOS系统分析(二)Mach-O二进制文件解析

0x04 loadcommands

load commands 直接就跟在header后面,所有command占用内存的总和在mach-o header里面已经给出了。在加载过header之后就是通过解析loadcommand来加载接下来的数据了。定义如下:

iOS系统分析(二)Mach-O二进制文件解析

cmd字段

根据cmd字段的类型不同,使用了不同的函数来加载。简单的列出一张表看一看在内核代码中不同的command类型都有哪些作用。

1. lc-segment;lc-segment-64   在内核中由load-segment 函数处理(将segment中的数据加载并映射到进程的内存空间去)

2. lc-load-dylinker    在内核中由load-dylinker 函数处理(调用/usr/lib/dyld程序)

3. lc-uuid 在内核中由load-uuid 函数处理 (加载128-bit的唯一id)

4. lc-thread  在内核中由load-thread 函数处理 (开启一个mach线程,但是不分配栈空间)

5. lc-unixthread 在内核中由load-unixthread 函数处理 (开启一个unix posix线程)

6. lc-code-signature 在内核中由load-code-signature 函数处理 (进行数字签名)

7. lc-encryption-info 在内核中由 set-code-unprotect 函数处理 (加密二进制文件)

uuid 二进制数据    128byte

uuid是16个字节(128bit)的一段数据,是文件的唯一标识,前面提到的符号化时,这个uuid必须要和app二进制文件中的uuid一致,才能被正确的符号化。dwarfdump查看的uuid就是这段数据。读取这部分数据时通过command结构读取的,也就是第一段(0x0000001b)表示接下来的数据类型,第二段(0x00000018)数据的大小(包含command数据)。 

symtab 二进制数据

1. 符号表数据块结构,前二段依然是command数据。后边4段分别为符号在文件中的偏移量(0x001df5e0)、符号个数(0x001df5e0)、字符串在文件中的偏移量(0x0020c3a0)、字符串表大小(0x000729a8)。 

2. 接下来就是读取segment和section数据块了,和上面读取数据块结构一样是根据command结构读取,下图展示的segment数据和section数据,它们在二进制文件中它们是连续的,也就是每一条segment数据后面会紧跟着多条对应的section数据,section的数据总数是通过segment结构中的nsects决定的。 

3. 这里我写了一个简单地mach-o解析工具 [https://github.com/liutianshx2012/tmacho](https://github.com/liutianshx2012/tmacho)

iOS系统分析(二)Mach-O二进制文件解析

segment数据

加载数据时,主要加载的就是lc-segmet活着lc-segment_64。其他的segment的用途在这里不做深究。

lcsegment以及lc-segment-64 定义如下图。

iOS系统分析(二)Mach-O二进制文件解析
iOS系统分析(二)Mach-O二进制文件解析

可以看出,这里大部分的数据是用来帮助内核将segment映射到虚拟内存的。

nsects 字段,标示了segment中有多少secetion ,section是具体有用的数据存放的地方。

text的vmaddr也就是程序的加载地址; —dwarf中表明了dwarf数据块的信息,表示dsym是dwarf格式的数据结构。 

section数据

iOS系统分析(二)Mach-O二进制文件解析

从section数据中,我们可以找到—debug-info、—debug-pubnames, —debug-line等调试信息,通过这些调试信息我们可以找到程序中符号的起始地址、变量类型等信息。如果我们要符号化的话,就可以通过解析这些数据得到我们想要的信息。

symbol 数据

通过symtab中的数据可以得到symbol在文件中的位置和个数,symbol块数据中包含了符号的起始地址、字符串的偏移量等数据,这部分数据结构可以参考\<nlist.h\> 和 \<stabl.h\>。在这部分数据全部读取后,就可以读取所有的符号数据了,也就是接下来的数据。 

symbol string 数据

1. 通过symtab和symbo中的数据可以得到每个符号字符串在文件中的偏移量和大小,每个符号数据是以0结尾的字符串。 

2. 我们通过以上两部分数据的组合就可以得到每个symbo在程序中的加载地址了。这些数据对于以后做符号工作都非常的有帮助。

3. 到此,关于dsym文件中头部数据读取就完成了。头部数据都有相应的数据结构定义,读取时相对会比较容易些,解析数据时要注意字节序的问题,32-bit和64-bit数据结构的差异、字节长度的差异,dwarf版本的差异,每个数据块之间都是紧密联系的,一个字节的读取偏差就会造成后续数据的读取错误,正所谓差之毫厘,失之千里。