底层原理

iOS中的Mach-O&重定向&符号绑定&符号重绑定

2021-07-15  本文已影响0人  希尔罗斯沃德_董

Mach-O

什么Mach-O

Mach-O为Mach Object文件格式的缩写,它是一种用于可执行文件,目标代码,动态库,内核转储的文件格式。作为a.out格式的替代,Mach-O提供了更强的扩展性,并提升了符号表中信息的访问速度。Mach-O是iOS、mac系统中的可执行文件格式。

目标文件.o
库文件
.a
.dylib
Framework
可执行文件
dyld
.dsym

使用lifo -info 可以查看MachO文件包含的架构
lipo -info MachO文件
使用lifo –thin 拆分某种架构
lipo MachO文件 –thin 架构 –output 输出文件路径
使用lipo -create 合并多种架构
lipo -create MachO1 MachO2 -output 输出文件路径

Mach-O文件结构

每个Mach-O文件包括一个Mach-O头,然后是一系列的载入命令,再是一个或多个,每个块包括0到255个。Mach-O使用REL再定位格式控制对符号的引用。Mach-O在两级命名空间中将每个符号编码成“对象-符号名”对,在查找符号时则采用线性搜索法。以下是苹果官方关于Mach-O的结构图:

苹果官方图片

通过MachOView可以查看更为详细的Mach-O的结构。如下图所示:

mac-o.jpg
struct mach_header_64 {
    uint32_t    magic;      /* mach-o 格式的标识符 */
    cpu_type_t  cputype;    /* cpu区分符 */
    cpu_subtype_t   cpusubtype; /* machine区分符 */
    uint32_t    filetype;   /* 文件类型 */
    uint32_t    ncmds;      /* 加载命令的个数 */
    uint32_t    sizeofcmds; /* 加载命令的字节数 */
    uint32_t    flags;      /* 程序的标识位 */
    uint32_t    reserved;   /* 保留字段 */
};
load_commands_detail.jpg

字段解析

LC_SEGMENT_64 将文件中(32位或64位)的段映射到进程地址空间中
LC_DYLD_INFO_ONLY 动态链接相关信息
LC_SYMTAB 符号地址
LC_DYSYMTAB 动态符号表地址
LC_LOAD_DYLINKER 使用谁加载,我们使用dyld
LC_UUID 文件的UUID
LC_VERSION_MIN_MACOSX 支持最低的操作系统版本
LC_SOURCE_VERSION 源代码版本
LC_MAIN 设置程序主线程的入口地址和栈大小
LC_LOAD_DYLIB 依赖库的路径,包含三方库
LC_FUNCTION_STARTS 函数起始地址表
LC_CODE_SIGNATURE 代码签名

text段是代码段。它用来放程序代码(code)。它通常是只读的。
data段是数据段。它用来存放初始化了的(initailized)全局变量(global)和初始化了的静态变量(static)。它是可读可写的。
bss段是全局变量数据段。它用来存放未初始化的(uninitailized)全局变量(global)和未初始化的静态变量

接下来先介绍数据区几个比较重要的模块:

_TEXT_text.jpg
_DATA_data.jpg
SymbolTable.jpg
StringTable.jpg
DynamicSymbolTable.jpg
LazySymbolPointers.jpg
Assembly.jpg

符号重定向

对于iOS程序来说,由于ASLR安全机制的原因,每次启动程序系统都会分配一个随机偏移值,程序启动时会根据偏移值进行符号地址修正。假设程序首地址的0x00000000, 随机偏移值是0x00008000, 那程序的首地址就变成0x00000000 + 0x00008000, 程序内某个符号的偏移地址是0x00001000,那么它的内存地址是0x00001000 + 0x00008000 = 0x00009000。符号重定向实际上就是在程序运行时,把符号的偏移地址加上启动时的随机偏移值得到符号的内存地址的一个过程。符号重定向针对的是程序内的符号。接下来我们通过下面的demo来进行验证。首先我们新建一个类MyObject, 然后定义
一个方法doSomething,然后运行:

demo.jpeg

获取编译后的可执行文件:

1.jpeg
2.jpeg
3.jpeg

这个黑色icon的文件就是我们的可执行文件,把这个文件拖入MachOView中:

mach-o.jpeg

接下来演示一下重定向的过程。首先在运行前- (void)doSomething入口打个断点,然后在xcode->Debug-> Debug WorkFlow -> Always show disassembly 进入汇编模式,运行然后就进入如下画面:

重定向-获取ASLR值.png

打开刚才的MachOView查看符号表,如下:

符号表.png

可以看到- (void)doSomething放的的符号-[MyObject doSomething]在文件中的偏移地址为5E28。接下来我们验证一下重定向的过程:

重定向.png

由图中可知方法doSomething的内存地址与程序的首地址的差值是不变的,而且是等于-[MyObject doSomething]在文件中的偏移值。

符号绑定

相对于符号重定向针对的是程序内的符号,符号绑定针对的是程序外的符号,比如所以依赖的动态库等。由于编译时并没有把动态库编译到程序内,只是在连接阶段生成动态符号表。但是生成这个动态符表的地址并不是符号的真是地址,只有在访问这个符号时才会调用系统的符号绑定函数进行进行绑定,并获取符号地址更新到符号表中。这个过程就叫符号绑定。

绑定流程

当程序首次访问外部函数的时候,它首先会访问外部函数的桩Symbol Stubs,并执行桩代码(Symbol Stubs中的Data字段对应的值),而这个桩代码执行后,最后会跳转到Lazy Symbol Pointers对应符号的地址。首次访问会根据这个地址在Assembly文件中找到相应的代码执行,最后调用dyld_stub_binder函数进行符号绑定。绑定完成之后就会更新Lazy Symbol Pointers表中的值,将符号地址直接写入到表中,再次访问的时候就可以直接访问这个地址而不需要在执行Assembly中的代码。 大致流程图如下:

绑定流程.jpg

下面我们利用NSLog(NSLog来自系统动态库Foundation)的例来对符号绑定整个过程进行演示。我们同时通过MachOView工具以及程序运行会的汇编调试来展示这个过程。同样的,以下面的demo为例:

demo.jpeg
访问SymbolStubs.jpg

红圈部分可以看到确实是访问了Simbol Stubs。

执行SymbolStubs中的代码.jpg

这一步可以看到实际上是执行Symbol Stubs中的Data断的代码,这个段代码最后会跳转到0x000000010015651c,这个地址是通过Lazy Symbol Pointers获取的。

访问Lazy Symbol Pointers.jpg
访问Assembly.png
Assembly.jpg
dyld_stub_binder.jpeg

为了进一步验证,执行br指令跳转到x16中的地址(实际上就是dyld_stub_binder函数地址)。进入如下页面:

dyld_stub_binder.jpeg
Assembly寻找函数dyld_stub_binder过程.png
Non-Lazy Symbol pointers.png
获取程序首地址.png
读取Non-Lazy Symbol Pointers中的运行时地址.jpeg

至此,NSlog函数第一次调用过程中的符号绑定流程就走完了。其他外部符号访问流程都是一样的。绑定成功之后会修改Lazy Symbol Pointers中的值为符号的内存地址,下次访问时不需要再次绑定,可以直接在Lazy Symbol Pointers进行访问。

第二次调用NSLog函数

查看Lazy Symbol Pointers中的编译时偏移地址:

第二次调用的Lazy Symbol Pointers.png

接下来我们进行第二次访问的验证。首先在第二次调用处打个断点,进入汇编调试,进入如下页面:

第二次调用.png

继续执行bl指令,跳转一个地址,进入如下页面:

第二次访问NSLog时的内存地址.jpeg
第二次直接跳转NSLog函数实现.png

最终看到第二次进入的时候Lazy Symbol Pointers中的值就已经是NSLog函数的地址了,程序就可以直接访问了。

符号重绑定

有前面知道符号的绑定过程,我们知道符号绑定的本质就是将外部符号的地址跟新到Lazy Symbol Pointers表中。符号重绑定实际上也就是修改Lazy Symbol Pointers中的值。常用的第三方库Fishhook之所以能够hook系统代码就是利用这个原理,直接修改了Lazy Symbol Pointers对应符号的地址值实现。这里我们以Hook系统函数NSLog为例,演示Fishhook工作的大致流程如下:

重绑定—StringTable.jpeg
重绑定-Symbols.jpeg
重绑定Indirect Symbol.png
重绑定Lazy Symbol Pointers.png

接下来通过汇编调试验证:
首先获取NSLog符号在Lazy Symbol Pointers中的偏移值:

重绑定_NSLog偏移值.png

拿到符号偏移地址为0xC000,开始进入汇编调试:

重绑定-demo.jpeg
重绑定-首地址.png
重绑定-第一次调用NSLog.png
重绑定-NSLog后hook前.png
重绑定-hook之后地址.png

hook之后符号地址就改成了我们自定义的函数myNSlog的地址。

上一篇 下一篇

猜你喜欢

热点阅读