源码信息

源码地址

fishhook是真正意义上精炼的代码,不管是接口还是总体代码量都十分简单,但是背后的原理以及它所具备的功能却是十分强大的。最早听说fishhook的时候,只知道它是用于hook的一个开源库,以为和Aspect同种类型所以没把它列到开源代码解析的列表中,
后面出于好奇看了下它居然是用于hook C函数,所以赶紧把它补上了,由于涉及底层的内容比较多,如果不是很清楚大家可以看后面推荐的文章,这里只做简单介绍。

那么上面叨叨了那么多,到底什么是fishhook呢,我们看下官方的解释:

fishhook is a very simple library that enables dynamically rebinding symbols in Mach-O binaries running on iOS in the simulator and on device.

上面的意思就是fishhook是一个用于动态修改 Mach-O 二进制文件里面symbols的一个开源库。fishhook 的强大之处在于它可以 hook 系统的静态 C 函数。我们接下来看下它是怎么做到的:

原理介绍

使用情景

我们先来看下fishhook对外暴露的内容,就两大类:一个rebinding结构体,用于封装对应的绑定信息,两个接口进行对C函数进行hook:

struct rebinding {
const char *name; // 目标符号名
void *replacement; // 用于替换的函数地址
void **replaced; // 用来存放原来的地址值
};

FISHHOOK_VISIBILITY
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);

FISHHOOK_VISIBILITY
int rebind_symbols_image(void *header,
intptr_t slide,
struct rebinding rebindings[],
size_t rebindings_nel);

rebind_symbols和rebind_symbols_image都用于符号重绑定,区别在于前者操作的对象是所有镜像,后者操作的对象是某个指定的镜像

我们先来看一个官方的例子:

#import <dlfcn.h>

#import <UIKit/UIKit.h>

#import "AppDelegate.h"
//1. 引入 fishhook.h
#import "fishhook.h"

//2. 声明与原函数签名相同的函数指针:
static int (*orig_close)(int);
static int (*orig_open)(const char *, int, ...);

//3. 实现用于替换的方法
int my_close(int fd) {
printf("Calling real close(%d)\n", fd);
//4.执行被hook的方法
return orig_close(fd);
}

//3. 实现用于替换的方法
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);
//4.执行被hook的方法
return orig_open(path, oflag, mode);
} else {
printf("Calling real open('%s', %d)\n", path, oflag);
//4.执行被hook的方法
return orig_open(path, oflag, mode);
}
}

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

rebind_symbols((struct rebinding[2]){{"close", my_close, (void *)&orig_close}/*初始化rebinding 结构体*/, {"open", my_open, (void *)&orig_open/*初始化rebinding 结构体*/}}, 2 /*rebind个数*/);

// Open our own binary and print out first 4 bytes (which is the same
// for all Mach-O binaries on a given architecture)
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]));
}
}

经过上面替换后代码中的open,close都会替换为my_open,my_close。所以运行这段代码会输出:

Calling real open('/var/mobile/Applications/161DA598-5B83-41F5-8A44-675491AF6A2C/Test.app/Test', 0)
Mach-O Magic Number: feedface
Calling real close(3)

原理介绍

要完全弄懂fishhook需要知道Mach-O,dyld,编译过程,动态链接,静态链接的过程,这里如果展开讲的话会涉及很大的一块内容,所以这个阶段先不展开细讲,后面会开一个底层系列到时候再回过头来详细得看下fishhood的源码。

我们这里只简单介绍下fishhook的大致原理:

我们知道现在很多代码都是由多个模块构成的(比如我们的一个项目都是由多个.m/.h文件对构成,这里的每个.m/.h文件都相当于一个编译模块)这样有利于复用,并且由于这些模块是可以单独编译的,所以如果只是单个模块发生改变,只要单独编译改变的模块,然后在与其他模块进行链接就可以了。而从源代码到可执行文件需要通过对每个模块的源代码进行编译,生成一个个目标文件,然后再通过
链接器将这些中间文件链接起来,形成一个最终的可行性文件Mach O,在单个模块中C函数在编译时就确定了函数指针的地址偏移量(Offset),这个偏移量在编译好的可执行文件中是固定的,而可执行文件每次被重新装载到内存中时被系统分配的起始地址是不断变化的。运行中的静态函数指针地址其实就等于上述 Offset + Mach0 文件在内存中的首地址.
而在链接阶段主要负责符号决议和重定位。之所以需要链接阶段是由于存在模块间依赖,不论是依赖方法还是依赖变量,本质上都是模块间的符号引用。

但是C函数的指针地址是相对固定且不可修改的。所以内部非动态链接库中的C函数fishhook是hook不了的,它只能用于Mach-O外部的函数。对于这些动态链接库是不会被编译到Mach O 文件的,而是在动态链接时才去重新绑定。

苹果采用了PIC(Position-independent code)技术来完成代码的动态加载特性:
编译时在 Mach-O 文件 _DATA 段的符号表中为每一个被引用的系统 C 函数建立一个指针(8字节的数据,放的全是0),这个指针用于动态绑定时重定位到共享库中的函数实现。在运行时当系统 C 函数被第一次调用时会动态绑定一次,然后将 Mach-O 中的 _DATA 段符号表中对应的指针,指向其在共享库中的实际内存地址,也就是会将我们需要替换的C函数在这个共享库中实际的内存地址保存到 Mach-O 中的 _DATA 段符号表中。
经过上面的一串操作我们在调用这个方法的时候都会读取_DATA中的这个地址。我们上面提到内部非动态链接库中的代码fishhook不能hook是因为这些地址值是存放在TEXT session中,而这部分数据是只读的。而动态链接库里面的最终符号地址存在可读写的 __DATA segment 的某个 section 中,fishhook 的实现原理就是通过修改这些 section 内容,进而实现符号的 rebind。

嗯,大致就是这样,讲得太细节容易把自己都陷进去,这样刚好能够理解。最近正好正在看底层的原理,打算放在后面整体梳理清楚后,再补一篇。赶紧逃。^ V ^

较好的文章

Contents
  1. 1. 源码信息
  2. 2. 原理介绍
  3. 3. 较好的文章