Mach-O应用 fishhook动态修改C函数

fishhook简介

C 语言往往会给我们留下不可修改的这一印象,而 fishhook 是一个由 facebook 开源的第三方框架,其主要作用就是动态修改 C 语言函数实现

这个框架的代码其实非常的简单,只包含两个文件:fishhook.c 以及 fishhook.h;两个文件所有的代码加起来也不超过 300 行。不过它的实现原理是非常有意思并且精妙的

fishhook 提供非常简单的两个接口以及一个结构体:

1
2
3
4
5
6
7
8
9
10
11
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);

我们可以从 fishhook 提供的demo中上手实践一下,这里的demo对 close 进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//main.m
#import <UIKit/UIKit.h>
#import "AppDelegate.h"

#import "dlfcn.h"
#import "fishhook.h"

//声明一个与原函数签名相同的函数指针,用保存原始的函数的地址
static int (*orig_close)(int);

//新的close
int my_close(int fd) {
printf("做一些额外操作\n");
printf("调用原 close(%d)\n", fd);
//调用的 orig_close 其实相当于执行原 close
return orig_close(fd);
}

int main(int argc, char * argv[]) {
@autoreleasepool {

struct rebinding closeBind;
//函数的名称
closeBind.name = "close";
//新的函数地址
closeBind.replacement = my_close;
//保存原始函数地址的变量的指针
closeBind.replaced = (void *)&orig_close;
//定义数组
struct rebinding rebs[] = {closeBind};
/*
对符号进行重绑定 arg1 : 存放rebinding结构体的数组 arg2 : 数组的长度
*/
rebind_symbols(rebs, 1);

// 开始测试:
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, NSStringFromClass([AppDelegate class]));
}
}

最后查看输出的信息可以看到 在对符号进行重绑定之后,所有调用 close 函数的地方实际上都会执行 my_close 的实现,也就完成了对 close 的修改。

那么 fishhook 是如何做到的呢?

fishhook 原理

fishhook 是 FaceBook 开源的可以动态修改 MachO 符号表的工具。fishhook 的强大之处在于它可以 HOOK 系统的静态 C 函数。

大家都知道 OC 的方法之所以可以 HOOK 是因为它的运行时特性,OC 的方法调用在底层都是 msg_send(id,SEL)的形式,这为我们提供了交换方法实现(IMP)的机会,但 C 函数在编译链接时就确定了函数指针的地址偏移量(Offset),这个偏移量在编译好的可执行文件中是固定的。既然 C 函数的指针地址是相对固定且不可修改的,那么 fishhook 又是怎么实现 对 C 函数的 HOOK 呢?其实内部/自定义的 C 函数 fishhook 也 HOOK 不了,它只能HOOK Mach-O 外部(共享缓存库中)的函数

现在先来看一下以下这几个知识点:

动态链接

动态链接就是负责将各种各样程序需要的镜像加载到程序运行的内存空间中,这个过程发生的时间非常早: 在 objc 运行时初始化之前

先来看一个简单的demo为例:

1
2
3
4
5
6
7
8
9
10
// helloFishhook.m
#include <stdio.h>

void hello_world() {
printf("Hello, World!\n");
}
int main(int argc, const char * argv[]) {
printf("Hello, World!\n");
return 0;
}

如我们在 iOS编译过程 中实践的一样 ,先用clang编译一下,再用nm命令查看符号:

1
2
3
4
5
6
7
#clang helloFishhook.m
#nm -nm a.out
(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 ,它包含一个内存地址以及 __TEXT 段。也就是说手写的一些函数,在编译之后,其地址并不是未定义的

与之对比的是看到 _printf 这个符号是未定义的(undefined),dyld_stub_binder 会在目标符号(例如 printf)被调用时,将其链接到指定的动态链接库 libSystem,再执行 printf 的实现

每一个镜像中的 DATA 端都包含两个与动态链接有关的表,其中一个是 nl_symbol_ptr,另一个是 __la_symbol_ptr

  • __nl_symbol_ptr 中的 non-lazy 符号是在动态链接库绑定的时候进行加载的
  • __la_symbol_ptr 中的符号会在该符号被第一次调用时,通过 dyld 中的 dyld_stub_binder 过程来进行加载

了解这两点的区别,你也大概可以猜出 fishhook 为什么能替换原C函数了。

PIC(Position-independent code)

为什么 printf 是未定义的?

ASLR技术:是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度。对于我们APP而言,它保证每次MachO文件加载的时候是随机地址 这个我们可以通过LLDB指令的image list去查看

苹果采用了PIC(Position-independent code)技术成功让 C 的底层也能有动态的表现:

  • 编译时在 Mach-O 文件 _DATA 段的符号表中为每一个被引用的系统 C 函数建立一个指针(8字节的数据,放的全是0),这个指针用于动态绑定时重定位到共享库中的函数实现。
  • 在运行时当系统 C 函数被第一次调用时会动态绑定一次,然后将 Mach-O 中的 _DATA 段符号表中对应的指针,指向外部函数(其在共享库中的实际内存地址)。

fishhook 正是利用了 PIC 技术做了这么两个操作:

  • 将指向系统方法(外部函数)的指针重新进行绑定指向内部函数/自定义 C 函数。
  • 将内部函数的指针在动态链接时指向系统方法的地址。

这样就把系统方法与自己定义的方法进行了交换,达到 HOOK 系统 C 函数(共享库中的)的目的。

dyld 加载回调

在 dyld 加载镜像时,会执行注册过的回调函数; 对于每一个已经存在的镜像,当它被动态链接时,都会执行回调

1
void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide)

传入文件的 mach_header 以及一个虚拟内存地址 intptr_t

dyld 通过更新 Mach-O 二进制文件 __DATA 段中的一些指针来绑定 lazy 和 non-lazy 的符号;

而 fishhook 先确定某一个符号在 __DATA 段中的位置,然后保存原符号对应的函数指针,并使用新的函数指针覆盖原有符号的函数指针,实现重绑定。

fishhook 是如何根据字符串找到对应指针在符号表中的偏移值的

直接上fishhook 的 README 中的流程图:

这张图初看很复杂,不过它演示的是寻找符号的过程,我们根据这张图来分析一下这个过程:

  1. 从 __DATA 段中的 lazy 符号指针表中查找某个符号,获得这个符号的偏移量 1061,然后在每一个 section_64 中查找 reserved1,通过这两个值找到 Indirect Symbol Table 中符号对应的条目
  2. 在 Indirect Symbol Table 找到符号表指针以及对应的索引 16343 之后,就需要访问符号表
  3. 然后通过符号表中的偏移量,获取字符串表中的符号 _close

图示中,1061 是间接符号表的偏移量,*(偏移量+间接符号地址)=16343,即符号表偏移量。符号表中每一个结构都是一个 nlist 结构体,其中包含字符表偏移量。通过字符表偏移量最终确定函数指针。

fishhook 就是对间接符号表的偏移量动的手脚,提供一个假的 nlist 结构体,从而达到 hook 的目的。

上面的流程图其实已经显示的很清楚了,这里我们重新来走一遍,一步步找到其在 MachO 文件里对应指针的偏移值,大致步骤如下:

1.在 String Table 中找到该字符串在 Symbols Table -> Symbols 中的位置:

用 0x9832 - 0x9780 = 0xB2

2.在 Symbols Table -> Symbols 中找到Data = 0xB2 的符号,其对应的 offset 值 0x16F 就是该符号在 Dynamic Symbols Table -> Indirect Symbols 表中的 Data 值

3.在 Dynamic Symbols Table -> Indirect Symbols 表中找到 Data 值为 0x16F 的符号,其位于该表中的位置(第一个)就是它在懒加载表中对应的位置。

4.懒加载表中对应位置的 Offset 值就是该指针最终的偏移量: