前言
Objective-C 作为基于 Runtime 的语言, 有非常强大的动态特性, 可以在运行期间自省进行方法调剂为类增加属性修改消息转发链路, 在代码运行期间通过 Runtime 几乎可以修改 Objecitve-C 层的一切类方法以及属性
在 Objective-C 语言中可以通过
method_exchangeImplementations(Method m1, Method m2)
或
class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
交换两个方法的 IMP 地址, 对 C 语言的函数函数实现却无能为力, 今天给大家介绍的一个开源框架 fishhook 让 C 语言的函数实现也能被替换
fishhook 简介
fishhook 是一个由 facebook 开源的第三方框架, 其主要作用就是动态修改 C 语言函数实现
这个框架的代码其实非常的简单, 只包含两个文件: fishhook.c 以及 fishhook.h; 两个文件所有的代码加起来也不超过 300 行不过它的实现原理是非常有意思并且精妙, 打开 fishhook.h 会发现只有一个结构体和两个方法:
- struct rebinding {
- const char *name; // 原始函数名字符串
- void *replacement; // 替换后的新的函数指针
- void **replaced; // 与原始函数原型一致的函数的指针的指针
- };
- int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);
- int rebind_symbols_image(void *header,
- intptr_t slide,
- struct rebinding rebindings[],
- size_t rebindings_nel);
rebind_symbols 函数和 rebind_symbols_image 函数是用来 HOOK 函数的两个方法, 只不过参数不同而已, 前者比较简单, 两个参数一个是 rebinding 数组, 一个是数组中 rebinding 个数后者就稍微复杂点, 根据源码中的注释说明, 该函数是在仅指定镜像的时候使用所以, 我们这里直接使用 rebind_symbols 函数就可以了
使用 fishhook 修改 C 函数
使用 fishhook 修改 C 函数很容易, 我们就拿官方给的 Demo 来介绍它的使用方法
- #import "fishhook.h"
- // 声明一个与原函数签名相同的函数指针
- static int (*orig_open)(const char *, int, ...);
- // 重新实现 my_open 函数
- int my_open(const char *path, int oflag, ...) {
- va_list ap = {0};
- mode_t mode = 0;
- if ((oflag & O_CREAT) != 0) {
- // mode only applies to O_CREAT
- va_start(ap, oflag);
- mode = va_arg(ap, int);
- va_end(ap);
- printf("Calling real open('%s', %d, %d)\n", path, oflag, mode);
- return orig_open(path, oflag, mode); // 其实相当于执行原 open 函数
- } else {
- printf("Calling real open('%s', %d)\n", path, oflag);
- return orig_open(path, oflag, mode); // 其实相当于执行原 open 函数
- }
- }
- int main(int argc, char * argv[]) {
- @autoreleasepool {
- // 初始化一个 rebinding 结构体
- struct rebinding open_rebinding = { "open", my_open, (void *)&original_open };
- // 将结构体包装成数组, 并传入数组的大小, 对原符号 open 进行重绑定
- rebind_symbols((struct rebinding[1]){open_rebinding}, 1);
- // 调用 open 函数
- int fd = open(argv[0], O_RDONLY);
- uint32_t magic_number = 0;
- read(fd, &magic_number, 4);
- printf("Mach-O Magic Number: %x \n", magic_number);
- close(fd);
- return UIApplicationMain(argc, argv, nil, @"AppDelegate");
- }
- }
简单的几行代码就完成了 my_open 函数对 open 函数的替换, 所有调用 open 函数的地方实际上都会执行 my_open 的实现, 也就完成了对 open 的修改
fishhook 的原理以及实现
在介绍 fishhook 具体实现原理之前, 有几个非常重要的知识需要我们了解, 那就是 dyld 动态链接以及 Mach-O 文件系统对 Mach-O 不熟悉的同学可以参考以前的文章 Mach-O 可执行文件
dyld 与动态链接
dyld 是 Apple 的动态链接器, 系统 kernel 做好启动程序的初始准备后, 交给 dyld 负责, 关于其作用顺序, 可参考文章 dyld: Dynamic Linking On OS X, 相关部分翻译内容如下:
从 kernel 留下的原始调用栈引导和启动自己
将程序依赖的动态链接库递归加载进内存, 当然这里有缓存机制
non-lazy 符号立即 link 到可执行文件, lazy 的存表里
运行可执行文件的静态初始化程序
找到可执行文件的 main 函数, 准备参数并调用
程序执行中负责绑定 lazy 符号提供 runtime dynamic loading services 提供调试器接口
程序 main 函数 return 后执行 static terminator
某些场景下 main 函数结束后调 libSystem 的_exit 函数
一句话: 负责将各种各样程序需要的镜像加载到程序运行的内存空间中 且这个过程发生的时间非常早 --- 在 objc 运行时初始化之前
dyld 加载镜像后会执行相关回调函数, 当一个镜像被动态链接时, 都会执行回调
void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide)
传入文件的 mach_header 以及一个虚拟内存地址 intptr_t 当然, 我们也可以使用下面的方法注册自定义的回调函数, 同时也会为所有已经加载的镜像执行回调:
- extern void _dyld_register_func_for_add_image(
- void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide)
- );
需要注意的是, dyld 只负责将一些需要动态链接的库加载进来, 比如说 C 语言标准库, 或者 Foundation 这些 ObjC 提供的库; 而我们自己在程序中写的代码是不会通过 dyld 进行加载的
以一个最简单的 Hello World 程序为例:
- #include <stdio.h>
- int main(int argc, const char * argv[]) {
- printf("Hello, World!\n");
- return 0;
- }
代码中只引用了一个 stdio 库中的函数 printf; 我们如果 Build 这段代码, 生成可执行文件之后, 使用下面的命令 nm:
$ nm -nm HelloWorld
nm 命令可以查看可执行文件中的符号(对 nm 不熟悉的读者可以在终端中使用 man nm 查看手册):
- (undefined) external _printf (from libSystem)
- (undefined) external dyld_stub_binder (from libSystem)
- 0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
- 0000000100000f50 (__TEXT,__text) external _main
在可执行文件中的符号列表中,_printf 这个符号是未定义 (undefined) 的, 换句话说, 编译器还不知道这个符号对应什么东西
但是, 如果在文件中加入一个 C 函数 hello_world:
- #include <stdio.h>
- void hello_world() {
- printf("Hello, World!\n");
- }
- int main(int argc, const char * argv[]) {
- printf("Hello, World!\n");
- return 0;
- }
在构建之后, 同样使用 nm 查看其中的符号:
- (undefined) external _printf (from libSystem)
- (undefined) external dyld_stub_binder (from libSystem)
- 0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
- 0000000100000f30 (__TEXT,__text) external _hello_world
- 0000000100000f50 (__TEXT,__text) external _main
我们的符号 _hello_world 并不是未定义的(undefined), 它包含一个内存地址以及 __TEXT 段也就是说手写的一些函数, 在编译之后, 其地址并不是未定义的, 这一点对于之后分析 fishhook 有所帮助
使用 nm 打印出的另一个符号 dyld_stub_binder 对应另一个同名函数 dyld_stub_binder 会在目标符号 (例如 printf) 被调用时, 将其链接到指定的动态链接库 libSystem, 再执行 printf 的实现(printf 符号位于 __DATA 端中的 lazy 符号表中):
每一个镜像中的 __DATA 端都包含两个与动态链接有关的表, 其中一个是 __nl_symbol_ptr, 另一个是 __la_symbol_ptr:
__nl_symbol_ptr 中的 non-lazy 符号是在动态链接库绑定的时候进行加载的
__la_symbol_ptr 中的符号会在该符号被第一次调用时, 通过 dyld 中的 dyld_stub_binder 过程来进行加载
0000000100001010 dq 0x0000000100000f9c ; XREF=0x1000002f8, imp___stubs__printf
地址
0x0000000100000f9c
就是 printf 函数打印字符串实现的位置:
在上述代码调用 printf 时, 由于符号是没有被加载的, 就会通过 dyld_stub_binder 动态绑定符号
Mach-O
每一个 Mach-O 文件都会被分为不同的 Segments, 比如 __TEXT, __DATA, __LINKEDIT:
这也就是 Mach-O 中的 segment_command(32 位与 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 */
- };
而每一个 segment_command 中又包含了不同的 section:
- 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 */
- };
fishhook 的原理
dyld 通过更新 Mach-O 二进制文件 __DATA 段中的一些指针来绑定 lazy 和 non-lazy 的符号; 而 fishhook 先确定某一个符号在 __DATA 段中的位置, 然后保存原符号对应的函数指针, 并使用新的函数指针覆盖原有符号的函数指针, 实现重绑定
整个过程可以用这么一张图来表示:
其中最复杂的部分就是从二进制文件中寻找某个符号的位置, 在 fishhook 的 README 中, 有这样一张图:
这张图初看很复杂, 不过它演示的是寻找符号的过程, 我们根据这张图来分析一下这个过程:
从 __DATA 段中的 lazy 符号指针表中查找某个符号, 获得这个符号的偏移量
1061
, 然后在每一个 section_64 中查找 reserved1, 通过这两个值找到 Indirect Symbol Table 中符号对应的条目
在 Indirect Symbol Table 找到符号表指针以及对应的索引
16343
之后, 就需要访问符号表
然后通过符号表中的偏移量, 获取字符串表中相关函数的符号
fishhook 的实现
上面梳理了寻找符号的过程, 现在, 我们终于要开始分析 fishhook 的源代码, 看它是如何一步一步替换原有函数实现的
对实现的分析会 rebind_symbols 函数为入口, 首先看一下函数的调用栈:
- int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);
- extern void _dyld_register_func_for_add_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide));
- static void _rebind_symbols_for_image(const struct mach_header *header, intptr_t slide)
- static void rebind_symbols_for_image(struct rebindings_entry *rebindings, const struct mach_header *header, intptr_t slide)
- static void perform_rebinding_with_section(struct rebindings_entry *rebindings, section_t *section, intptr_t slide, nlist_t *symtab, char *strtab, uint32_t *indirect_symtab)
其实函数调用栈非常简单, 因为整个库中也没有几个函数, rebind_symbols 作为接口, 其主要作用就是注册一个函数并在镜像加载时回调:
- int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
- int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
- if (retval < 0) return retval;
- if (!_rebindings_head->next) {
- _dyld_register_func_for_add_image(_rebind_symbols_for_image);
- } else {
- uint32_t c = _dyld_image_count();
- for (uint32_t i = 0; i < c; i++) {
- _rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
- }
- }
- return retval;
- }
在 rebind_symbols 最开始执行时, 会先调用一个 prepend_rebindings 的函数, 将整个 rebindings 数组添加到 _rebindings_head 这个私有数据结构的头部:
- static int prepend_rebindings(struct rebindings_entry **rebindings_head,
- struct rebinding rebindings[],
- size_t nel) {
- struct rebindings_entry *new_entry = malloc(sizeof(struct rebindings_entry));
- if (!new_entry) {
- return -1;
- }
- new_entry->rebindings = malloc(sizeof(struct rebinding) * nel);
- if (!new_entry->rebindings) {
- free(new_entry);
- return -1;
- }
- memcpy(new_entry->rebindings, rebindings, sizeof(struct rebinding) * nel);
- new_entry->rebindings_nel = nel;
- new_entry->next = *rebindings_head;
- *rebindings_head = new_entry;
- return 0;
- }
也就是说每次调用的 rebind_symbols 方法传入的 rebindings 数组以及数组的长度都会以 rebindings_entry 的形式添加到 _rebindings_head 这个私有链表的首部:
- struct rebindings_entry {
- struct rebinding *rebindings;
- size_t rebindings_nel;
- struct rebindings_entry *next;
- };
- static struct rebindings_entry *_rebindings_head;
这样可以通过判断
_rebindings_head->next
的值来判断是否为第一次调用, 然后使用
_dyld_register_func_for_add_image
将
_rebind_symbols_for_image
注册为回调或者为所有存在的镜像单独调用
- _rebind_symbols_for_image
- :
- // 完成动态库的 binding 之后, 会回调这个函数其中 slide 跟 ALSR(Address space layout randomization)有关系, 是一个随机的加载地址
- static void _rebind_symbols_for_image(const struct mach_header *header, intptr_t slide) {
- rebind_symbols_for_image(_rebindings_head, header, slide);
- }
- _rebind_symbols_for_image
只是对另一个名字非常相似的函数
rebind_symbols_for_image
的封装, 从这个函数开始, 就到了重绑定符号的过程:
- static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
- const struct mach_header *header,
- intptr_t slide) {
- Dl_info info;
- if (dladdr(header, &info) == 0) {
- return;
- }
- segment_command_t *cur_seg_cmd;
- segment_command_t *linkedit_segment = NULL;
- struct symtab_command* symtab_cmd = NULL;
- struct dysymtab_command* dysymtab_cmd = NULL;
- // 从下面的代码来看, SEG_LINKEDIT 这个 segment 非常重要, Indirect Symbol TableSymbol TableString Table 的地址都要基于它获取
- uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
- for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
- cur_seg_cmd = (segment_command_t *)cur;
- if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
- if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
- linkedit_segment = cur_seg_cmd;
- }
- } else if (cur_seg_cmd->cmd == LC_SYMTAB) {
- symtab_cmd = (struct symtab_command*)cur_seg_cmd;
- } else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
- dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
- }
- }
- if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
- !dysymtab_cmd->nindirectsyms) {
- return;
- }
- // Find base symbol/string table addresses
- uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
- //linkedit_base+symtab_cmd->symoff 是 Symbol Table 的位置
- nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
- //linkedit_base+symtab_cmd->stroff 是 String Table 的位置
- char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
- // Get indirect symbol table (array of uint32_t indices into symbol table)
- uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
- cur = (uintptr_t)header + sizeof(mach_header_t);
- for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
- cur_seg_cmd = (segment_command_t *)cur;
- if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
- // 找到 DATA 和 DATA_CONST segment
- if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
- strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
- continue;
- }
- for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
- // 找到__nl_symbol_ptr 和__la_symbol_ptr 这两个 section
- section_t *sect =
- (section_t *)(cur + sizeof(segment_command_t)) + j;
- if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
- perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
- }
- if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
- perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
- }
- }
- }
- }
- }
这部分的代码主要功能是从镜像中查找
linkedit_segment symtab_command
和 dysymtab_command; 在开始查找之前, 要先跳过 mach_header_t 长度的位置, 然后将当前指针强转成 segment_command_t, 通过对比 cmd 的值, 来找到需要的 segment_command_t
在查找了几个关键的 segment 之后, 我们可以根据几个 segment 获取对应表的内存地址:
- uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
- nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
- char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
- uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
在 linkedit_segment 结构体中获得其虚拟地址以及文件偏移量, 然后通过一下公式来计算当前 __LINKEDIT 段的位置:
slide + vmaffr - fileoff
类似地, 在 symtab_command 中获取符号表偏移量和字符串表偏移量, 从 dysymtab_command 中获取间接符号表 (indirect symbol table) 偏移量, 就能够获得_符号表__字符串表_以及_间接符号表_的引用了
间接符号表中的元素都是 uint32_t *, 指针的值是对应条目 n_list 在符号表中的位置
符号表中的元素都是 nlist_t 结构体, 其中包含了当前符号在字符串表中的下标
- struct nlist_64 {
- union {
- uint32_t n_strx; /* index into the string table */
- } n_un;
- uint8_t n_type; /* type flag, see below */
- uint8_t n_sect; /* section number or NO_SECT */
- uint16_t n_desc; /* see <mach-o/stab.h> */
- uint64_t n_value; /* value of this symbol (or stab offset) */
- };
字符串表中的元素是 char 字符
该函数的最后一部分就开启了遍历模式, 查找整个镜像中的 SECTION_TYPE 为
S_LAZY_SYMBOL_POINTERS
或者
S_NON_LAZY_SYMBOL_POINTERS
的 section, 然后调用下一个函数
perform_rebinding_with_section
来对 section 中的符号进行处理:
- static void perform_rebinding_with_section(struct rebindings_entry *rebindings,
- section_t *section,
- intptr_t slide,
- nlist_t *symtab,
- char *strtab,
- uint32_t *indirect_symtab) {
- uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
- // 找到库函数的地址: section->addr <==> section(__DATA,__la_symbol_ptr)
- void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
- // 遍历 section 里面的每一个符号
- for (uint i = 0; i < section->size / sizeof(void *); i++) {
- // 查找 indirect_symbol_indices 数组, 获取其中的内容, 得到 symtab_index
- uint32_t symtab_index = indirect_symbol_indices[i];
- if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
- symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) {
- continue;
- }
- // 找到函数名位于 string table 的偏移
- uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
- // 找到函数名
- char *symbol_name = strtab + strtab_offset;
- if (strnlen(symbol_name, 2) < 2) {
- continue;
- }
- struct rebindings_entry *cur = rebindings;
- while (cur) {
- for (uint j = 0; j < cur->rebindings_nel; j++) {
- if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
- if (cur->rebindings[j].replaced != NULL &&
- indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
- // 把函数地址保存到 rebindings.replaced 里
- *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
- }
- // 替换内容为自定义函数地址
- indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
- goto symbol_loop;
- }
- }
- cur = cur->next;
- }
- symbol_loop:;
- }
- }
该函数的实现的核心内容就是将符号表中的 symbol_name 与 rebinding 中的名字 name 进行比较, 如果出现了匹配, 就会将原函数的实现传入 origian_open 函数指针的地址, 并使用新的函数实现 my_open 代替原实现
如果你理解了上面的实现代码, 该函数的其它代码就很好理解了:
通过
indirect_symtab + section->reserved1
获取
indirect_symbol_indices *
, 也就是符号表的数组
通过
(void **)((uintptr_t)slide + section->addr)
获取函数指针列表
indirect_symbol_bindings
遍历符号表数组
indirect_symbol_indices *
中的所有符号表中, 获取其中的符号表索引 symtab_index
通过符号表索引 symtab_index 获取符号表中某一个 n_list 结构体, 得到字符串表中的索引
symtab[symtab_index].n_un.n_strx
最后在字符串表中获得符号的名字 char *symbol_name
到这里比较前的准备工作就完成了, 剩下的代码会遍历整个 rebindings_entry 数组, 在其中查找匹配的符号, 完成函数实现的替换
在之后对某一函数的调用(例如 open), 当查找其函数实现时, 都会查找到 my_open 的函数指针; 在 my_open 调用 origianl_open 时, 同样也会执行原有的函数实现, 因为我们通过
*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i]
将原函数实现绑定到了新的函数指针上
fishhook 的局限
fishhook 在 dyld 加载镜像时, 插入了一个回调函数, 交换了原有函数的实现; 但是对于非动态链接库, 比如开发人员自己手写的函数没有作用, 因为自己手写的函数并不在任何的镜像中存在, 这也符合在最开始我们研究 dyld 时得出的结论:
dyld 只负责将一些需要动态链接的库加载进来, 比如说 C 语言标准库, 或者 Foundation 这些 ObjC 提供的库; 而我们自己在程序中写的代码是不会通过 dyld 进行加载的, 也就无法修改其实现
fishhook 的例子
聊聊苹果的 Bug - iOS 10 nano_free Crash
iOS 逆向工程之 fishhook
iOS 逆向之四 - FishHook 的简单使用
来源: https://juejin.im/post/5a97b8e851882555666f144b