fishhook简介
首先 fishhook 是 facebook 开源的动态重新绑定iOS上运行的Mach-O二进制文件符号表工具,它的强大之处在于它可以 HOOK 系统静态 C 函数(非用户自定义C函数)。
官方英文介绍:
A library that enables dynamically rebinding symbols in Mach-O binaries running on iOS.
fishhook官方github:fishhook
本文Demo:FishhookTest
fishhook的工作原理
在讲解fishhook的原理之前先了解以下知识点:
- Mach-O结构
- 懒加载符号表
- 符号绑定
- 共享缓冲区
- App在编译阶段、链接阶段 都做了什么? 阅读过Mach-O文件结构的读者应该了解,在编译阶段,在工程中使用到的系统库是不会编译进Mach-O中的(这会增大Mach-O的体积),仅仅将依赖的系统库记录在Load Commands段中的LC_LOAD_DYLIB,如图:
工程中使用到的系统库函数(如NSLog函数)记录在该_DATA段的懒加载符号表 Lazy Symbol Pointers 中,如图:
所谓符号绑定,以 Foundation 库中的NSLog为例:
启动APP,dyld 读取Mach-O文件,将 Load Commands 中列出的系统库 Foundation 等加载进共享缓冲区中,dyld 会去找到 Foundation 中 NSLog 的真实地址写到 _DATA 段的符号表中 NSLog 的符号上面,此为符号绑定;
详细流程如下 :
-
在工程编译时,所产生的 Mach-O 可执行文件中会预留出一段空间 , 这个空间其实就是符号表,存放在 _DATA 数据段中(因为 _DATA 段在运行时是可读可写的)
-
编译时: 工程中所有引用了共享缓存区中的系统库方法,其指向的地址设置成符号地址,(例如工程中有一个 NSLog,那么编译时就会在 Mach-O 中创建一个 NSLog 的符号,工程中的 NSLog 就指向这个符号)
-
运行时: 当 dyld将应用加载到内存中时 , 根据 load commands 中列出的需要加载哪些库文件 , 去做绑定的操作(以 NSLog 为例 , dyld 就会去找到 Foundation 中 NSLog 的真实地址写到 _DATA 段的符号表中 NSLog 的符号上面)
这个过程被称为 PIC 技术(Position Independent Code : 位置代码独立)
而 fishhook 的原理,就是通过更新 Mach-O 二进制文件中的_DATA段符号表,目的是将符号上的地址重新写为用户自己的函数地址,这样在调用系统函数时就会调用户自己的函数了;
重绑定符号地址用的就是fishhook中的 rebind_symbols函数;
讲完了fishhook的工作原理,下面来实际写个demo;
fishhook的基本使用
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
//rebinding结构体
struct rebinding nslog;
//需要HOOK的函数名称,C字符串
nslog.name = "NSLog";
//将系统库函数所指向的符号,在运行时重新绑定到用户指定的函数地址
nslog.replacement = myNslog;
//将原系统库函数的真实地址赋值到用户指定的函数指针上
nslog.replaced = (void*)&sys_nslog;
//rebinding结构体数组
struct rebinding rebs[1] = {nslog};
/**
* 参数1 : 存放rebinding结构体的数组
* 参数2 : 数组的长度
*/
rebind_symbols(rebs,1);
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"点击了屏幕");
}
//用户指定的函数指针
void(*sys_nslog)(NSString *format,...);
//新的nslog函数
void myNslog(NSString *format,...) {
format = [format stringByAppendingString:@"勾上了"];
//调用一下原系统函数,不然不会打印
sys_nslog(format);
}
输出:点击了屏幕...勾上了
重绑定的流程如下:
1.声明rebinding结构体;
2.需要HOOK的函数名称;
3.将系统库函数所指向的符号,在运行时重新绑定到用户指定的函数地址;
4.将原系统函数的真实地址赋值到用户指定的指针上;
5.声明rebinding结构体数组,其中的数组元素就是上述声明的rebinding结构体;
6.调用重绑定函数 rebind_symbols;
实操验证
在上述原理中讲到fishhook是通过重绑定懒加载符号表的符号地址的方式来实现hook系统库函数的,下面来验证一下:
首先为什么称之为“懒加载”?
注:
1.对于非懒加载符号表,dyld 会在动态链接时就链接动态库
2.对于懒加载符号表,dyld 会在运行时函数第一次被调用时动态绑定一次
NSLog 在懒加载表中可以理解为在第一次调用库函数时才会去将真实的函数地址绑定到具体符号上,这里还是以NSLog函数为例,在上述代码中新增NSLog方法调用,也就是去执行一次符号绑定操作;
代码准备
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSLog(@"hello");
//rebinding结构体
struct rebinding nslog;
...
}
设置断点,进入LLDB调试模式
工程中总共设置三个断点,分别为:
- NSLog函数输出之前
- NSLog函数输出之后
- 点击屏幕函数
首先运行工程,来到第一个断点处,这时还没有调用NSLog函数,所以 dyld 还没有执行符号绑定操作,符号上的地址应该为空:
LLDB查看Mach-O内存地址
执行 image list 获取Mach-O的内存地址:0x0000000106669000
查看符号NSLog的偏移量
NSLog符号的偏移量为 8000
根据符号偏移量和Mach-O起始地址计算符号的地址
打开计算器切换到16进制计算模式,计算可得:
0x0000000106669000 + 8000 = 0x106671000
0x106671000 就是NSLog符号所在内存地址;
查看内存和汇编代码
查看该内存地址0x106671000的内容,并查看其汇编代码
执行 x + 0x106671000
查看前8个字节的内容
注意:iOS是小端模式,所以从右往左读,图上对应的字节就是00 00 00 01 06 66 b3 90,去掉00也就是010666b390
查看汇编内容,执行 dis -s 0x010666b390
汇编代码看不懂没关系,先别管它;
理论上 dyld 没有执行符号绑定,该内存空间中是不会存在任何有用的内容;
这是还没有执行NSLog函数之前的符号内容,下面过掉断点,它会NSLog函数;
来到第二个断点:
然后再去查看NSLog符号的内容:
很明显内容已经变成7fff207ffd0d,下面在查看汇编代码:
执行 dis -s 0x7fff207ffd0d
汇编代码先不要管,可以看到左上角已经显示 Foundation NSLog:,说明已经将系统库函数NSLog的真实内存地址绑定到了符号NSLog上了,现在看到就是 Foundation 中的 NSLog 函数的汇编代码,验证了“懒加载的说法”;
下面继续过掉断点,让代码去执行fishhook的rebinding操作
点击屏幕来到第三个断点
重复以上查看符号内存地址的操作
第三次查看,符号内容又变了,变成 010666a420
查看汇编内容 dis -s 0x010666a420
可以看到FishhookTest myNslog:,说明符号已经rebinding到用户自己的函数上;
综上:已经成功验证了fishhook的工作原理~
题外话
fishhook 只能 hook 系统库函数,不能 hook 用户自定义的C函数,这是因为 fishhook 的 hook 原理依赖于符号表的重绑定,而用户自定义的C函数没有存在于符号表中,所以不满足 hook 的要求!