在OC中想在hook一个函数,绝对是利用runtime swizzle 方法。但是对于c函数我们应该怎么hook呢?
没错,就是利用今天的主角fishhook
使用方法
我们以 hook NSlog()
为例
先定义我们自己的 log
函数 与 NSLog
的函数指针
1 | void mLog(NSString * format, ...) { |
声明 rebinding
的结构体。这个结构体包含了重新绑定的信息。1
2
3
4
5
6
7
8struct rebinding nslog;
nslog.name = "NSLog";
nslog.replacement = mLog;
nslog.replaced = (void *)&sys_nslog;
//rebinding结构体数组
struct rebinding rebs[1] = {nslog};
rebind_symbols(rebs, 1);
最终我们调用 NSLog
方法,打印如下:
xxx 勾上了!
此处有一个小问题?如果在这个项目里面的 c 函数能hook住吗?答案是不能!
原理解释
新建一个项目,在ViewDidload
里面打印文本,把生成的可执行文件拖到 Hopper Disassembler 中。
这里跳到 imp__stubs_NSLog
这个位置,进来继续看
上面是把 0x0c00
位置的值放到x16寄存器上,我们看一下地址
找到 0x0C000
的地址,这里是 Data Segment
,__la_symbol_ptr Section
。这里有两个点很重要:
- 属于
Data
段,可读写 __la_symbol_ptr section
是存放着动态链接的符号表,这里面的符号会在该符号被调用时,通过dyld的dyld_stb_binder
过程进行加载。
还有一种动态链接相关的表是__nl_symbol_ptr
表示在动态链接库绑定的时候进行加载的
到这里就很显示了,fishhook 就是通过修改 __la_symbol_ptr
这张符号表里面的值来达到hook c函数的目的。
下面我们来验证一下
原理验证
在一个新项目的ViewController
里面写上面代码,然后我们关注 __nl_symbol_ptr
这个符号表里面NSLog
对应地址的变化。把可执行文件拖到 MachOView
里面
位置是这个0xc000
,运行程序,进入断点。找到ALSR
的偏移地址
lldb x
命令指地址的值读出来,dis -s
以当前赋值的地址反汇编。
可以看出来,我们NSLog
调用的地址,暂时看不出来是什么(实际是调了dyld_stb_binder
相关函数,进行动态绑定)。
接下来,我们继续走下一步,NSLog动态符号表对应的值 发生改变,对当前地址反汇编后发现是 Foundation
的NSLog
方法。
最后,让代码继续走过rebind_symbols
方法,又又发现NSLog
动态符号表对应的值 发生改变,对当前地址反汇编后发现是我们自定义的方法
结论很明显,__nl_symbol_ptr
处于Data段,可读写。动态符号可通过这里对应的地址值进行跳转,fishhook
就是通过修改这里的值达到Hook目的。
fishhook源码解读
拿Mach-O
文件和源码的操作一起对着来看。
rebind_symbols 方法
1 | int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) { |
第一步:preped_rebindings,这部分主要是把 rebindings_entry 以一个链表的形式保存起来。
1 | static int prepend_rebindings(struct rebindings_entry **rebindings_head, |
从上面看到,入口函数最终会调到_rebind_symbols_for_image,从页调到rebind_symbols_for_image这个函数里面。1
2
3
4static 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)函数里面,
1 | /// |
这里可以分三部分来讲解:
第一部分:确定这个header是否合法
dladdr函数主要作用是获取 &info 所在动态库的信息,如果获取不到,就证明这个header地址是不合法的。
1 | Dl_info info; |
第二部分:可以看到在查找三个 load_command
分别是
linkedit_segment、symtab_cmd、dysymtab_cmd,
1 | // loadCommand的开始地址在header的后面 |
分析一下这三个LoadCommand主要会加载哪些信息
LINKEDIT Commond
根据地址可以看到,linkedit_segment 这个命令是用于加载 Dynamic Loader Info 相关的信息。
LC_SYMTAB(符号表)
LC_DYSYMTAB(动态符号表相关)
我们能看到这个 IndSym Table Offset
的值是 0x11648
,其他的符号表的offset 是0。
IndSym Table Offset
用于存放在字符表的位置
通过上面的分析,我们搞明白了上面这几个load_command的作用了。那么回过头业,继续往下看第三部分
第三部分找到各个 符号表地址、字符串表地址、动态符号表地址,然后调用重新绑定函数的方法进行替换
1 | // Find base symbol/string table addresses |
至于为什么叫 got
段,估计是和 ELF 文件有关系
找到了这 __la_symbol_prt
Section
的位置后,就进入了查找符号位置,替换方法的逻辑。
1 | /// 找到符号表所对应的位置进行绑定 |
例子
下面以NSLog
为例,一步一步使用 Mach-O
对应的把流程走完看看,(NSLog
在 __la_symbol_ptr
符号表里面)
1 | uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1; // 获取 __la_symbol_ptr 符号在 indSymBol的开始位置。 |
下面这图可以看到在Indirect Symbols
偏移是 0x19,也就是第25个。
走到这步, 其实就是 __la_symbol_ptr
的位置1
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
进入for 循环第一句,获取在 symtab_index,在 0xE3 这个位置
uint32_t symtab_index = indirect_symbol_indices[i];
根据在 符号表里面的Index(0xE3 = 227),取到符号对象,并获取在字符串表 strtab的位置。1
2//以symtab_index作为下标,访问symbol table
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
所以我们取到的字符的位置 0x11708 + 0xD4 = 0x117DC
然后判断symbol是否大于一,因为前面的 “_”字符是编译期加上的。1
bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1];
进入while循环,把rebindings都过一遍,看看和上面字符相等的函数名,进行方法替换。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18while (cur) {
for (uint j = 0; j < cur->rebindings_nel; j++) {
//这里if的条件就是判断从symbol_name[1]两个函数的名字是否都是一致的,以及判断两个
if (symbol_name_longer_than_1 &&
strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
//判断replaced的地址不为NULL以及我方法的实现和rebindings[j].replacement的方法不一致
if (cur->rebindings[j].replaced != NULL &&
indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
//让rebindings[j].replaced保存indirect_symbol_bindings[i]的函数地址
*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
}
//将替换后的方法给原先的方法,也就是替换内容为自定义函数地址
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
goto symbol_loop;
}
}
cur = cur->next;
}
我们回顾一下上面查找符号的过程:
- 从
Loadcomamd
的reserved1
的位置,找到在Indirect Symbol Table
的开始位置。 - 进入循环,从开始位置遍历
Indirect Symbol Table
,长度是Section.size
- 遍历过程中,拿到
Symbol Table
的位置,从而知道符号的字符串 - 遍历过程中, 字符串和被替换相匹配,如果匹配就进行替换函数。