基础知识提要
各种表在 Mach-O 文件中,是位于 Section 数据之后的一些记录数据。下面介绍本文会用到的几个表。
懒加载(lazy load),又叫做延迟加载。在实际需要使用该符号(或资源)的时候,该符号才会通过 dyld 中的 dyld_stub_binder
来进行加载。与之相对的是非懒加载(non-lazy load),这些符号在动态链接库绑定的时候,就会被加载。
在 Mach-O 中,相对应的就是 _nl_symbol_ptr(非懒加载符号表)和 _la_symbol_ptr(懒加载符号表)。这两个指针表,保存着与字符串表对应的函数指针。
Dynamic Symbol Table(Indirect Symbols): 动态符号表是加载动态库时导出的函数表,是符号表的 subset。动态符号表的符号 = 该符号在原所属表指针中的偏移量(offset)+ 原所属表在动态符号表中的偏移量 + 动态符号表的基地址(base)。在动态表中查找到的这个符号的值又等于该符号在 symtab 中的 offset。
Symbol Table(以下简称为 symtab): 即符号表。每个目标文件都有自己的符号表,记录了符号的映射。在 Mach-O 中,符号表是由结构体 n_list 构成。
|
|
以上为 n_list 的结构。通过在动态符号表中找的偏移,再加上符号表的基址,就可以找到这个符号的 n_list,其中 n_strx 的值代表该字符串在 strtab 中的偏移量(offset)。关于 n_list 的具体结构解析详见 nlist-Mach-O文件重定向信息数据结构分析
String Table(以下简称为 strtab): 是放置 Section 名、变量名、符号名的字符串表,字符串末尾自带的 \0 为分隔符(机器码00)。知道 strtab 的基地址(base),然后加上在 Symbol Table 中找到的该字符串的偏移量(offset)就可以找到这个字符串。
fishhook 概述
fishhook 是 facehook 开源的重绑定 Mach-O 符号的库,用来 hook C 语言函数(即只能重绑定 C 符号)。主要原因在于只针对 C 语言做了符号修饰。
基本思路为:
- 先找到 Mach-O 文件的 Load_Commands 中的 LC_SEGMENT_64(_DATA),然后找到这条加载指令下的 Section64 Header(_nl_symbol_ptr),以及 Section64 Header(_la_symbol_ptr);
- 其中 Section Header 字段的
reserved1
的值即为该 Section 在 Dynamic Symbol Table 中的 offset。然后通过定位到该 Section 的数据,找到目标符号在 Section 中的偏移量,与之前的 offset 相加,即为在动态符号表中的偏移;- 通过 Indirect Symbols 对应的数值,找到在 symtab 中的偏移,然后取出 n_list->n_un->n_strx 的值;
- 通过这个值找到在 strtab 中的偏移,得到该字符串,进行匹配置换。
源码解析
fishhook 的源文件很少,只有一个 .h 头文件和一个 .c 文件,其中 fishhook.h 文件只暴露出了两个函数接口和一个结构体。
|
|
rebind_symbols 函数
以 ReadMe 中的示例为例,先是声明与将要被 hook 的函数签名相同的函数指针,接着自定义了替换后的函数,my_close、my_open。然后在 main 函数中调用 rebind_symbols 函数。
1 | rebind_symbols((struct rebinding[2]){{"close", my_close, (void *)&orig_close}, {"open", my_open, (void *)&orig_open}}, 2); |
在传递的参数中定义了一个结构体数组,传递了两个 rebinding 结构体,以及数组的个数2。
1 | static struct rebindings_entry *_rebindings_head; |
在 rebind_symbols 函数中,首先调用了 prepend_rebindings 函数,传入了 rebindings_head 的二级指针, rebind_symbols 函数参数中的 rebindings 数组,以及数组个数。然后将这个函数的返回值作为整个函数的返回值。
如果这个函数返回值>0,且 _rebindings_head->next 的值为空(其具体含义在 prepend_rebindings 函数中讲),则调用_dyld_register_func_for_add_image 来注册回调函数 _rebind_symbols_for_image。
在 dyld 加载镜像(即 image,在 Mach-O 中,所有的可执行文件、dylib、Bundle 都是 image)的时候,会执行注册过的回调函数。这一步可以使用 _dyld_register_func_for_add_image 方法来注册自定义的回调函数,传入这个 image 的 mach_header 和 slide,同时也会为所有已加载的 image 执行回调。
|
|
如果 _rebindings_head->next 的值不为空,则直接调用回调函数。
prepend_rebindings 函数
|
|
这里主要是一个将 rebingdings 数组拷贝到 new_entry 结构体中,并把这个结构体添加到 _rebings_head 这个链表首部的操作。首先定义一个 rebindings_entry 类型的 new_entry 结构体,并初始化,给 new_entry 以及 new_entry->rebindings 分配内存。
然后拷贝传入的参数数组 rebindings 到 new_entry->rebindings 中。同时给 new_entry->rebindings_nel 赋值为数组的个数,将 new_entry->next 赋值为 *rebindings_head 指针,即 _rebindings_head 内的数值。最后再使 _rebindings_head 与 new_entry 指向同一个地址。
这里比较容易混淆的是 rebindings_head 与 _rebindings_head。rebind_symbols 函数调用 prepend_rebindings 函数时,传入的是 &_rebindings_head
,也就是结构体指针的地址,是一个二级指针。prepend_rebindings 函数接收这个参数用的是 struct rebindings_entry **rebindings_head
,也就是说 *rebindings_head 就是 _rebinding_head 指针。
上面的动图很容易看出,这个链表是如何形成的。回到 rebind_symbols 函数中的遗留问题,_rebindings_head->next 的值为空时,是什么意思?这意味着 rebind_symbols 函数第一次被调用,因为之后被调用,_rebindings_head->next 都指向的是前一个被添加进链表的 new_entry。也只有在第一次被调用时,才需要注册回调函数,之后都是直接调用即可。
rebind_symbols_for_image 函数
在 _rebind_symbols_for_image 中,就执行了一个调用 rebind_symbols_for_image 函数的操作。接下来是比较核心的部分了。
1 | static void rebind_symbols_for_image(struct rebindings_entry *rebindings, |
首先定义了4个会被用到的结构体指针。其中 segment_command_t 就是LC_SEGMENT_64 结构,symtab_command 是 Section Header 中 LC_SYMTAB 的结构,dysymtab_command 是 Section Header 中 LC_DYSYMTAB 的结构。(有关 Mach-O 文件的结构可以参考我之前的文章 解读 Mach-O 文件格式)
接下来跳过 Mach-O 的 Header 结构,开始遍历 Load Commands。通过 Header->ncmds,以及 Segment->cmdsize 来控制循环。通过遍历,找到 LC_SEGMENT_64(_LINKEDIT),赋值给 linkedit_segment,然后给 symtab_cmd 和 dysymtab_cmd 赋值。
|
|
通过找到的 _LINKEDIT 段和传入的参数 slide 来计算 base 地址。也就是这个 Mach-O 文件在 ASLR 偏移后的首地址(因为这里用到的这些表都是属于 _LINKEDIT 段的)。 base = vmaddr - fileoffset + slide。
然后 base + symtab 段的 Symbol Table Offset(该表在文件中的偏移) = symtab 的首地址(该表在内存中的偏移),base + symtab 段的 String Table Offset = strtab 的首地址。base + DYSYTAB 段的 IndSym Table Offset = 动态符号表的首地址。
|
|
这是一个嵌套循环,外层循环依旧是遍历 Load Commands,内循环则是遍历 LC_SEGMENT_64(_DATA) 段内的 Section Header,通过 Section->flags & SECTION_TYPE 来寻找 _nl_symbol_ptr 和 _la_symbol_ptr。找到后调用 perform_rebinding_with_section 函数。
在循环内的 if 语句嵌套,一般最多用两层,太多层会显得代码冗杂,且可读性较差,容易出错。那两层以上怎么办呢?fishhook 给了我们一个很好的示范。用 if 语句作非判断,然后加上 continue 跳出本次循环。详见上面的代码。
perform_rebindin_with_section 函数
|
|
这里需要注意的是指针的加法。比如 uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
这句代码中,三个变量均为 uint32_t 格式,即4个字节,那么 indirect_symtab 指针实际应该加上 (reserved1 的值 * 4)个字节。以 _nl_symbol_ptr 为例:
reserved1(也就是上图 MachOView 中显示的 Indirect Sym Index)为25,那么在动态符号表中 _nl_symbol_ptr 所在的首地址应该是(先不考虑 slide): Dynamic Symbol Table 的首地址 + (reserved1 * 4) = 0x100005F30 + 0x64 = 0x100005F94。
slide + section->addr 为 _nl_symbol_ptr section 数据所在的地址。然后遍历 dysymtab 中从 _nl_symbol_ptr 开始的符号,取得 Symbol 数据,如果为 INDIRECT_SYMBOL_ABS(即懒加载符号指针结束的地方)等,则跳出本次循环。否则将取得的 Symbol 数据作为 symtab 中的 offset。
如上图所示,_dyld_stub_binder 符号(也是这个程序中的 _nl_symbol_ptr 的首个符号)在 dysymtab 中的 Symbol 数据为0xA1,那么在对应的 symtab 中,它的地址应为 symtab 首地址 + 0xA1 * 16 = 0x10005510 + 161 * 16 = 0x10005F20。(16是 n_list 结构共16个字节)
Symbol Table 中的 n_strx(之前提到的 n_list 结构)即为 strtab 中的 index。
最后就是匹配替换了。比较字符串表中的字符串与 rebindings 数组中的 name 字段,匹配成功后,将 _nl_symbol_ptr 或 _la_symbol_ptr 这两个 Section 的指针表中对应的函数指针(indirect_symbol_bindings[i]
)赋值给 rebindings 数组中的 replaced 字段,然后用数组中的 replacement 字段(也就是自定义的 my_open 或 my_close 函数的指针)覆盖原来的函数指针。
这里使用了 goto 来跳出双重循环,值得参考。
Reference
[1] 动态修改 C 语言函数的实现 http://draveness.me/fishhook/
[2] 趣探 Mach-O:FishHook 解析 http://www.open-open.com/lib/view/open1487057519754.html
[3] 编译体系漫游 http://www.tuicool.com/articles/uI7Bria