@qidiandasheng
2022-08-29T01:15:27.000000Z
字数 11089
阅读 2500
iOS运行时
fishhook 是 FaceBook 开源的可以动态修改 MachO 符号表的工具。fishhook 的强大之处在于它可以 HOOK 系统的静态 C 函数。
我们在链接器:符号是怎么绑定到地址上的?中有讲到可执行文件调用动态库的方法时,符号是在第一次动态库加载或者调用的时候绑定的。
苹果采用了PIC(Position-independent code)技术成功让 C 的底层也能有动态的表现:
fishhook 正是利用了 PIC 技术做了这么两个操作:
这样就把系统方法与自己定义的方法进行了交换,达到 HOOK 系统 C 函数(共享库中的)的目的。
fishhook可以用来Hook C函数,但他也可以用来防止HOOK OC函数,基本思路如下:
#import "NSObject+fishHook.h"#import <objc/runtime.h>#import "fishhook.h"@implementation NSObject (fishHooklog)+ (void)load{// rebinding结构体struct rebinding ex;// 要hook的方法ex.name = "method_exchangeImplementations";// 函数指针指向我们新实现的方法ex.replacement = myExchange;// 指向指针的指针(&exchangeP:函数指针的地址)ex.replaced = (void *)&exchangeP;// rebinding结构体数组struct rebinding rebs[1] = {ex};rebind_symbols(rebs, 1);}// exchangeP:函数指针void(*exchangeP)(Method _Nonnull m1, Method _Nonnull m2);// Hook的方法void myExchange(Method _Nonnull m1, Method _Nonnull m2){SEL methodName = method_getName(m1);NSString *log = [NSString stringWithFormat:@"❗️发现了非法操作有人想要交换方法:%@",NSStringFromSelector(methodName)];NSLog(@"%@", log);//调用原始的方法exchangeP(m1,m2);}@end
这里利用fishhook主要hook了method_exchangeImplementations函数,在我们自定义的方法里输出被hook的方法。
我看可以设置一个白名单,在白名单里的函数允许被hook,即调用exchangeP(m1,m2);
以下代码hook失败:
- (void)viewDidLoad {[super viewDidLoad];self.view.backgroundColor = [UIColor whiteColor];struct rebinding rb;rb.name = "printHelloWorld";rb.replacement = hook_printHelloWorld;rb.replaced = (void *)&app_printHelloWorld;struct rebinding rbs[] = {rb};rebind_symbols(rbs, 1);}void printHelloWorld() {NSLog(@"Hello World!");}static void (*app_printHelloWorld)(void);void hook_printHelloWorld() {NSLog(@"hook成功");// 调用原始的printHelloWorld函数app_printHelloWorld();}
原因:
系统定义的C函数,由于具体的函数实现是在系统共享库中,因此在程序编译期间是无法获取到这个C函数的实现地址,只能通过一种叫符号绑定的方法动态链接到函数名。
NSLog当我们编译的时候,NSLog函数实现的地址是undefinedFoundation库加载到内存中NSLog函数调用时,就会到Foundation库的MachO文件中查询NSLog函数的实现地址。然后将函数的实现地址和符号表中的NSLog字符串进行绑定自定义的C函数,由于函数的实现和函数的调用是在同一个MachO文件(App本身的MachO文件)中,因此在编译链接期间,xcode就直接将函数调用语句和函数的实现地址进行了链接,也就不会有系统C函数的那些步骤了。
// rebinding结构体struct rebinding ex;// 要hook的方法ex.name = "method_exchangeImplementations";// 函数指针指向我们新实现的方法ex.replacement = myExchange;// 指向指针的指针(&exchangeP:函数指针的地址)ex.replaced = (void *)&exchangeP;// rebinding结构体数组struct rebinding rebs[1] = {ex};rebind_symbols(rebs, 1);
rebinding 类型的结构体变量,其源码如下:
struct rebinding {const char *name;void *replacement;void **replaced;};
void **replaced 是指向指针的指针,可以理解为一个存着另一个指针地址的指针,在上述示例中, *replaced 取出的就是一个指向共享库中 method_exchangeImplementations 函数实现的指针,再对其取值,**replaced 得到的就是共享库中 method_exchangeImplementations 函数实现的首地址。
// exchangeP:函数指针void(*exchangeP)(Method _Nonnull m1, Method _Nonnull m2);// Hook的函数void myExchange(Method _Nonnull m1, Method _Nonnull m2){SEL methodName = method_getName(m1);NSString *log = [NSString stringWithFormat:@"❗️发现了非法操作有人想要交换方法:%@",NSStringFromSelector(methodName)];NSLog(@"%@", log);//调用原始的方法exchangeP(m1,m2);}
把上面声明好的结构体变量放入数组中,调用rebind_symbols开始重绑定符号(如果绑定成功返回 0,否则返回 -1)。传入参数为结构体数组和数组长度。
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);if (retval < 0) {return retval;}// 根据_rebindings_head->next是否为空判断是不是第一次调用if (!_rebindings_head->next) {// 第一次调用的话,调用_dyld_register_func_for_add_image注册监听方法// 已经被dyld加载过的image会立刻进入回调,之后的image会在dyld装载的时候触发回调,回调方法是_rebind_symbols_for_image_dyld_register_func_for_add_image(_rebind_symbols_for_image);} else {// 遍历已经加载的image,找到所有目标函数逐一进行hookuint32_t c = _dyld_image_count();for (uint32_t i = 0; i < c; i++) {_rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));}}return retval;}
把需要绑定的数据信息放入链表中备用。
static int prepend_rebindings(struct rebindings_entry **rebindings_head,struct rebinding rebindings[],size_t nel) {struct rebindings_entry *new_entry = (struct rebindings_entry *) malloc(sizeof(struct rebindings_entry));if (!new_entry) {return -1;}new_entry->rebindings = (struct rebinding *) malloc(sizeof(struct rebinding) * nel);if (!new_entry->rebindings) {free(new_entry);return -1;}memcpy(new_entry->rebindings, rebindings, sizeof(struct rebinding) * nel);new_entry->rebindings_nel = nel;new_entry->next = *rebindings_head;*rebindings_head = new_entry;return 0;}
_rebindings_head 被声明为一个指向 rebindings_entry 类型结构体的静态指针变量,那 &_rebindings_head 就是取出这个指针的地址,再看该函数的参数声明 struct rebindings_entry ** ,没错这又是一个指向指针的指针。
struct rebindings_entry {struct rebinding *rebindings;size_t rebindings_nel;struct rebindings_entry *next;};
结构体 rebindings_entry 的三个成员分别是:
rebindings:指向 rebinding 类型结构体的指针(用来指向传入结构体数组的首元素地址)、rebindings_nel:记录此次要重绑定的数量(用于开辟对应大小的空间)、next:指向下一个 rebindings_entry 类型的结构体(记录下一次需要重绑定的数据)这就是典型的数据结构——链表的一种实现。_rebindings_head 就是指向该链表的指针。
最后形成的链表如下图所示,调用的顺序为rebind_symbols({rebinding4},1)、rebind_symbols({rebinding3},1)、rebind_symbols({rebinding1,rebinding2},2):

一句话总结 prepend_rebindings 函数的目的:将新加入的 rebindings 数组不断的添加到 _rebindings_head 这个链表的头部成为新的头节点。
fishhook 的代码执行时间非常早,所以第一次执行时要 hook 的库可能还没完成装载,因此这里如果是第一次调用会通过一个函数对库的装载完成注册监听和回调的方法:
_dyld_register_func_for_add_image(_rebind_symbols_for_image);
已经被dyld加载过的image会立刻进入回调,之后的image会在dyld装载的时候触发回调,回调方法是_rebind_symbols_for_image。
第二次调用的时候镜像文件已经全部装载完成了,所以调用一下方法遍历即可:
// 遍历已经加载的image,找到所有目标函数逐一进行hookuint32_t c = _dyld_image_count();for (uint32_t i = 0; i < c; i++) {_rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));}
当回调到 _rebind_symbols_for_image 时,会将存着待绑定函数信息的链表作为参数传入,用于符号查找和函数指针的交换,第二个参数 header 是当前 image 的头信息,第三个参数 slide 是 ASLR 的偏移:
static void _rebind_symbols_for_image(const struct mach_header *header,intptr_t slide) {rebind_symbols_for_image(_rebindings_head, header, slide);}
主要通过image里的Load Commands来查找链接时程序的基址、符号表地址、字符串表地址、动态符号表地址、懒加载表、非懒加载表。
查找链接时程序的基址

查找符号表地址、字符串表地址

查找动态符号表地址

查找懒加载表、非懒加载表

static void rebind_symbols_for_image(struct rebindings_entry *rebindings,const struct mach_header *header,intptr_t slide) {Dl_info info;// 这个dladdr杉树就是在程序里面找headerif (dladdr(header, &info) == 0) {return;}// 定义好几个变量,然后从MachO里面去找并一一赋值segment_command_t *cur_seg_cmd;segment_command_t *linkedit_segment = NULL;struct symtab_command* symtab_cmd = NULL;struct dysymtab_command* dysymtab_cmd = NULL;// 跳过header的大小,找loadCommanduintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);// header->ncmds:loadCommand的数量for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {// 当前的loadCommandcur_seg_cmd = (segment_command_t *)cur;if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {linkedit_segment = cur_seg_cmd;}} else if (cur_seg_cmd->cmd == LC_SYMTAB) {symtab_cmd = (struct symtab_command*)cur_seg_cmd;} else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;}}// 如果刚出获取的有一项为空就直接返回if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||!dysymtab_cmd->nindirectsyms) {return;}// Find base symbol/string table addresses// 链接时程序的基址 = __LINKEDIT.VM_Address - __LINKEDIT.File_Offset + slide的改变值uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;// 符号表的地址 = 基址 + 符号表偏移量nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);// 字符串表地址 = 基址 + 字符串表偏移量char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);// Get indirect symbol table (array of uint32_t indices into symbol table)// 动态符号表地址 = 基址 + 动态符号表偏移量uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);// 跳过header的大小,找loadCommandcur = (uintptr_t)header + sizeof(mach_header_t);// header->ncmds:loadCommand的数量for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {cur_seg_cmd = (segment_command_t *)cur;if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {// 寻找data段if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {continue;}// cur_seg_cmd->nsects:在segment里的section的数量for (uint j = 0; j < cur_seg_cmd->nsects; j++) {section_t *sect =(section_t *)(cur + sizeof(segment_command_t)) + j;// 找懒加载表if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);}// 找非懒加载表if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);}}}}}

这张图主要在描述如何由一个字符串(比如 "NSLog"),跟着它在 MachO 文件的懒加载表中对应的指针,一步步的找到该指针指向的函数实现地址,大致步骤如下:
得到Indirect Symbols
Indirect Symbols在indirect symbol table中的地址 = 懒加载表或非懒加载表的reserved1 + 动态符号表基址。
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;

找到符号在Symbol Table表中的索引
遍历section里的每一个符号,在indirect symbol table->Indirect Symbols找到符号在Symbol Table表中的索引,下图所示NSLog是第一个,Symbol Table表中的索引为0xA=10。
uint32_t symtab_index = indirect_symbol_indices[i];

找到符号在String Table中的偏移
从第二步中得到的索引10在Symbol Table中查找,从上往下数第11个,如下图所示,索引10的位置对应的 Data = 0x44。
//strtab_offset就是0x44uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;

在String Table中找到符号地址
String Table的起始地址(0x3300)+ 第三步中的 Data(0x44)= 就是符号的地址(0x3344),可以得到对应的符号名:5F 4E 53 4C 6F 67,对应ASCII就是_NSLog。
char *symbol_name = strtab + strtab_offset;

通过符号名相等确定函数地址
遍历section里的每一个符号,通过以上步骤得到符号名,判断符号名跟我们需要hook的函数符号名相等,得到函数实现对应的索引,然后就可以开始Hook替换函数地址为自定义函数地址了。以下为主要函数替换部分的代码:
// slide+section->addr 就是符号对应的存放函数实现的数组,用来寻找到函数的地址void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);// 让rebindings[j].replaced保存indirect_symbol_bindings[i]的函数地址*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];// 将替换后的方法给原先的方法,也就是替换内容为自定义函数地址indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
完整实现代码:
static void perform_rebinding_with_section(struct rebindings_entry *rebindings,section_t *section,intptr_t slide,nlist_t *symtab,char *strtab,uint32_t *indirect_symtab) {// 懒加载表或非懒加载表的reserved1 + 动态符号表基址 = Indirect Symbols在indirect symbol table中的地址uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;// slide+section->addr 就是符号对应的存放函数实现的数组,用来寻找到函数的地址void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);// 遍历section里的每一个符号for (uint i = 0; i < section->size / sizeof(void *); i++) {// 找到符号在Symbol Table表中的索引uint32_t symtab_index = indirect_symbol_indices[i];if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) {continue;}// 以symtab_index作为下标,访问symbol table中对应的符号信息uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;// 获取symbol_namechar *symbol_name = strtab + strtab_offset;// 判断是否函数的名称有两个字符,为啥是两个,因为函数前面有个_,所以方法的名称最少要1个if (strnlen(symbol_name, 2) < 2) {continue;}// 遍历最初的链表,逐一进行hookstruct rebindings_entry *cur = rebindings;while (cur) {for (uint j = 0; j < cur->rebindings_nel; j++) {// 判断symbol_name[1]与rebindings中对应的函数名是否相等,相等即为目标hook函数if (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;}symbol_loop:;}}