一、Fishhook 是什么?
简单来说Fishhook就是hook函数的一种工具,当然它hook的原理和我们熟知的Method Swizzle 方式是不一样的,它是Facebook提供的一个动态修改链接mach-O文件的工具。
- Method Swizzle 是利用OC的Runtime特性,动态改变SEL(方法编号)和IMP(方法实现)的对应关系,达到OC方法调用流程改变的目的。主要用于OC方法。
- Fishhook 是利用MachO文件加载原理,通过修改懒加载和非懒加载两个表的指针达到C函数HOOK的目的。
二、Fishhook的简单使用
首先在github上下载fishhook库:
解压zip包后,将其中的.c和.h文件拖入你想hook的项目当中,我们所用到的就是这两个文件,当然网上有大把的关于fishhook的源码剖析资料,大家可以自行查阅,这里我就直接上代码了,拖入步骤不作详细介绍。
#import "ViewController.h"
#import "fishhook.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//定义rebinding 结构体
struct rebinding rebind = {};
rebind.name = "NSLog";
rebind.replacement = hookNSLog;
rebind.replaced = (void *)&nslogMethod;
//将上面的结构体 放入 reb结构体数组中
struct rebinding red[] = {rebind};
/*
* arg1 : 结构体数据组
* arg2 : 数组的长度
*/
rebind_symbols(red, 1);
}
//定义一个函数指针 用于指向原来的NSLog函数
static void (*nslogMethod)(NSString *format, ...);
void hookNSLog(NSString *format, ...){
format = [format stringByAppendingString:@"被勾住了"];
nslogMethod(format);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"原有NSLog函数");
}
将文件导入项目后,在ViewController.m 文件中写入代码如上,代码目的是:将点击屏幕时执行的NSLog函数,替换成执行hookNSLog函数。
点击屏幕时执行结果:
我们先不管原理是怎样,大家是不是觉得,这执行结果和以上代码干的事,是不是很像runtime的方法交换。
但是从表面上看,runtime和fishhook的区别,大家都知道使用runtime交换的是OC的方法,而fishhook却是交换C函数,这里就会有个疑惑,C语言不是静态语言吗,为什么fishhook可以直接交换函数呢?
ok ! 在疑问之前,我们先将以上代码稍作修改,在看一下结果。
#import "ViewController.h"
#import "fishhook.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//定义rebinding 结构体
struct rebinding rebind = {};
rebind.name = "funcDlog";
rebind.replacement = hookNSLog;
rebind.replaced = (void *)&nslogMethod;
//将上面的结构体 放入 reb结构体数组中
struct rebinding red[] = {rebind};
/*
* arg1 : 结构体数据组
* arg2 : 数组的长度
*/
rebind_symbols(red, 1);
}
//定义一个函数指针 用于指向原来的NSLog函数
static void (*nslogMethod)(NSString *format, ...);
void hookNSLog(NSString *format, ...){
format = [format stringByAppendingString:@"被勾住了"];
nslogMethod(format);
}
void funcDlog(NSString *format, ...){
NSLog(@"%@", format);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
funcDlog(@"原有NSLog函数");
}
@end
来我们看下代码的改动:
我们将原来hook的目标转换了,自己定义了一个函数void funcDlog(NSString *format, ...)
,在函数调用NSLog函数,而我们这次Hook的目标变成了 funcDlog函数,按照之前的运行结果,猜测这次的运行结果应该还是“ 原有NSLog函数被勾住了”,但是实际运行结果却是:
结果显示我们的代码并没有hook成功,综合以上两次运行结果,我们发现了,fishhook可以实现runtime做不到的将C函数进行交换,但是我们自己定义的C函数却不能交换,只能hook系统的函数,这是为什么呢?我们来看下一个章节。
三、Fishhook的原理探究
首先在探究fishhook的原理之前,我要清楚几个问题:
- MachO是怎么加载的
- 首先我们要知道,MachO文件是被dyld加载的,dyld是动态加载,也负责动态库的加载。我们都知道动态库加载时并不在我们自己的MachO文件内部,是存在系统动态缓存区,当我们MachO文件加载过后会动态加载我们所依赖的那些动态库。
- ASLR技术
- 我们的MachO文件在内存当中每次的运行地址是不一样的,MachO文件加载的时地址是随机的,这就是ASLR技术。苹果每次加载MachO文件时会随机分配一个地址,来降低缓冲区溢出。
那问题来了,每次MachO文件加载的地址不一样,我们大家都知道,系统的动态库缓存区的地址也是变化的,那每次dyld加载动态库的时候是怎样找到依赖动态库进行加载的呢?
-
PIC 位置代码独立
- 当MachO内部需要调用系统的库函数时,会现在MachO文件_DATA段中建立一个指针,指向外部函数,dyld就会将指针与函数进行动态绑定,将MachO中的DATA段中的指针指向外部函数,从而实现加载。
我们来看下第一份代码的MachO文件,如图:
当MachO内部有两个指针用来加载动态库:
- Non-Lazy Symbol Pointers :不懒加载指针
- Lazy Symbol Pointers :懒加载指针
这两个指针就是dyld用来在MachO内部调用系统库函数时来指向外部函数进行动态绑定加载。从上图我们可以看出在Lazy Symbol Pointers
指针表中NSLog的位置,以及偏移地址是0x8018。
那我们根据这个偏移地址在第一份代码中进行LLDB调试看看结果如何:
-
首先我们先在 fishhook的代码执行前设置断点:
-
然后我们先拿到MachO文件在内存中的首地址:0x0000000100154000
-
根据偏移地址拿到指针指向的地址值:
-
根据地址值中的地址进行反汇编,我看下结果:
反汇编后结果清楚显示地址指向的是Foundation下的 NSLog
函数
ok! 这是还没有进行fishhook前的NSLog ,那我们将断点断到fishhook代码执行后在看结果:
同样的偏移地址,地址值却发生了变化,反汇编后却发现,地址指向的是fishhookDemo1下的hookNSLog
函数。
这就是Fishhook的原理:在dyld加载MachO文件所需要的系统函数时,通过改变MachO文件中的DATA段中指向外部函数的指针所指向的地址,来实现动态交换。
这里就可以解释我们在两次运行结果对比留下的两个疑惑:
- 为什么fishhook可以交换C函数
- fishhook 是通过改变dyld加载动态库所用的指针指向的函数地址,来实现的函数交换 ,所以C函数通过dyld动态加载动态库也展现出C函数动态的一面。
- 为什么我们自己写的C函数fishhook交换不了
- 我们自己写的C函数不在系统动态库缓存区,而是存在我们自己的MachO文件当中,不经过dyld加载,直接编译成汇编语言,在MachO中的TEXT段中。所以fishhook的原理不能交换我们自己写的C函数。
四、通过符号找到字符串
在上面我们知道了系统的动态库与我们的MachO文件是通过dyld在加载时在MachO文件中的一个指针指向了外部函数来进行加载,但是这只是个地址,我们在代码中调用函数时通过函数名称来调用的,那这个地址和我们调用的函数名称是如何建立起联系的呢?我们先来看下刚才的MachO文件,如图:
图1:
图2:
从图1、图2中我们可以看出Lazy Symbol Pointers 表 和 Dyamic Symbol Table 表中的关系是一一对应的,遍历前一张表的index就可以对应到后一张表。注意图2中的对应index行中的 Data 值为0x7F。
图3:
在Symbol Table 表中先拿到第一条的偏移地址0xC500。
图4:
在图2中index中获取的Data值为0x7F,这值是图4中index的标号,由于图4中index的偏移为0x10,所以偏移地址+Data对应的偏移地址为0xCCF0,找到当前index的Data值是0xA7。
图5:
最后在String Table 表中 偏移首地址+0xA7 的值为0x0000D02B,在表中差得字符串以“_”开头,以“.”结尾。所得的字符串就是外部函数地址所对应的函数名。