[关闭]
@qidiandasheng 2022-08-29T01:15:27.000000Z 字数 11089 阅读 1640

fishhook的实现原理(😁)

iOS运行时


介绍

fishhook 是 FaceBook 开源的可以动态修改 MachO 符号表的工具。fishhook 的强大之处在于它可以 HOOK 系统的静态 C 函数。

我们在链接器:符号是怎么绑定到地址上的?中有讲到可执行文件调用动态库的方法时,符号是在第一次动态库加载或者调用的时候绑定的。

苹果采用了PIC(Position-independent code)技术成功让 C 的底层也能有动态的表现:

fishhook 正是利用了 PIC 技术做了这么两个操作:

这样就把系统方法与自己定义的方法进行了交换,达到 HOOK 系统 C 函数(共享库中的)的目的。

fishhook 使用场景

fishhook可以用来Hook C函数,但他也可以用来防止HOOK OC函数,基本思路如下:

  1. #import "NSObject+fishHook.h"
  2. #import <objc/runtime.h>
  3. #import "fishhook.h"
  4. @implementation NSObject (fishHooklog)
  5. + (void)load{
  6. // rebinding结构体
  7. struct rebinding ex;
  8. // 要hook的方法
  9. ex.name = "method_exchangeImplementations";
  10. // 函数指针指向我们新实现的方法
  11. ex.replacement = myExchange;
  12. // 指向指针的指针(&exchangeP:函数指针的地址)
  13. ex.replaced = (void *)&exchangeP;
  14. // rebinding结构体数组
  15. struct rebinding rebs[1] = {ex};
  16. rebind_symbols(rebs, 1);
  17. }
  18. // exchangeP:函数指针
  19. void(*exchangeP)(Method _Nonnull m1, Method _Nonnull m2);
  20. // Hook的方法
  21. void myExchange(Method _Nonnull m1, Method _Nonnull m2){
  22. SEL methodName = method_getName(m1);
  23. NSString *log = [NSString stringWithFormat:@"❗️发现了非法操作有人想要交换方法:%@",NSStringFromSelector(methodName)];
  24. NSLog(@"%@", log);
  25. //调用原始的方法
  26. exchangeP(m1,m2);
  27. }
  28. @end

这里利用fishhook主要hook了method_exchangeImplementations函数,在我们自定义的方法里输出被hook的方法。

我看可以设置一个白名单,在白名单里的函数允许被hook,即调用exchangeP(m1,m2);

fishhook为什么不能hook自定义C函数

以下代码hook失败:

  1. - (void)viewDidLoad {
  2. [super viewDidLoad];
  3. self.view.backgroundColor = [UIColor whiteColor];
  4. struct rebinding rb;
  5. rb.name = "printHelloWorld";
  6. rb.replacement = hook_printHelloWorld;
  7. rb.replaced = (void *)&app_printHelloWorld;
  8. struct rebinding rbs[] = {rb};
  9. rebind_symbols(rbs, 1);
  10. }
  11. void printHelloWorld() {
  12. NSLog(@"Hello World!");
  13. }
  14. static void (*app_printHelloWorld)(void);
  15. void hook_printHelloWorld() {
  16. NSLog(@"hook成功");
  17. // 调用原始的printHelloWorld函数
  18. app_printHelloWorld();
  19. }

原因:

系统定义的C函数,由于具体的函数实现是在系统共享库中,因此在程序编译期间是无法获取到这个C函数的实现地址,只能通过一种叫符号绑定的方法动态链接到函数名。

自定义的C函数,由于函数的实现和函数的调用是在同一个MachO文件(App本身的MachO文件)中,因此在编译链接期间,xcode就直接将函数调用语句和函数的实现地址进行了链接,也就不会有系统C函数的那些步骤了。

源码分析

调用Hook方法

  1. // rebinding结构体
  2. struct rebinding ex;
  3. // 要hook的方法
  4. ex.name = "method_exchangeImplementations";
  5. // 函数指针指向我们新实现的方法
  6. ex.replacement = myExchange;
  7. // 指向指针的指针(&exchangeP:函数指针的地址)
  8. ex.replaced = (void *)&exchangeP;
  9. // rebinding结构体数组
  10. struct rebinding rebs[1] = {ex};
  11. rebind_symbols(rebs, 1);

声明rebinding类型的结构体变量

rebinding 类型的结构体变量,其源码如下:

  1. struct rebinding {
  2. const char *name;
  3. void *replacement;
  4. void **replaced;
  5. };

void **replaced 是指向指针的指针,可以理解为一个存着另一个指针地址的指针,在上述示例中, *replaced 取出的就是一个指向共享库中 method_exchangeImplementations 函数实现的指针,再对其取值,**replaced 得到的就是共享库中 method_exchangeImplementations 函数实现的首地址。

声明原函数指针和替换的函数

  1. // exchangeP:函数指针
  2. void(*exchangeP)(Method _Nonnull m1, Method _Nonnull m2);
  3. // Hook的函数
  4. void myExchange(Method _Nonnull m1, Method _Nonnull m2){
  5. SEL methodName = method_getName(m1);
  6. NSString *log = [NSString stringWithFormat:@"❗️发现了非法操作有人想要交换方法:%@",NSStringFromSelector(methodName)];
  7. NSLog(@"%@", log);
  8. //调用原始的方法
  9. exchangeP(m1,m2);
  10. }

重新绑定符号

把上面声明好的结构体变量放入数组中,调用rebind_symbols开始重绑定符号(如果绑定成功返回 0,否则返回 -1)。传入参数为结构体数组和数组长度。

  1. int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
  2. int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
  3. if (retval < 0) {
  4. return retval;
  5. }
  6. // 根据_rebindings_head->next是否为空判断是不是第一次调用
  7. if (!_rebindings_head->next) {
  8. // 第一次调用的话,调用_dyld_register_func_for_add_image注册监听方法
  9. // 已经被dyld加载过的image会立刻进入回调,之后的image会在dyld装载的时候触发回调,回调方法是_rebind_symbols_for_image
  10. _dyld_register_func_for_add_image(_rebind_symbols_for_image);
  11. } else {
  12. // 遍历已经加载的image,找到所有目标函数逐一进行hook
  13. uint32_t c = _dyld_image_count();
  14. for (uint32_t i = 0; i < c; i++) {
  15. _rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
  16. }
  17. }
  18. return retval;
  19. }

调用函数-- prepend_rebindings

把需要绑定的数据信息放入链表中备用。

  1. static int prepend_rebindings(struct rebindings_entry **rebindings_head,
  2. struct rebinding rebindings[],
  3. size_t nel) {
  4. struct rebindings_entry *new_entry = (struct rebindings_entry *) malloc(sizeof(struct rebindings_entry));
  5. if (!new_entry) {
  6. return -1;
  7. }
  8. new_entry->rebindings = (struct rebinding *) malloc(sizeof(struct rebinding) * nel);
  9. if (!new_entry->rebindings) {
  10. free(new_entry);
  11. return -1;
  12. }
  13. memcpy(new_entry->rebindings, rebindings, sizeof(struct rebinding) * nel);
  14. new_entry->rebindings_nel = nel;
  15. new_entry->next = *rebindings_head;
  16. *rebindings_head = new_entry;
  17. return 0;
  18. }

_rebindings_head 被声明为一个指向 rebindings_entry 类型结构体的静态指针变量,那 &_rebindings_head 就是取出这个指针的地址,再看该函数的参数声明 struct rebindings_entry ** ,没错这又是一个指向指针的指针。

  1. struct rebindings_entry {
  2. struct rebinding *rebindings;
  3. size_t rebindings_nel;
  4. struct rebindings_entry *next;
  5. };

结构体 rebindings_entry 的三个成员分别是:

这就是典型的数据结构——链表的一种实现。_rebindings_head 就是指向该链表的指针。

最后形成的链表如下图所示,调用的顺序为rebind_symbols({rebinding4},1)rebind_symbols({rebinding3},1)rebind_symbols({rebinding1,rebinding2},2)
未命名文件 (1).png-29.2kB

一句话总结 prepend_rebindings 函数的目的:将新加入的 rebindings 数组不断的添加到 _rebindings_head 这个链表的头部成为新的头节点。

获取镜像文件的header和偏移

fishhook 的代码执行时间非常早,所以第一次执行时要 hook 的库可能还没完成装载,因此这里如果是第一次调用会通过一个函数对库的装载完成注册监听和回调的方法:

  1. _dyld_register_func_for_add_image(_rebind_symbols_for_image);

已经被dyld加载过的image会立刻进入回调,之后的image会在dyld装载的时候触发回调,回调方法是_rebind_symbols_for_image

第二次调用的时候镜像文件已经全部装载完成了,所以调用一下方法遍历即可:

  1. // 遍历已经加载的image,找到所有目标函数逐一进行hook
  2. uint32_t c = _dyld_image_count();
  3. for (uint32_t i = 0; i < c; i++) {
  4. _rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
  5. }

当回调到 _rebind_symbols_for_image 时,会将存着待绑定函数信息的链表作为参数传入,用于符号查找和函数指针的交换,第二个参数 header 是当前 image 的头信息,第三个参数 slideASLR 的偏移:

  1. static void _rebind_symbols_for_image(const struct mach_header *header,
  2. intptr_t slide) {
  3. rebind_symbols_for_image(_rebindings_head, header, slide);
  4. }

通过image的Load Commands获取各种地址

主要通过image里的Load Commands来查找链接时程序的基址、符号表地址、字符串表地址、动态符号表地址、懒加载表、非懒加载表。

  1. 查找链接时程序的基址
    截屏2020-07-14 上午9.36.19.png-353.4kB

  2. 查找符号表地址、字符串表地址
    截屏2020-07-14 上午9.40.31.png-351.9kB

  3. 查找动态符号表地址
    截屏2020-07-14 上午9.44.54.png-548.9kB

  4. 查找懒加载表、非懒加载表
    截屏2020-07-14 上午9.46.57.png-398.4kB

  1. static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
  2. const struct mach_header *header,
  3. intptr_t slide) {
  4. Dl_info info;
  5. // 这个dladdr杉树就是在程序里面找header
  6. if (dladdr(header, &info) == 0) {
  7. return;
  8. }
  9. // 定义好几个变量,然后从MachO里面去找并一一赋值
  10. segment_command_t *cur_seg_cmd;
  11. segment_command_t *linkedit_segment = NULL;
  12. struct symtab_command* symtab_cmd = NULL;
  13. struct dysymtab_command* dysymtab_cmd = NULL;
  14. // 跳过header的大小,找loadCommand
  15. uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
  16. // header->ncmds:loadCommand的数量
  17. for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
  18. // 当前的loadCommand
  19. cur_seg_cmd = (segment_command_t *)cur;
  20. if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
  21. if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
  22. linkedit_segment = cur_seg_cmd;
  23. }
  24. } else if (cur_seg_cmd->cmd == LC_SYMTAB) {
  25. symtab_cmd = (struct symtab_command*)cur_seg_cmd;
  26. } else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
  27. dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
  28. }
  29. }
  30. // 如果刚出获取的有一项为空就直接返回
  31. if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
  32. !dysymtab_cmd->nindirectsyms) {
  33. return;
  34. }
  35. // Find base symbol/string table addresses
  36. // 链接时程序的基址 = __LINKEDIT.VM_Address - __LINKEDIT.File_Offset + slide的改变值
  37. uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
  38. // 符号表的地址 = 基址 + 符号表偏移量
  39. nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
  40. // 字符串表地址 = 基址 + 字符串表偏移量
  41. char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
  42. // Get indirect symbol table (array of uint32_t indices into symbol table)
  43. // 动态符号表地址 = 基址 + 动态符号表偏移量
  44. uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
  45. // 跳过header的大小,找loadCommand
  46. cur = (uintptr_t)header + sizeof(mach_header_t);
  47. // header->ncmds:loadCommand的数量
  48. for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
  49. cur_seg_cmd = (segment_command_t *)cur;
  50. if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
  51. // 寻找data段
  52. if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
  53. strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
  54. continue;
  55. }
  56. // cur_seg_cmd->nsects:在segment里的section的数量
  57. for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
  58. section_t *sect =
  59. (section_t *)(cur + sizeof(segment_command_t)) + j;
  60. // 找懒加载表
  61. if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
  62. perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
  63. }
  64. // 找非懒加载表
  65. if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
  66. perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
  67. }
  68. }
  69. }
  70. }
  71. }

找到目标函数实现地址

导出图片Tue Jul 14 2020 09_09_45 GMT+0800 (中国标准时间).png-203kB

这张图主要在描述如何由一个字符串(比如 "NSLog"),跟着它在 MachO 文件的懒加载表中对应的指针,一步步的找到该指针指向的函数实现地址,大致步骤如下:

  1. 得到Indirect Symbols
    Indirect Symbolsindirect symbol table中的地址 = 懒加载表或非懒加载表的reserved1 + 动态符号表基址。

    1. uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;

    截屏2020-07-14 上午10.25.03.png-361.2kB

  2. 找到符号在Symbol Table表中的索引
    遍历section里的每一个符号,在indirect symbol table->Indirect Symbols找到符号在Symbol Table表中的索引,下图所示NSLog是第一个,Symbol Table表中的索引为0xA=10

    1. uint32_t symtab_index = indirect_symbol_indices[i];

    截屏2020-07-14 上午10.29.55.png-704.5kB

  3. 找到符号在String Table中的偏移
    从第二步中得到的索引10在Symbol Table中查找,从上往下数第11个,如下图所示,索引10的位置对应的 Data = 0x44

    1. //strtab_offset就是0x44
    2. uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;

    截屏2020-07-14 上午10.38.39.png-777.6kB

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

    1. char *symbol_name = strtab + strtab_offset;

    截屏2020-07-14 上午11.28.48.png-835.7kB

  5. 通过符号名相等确定函数地址
    遍历section里的每一个符号,通过以上步骤得到符号名,判断符号名跟我们需要hook的函数符号名相等,得到函数实现对应的索引,然后就可以开始Hook替换函数地址为自定义函数地址了。以下为主要函数替换部分的代码:

  1. // slide+section->addr 就是符号对应的存放函数实现的数组,用来寻找到函数的地址
  2. void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
  3. // 让rebindings[j].replaced保存indirect_symbol_bindings[i]的函数地址
  4. *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
  5. // 将替换后的方法给原先的方法,也就是替换内容为自定义函数地址
  6. indirect_symbol_bindings[i] = cur->rebindings[j].replacement;

完整实现代码:

  1. static void perform_rebinding_with_section(struct rebindings_entry *rebindings,
  2. section_t *section,
  3. intptr_t slide,
  4. nlist_t *symtab,
  5. char *strtab,
  6. uint32_t *indirect_symtab) {
  7. // 懒加载表或非懒加载表的reserved1 + 动态符号表基址 = Indirect Symbols在indirect symbol table中的地址
  8. uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
  9. // slide+section->addr 就是符号对应的存放函数实现的数组,用来寻找到函数的地址
  10. void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
  11. // 遍历section里的每一个符号
  12. for (uint i = 0; i < section->size / sizeof(void *); i++) {
  13. // 找到符号在Symbol Table表中的索引
  14. uint32_t symtab_index = indirect_symbol_indices[i];
  15. if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
  16. symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) {
  17. continue;
  18. }
  19. // 以symtab_index作为下标,访问symbol table中对应的符号信息
  20. uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
  21. // 获取symbol_name
  22. char *symbol_name = strtab + strtab_offset;
  23. // 判断是否函数的名称有两个字符,为啥是两个,因为函数前面有个_,所以方法的名称最少要1个
  24. if (strnlen(symbol_name, 2) < 2) {
  25. continue;
  26. }
  27. // 遍历最初的链表,逐一进行hook
  28. struct rebindings_entry *cur = rebindings;
  29. while (cur) {
  30. for (uint j = 0; j < cur->rebindings_nel; j++) {
  31. // 判断symbol_name[1]与rebindings中对应的函数名是否相等,相等即为目标hook函数
  32. if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
  33. // 判断replaced的地址不为NULL以及我方法的实现和rebindings[j].replacement的方法不一直,避免重复交换和空指针
  34. if (cur->rebindings[j].replaced != NULL &&
  35. indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
  36. // 让rebindings[j].replaced保存indirect_symbol_bindings[i]的函数地址
  37. *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
  38. }
  39. // 将替换后的方法给原先的方法,也就是替换内容为自定义函数地址
  40. indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
  41. goto symbol_loop;
  42. }
  43. }
  44. cur = cur->next;
  45. }
  46. symbol_loop:;
  47. }
  48. }

参考

fishhook的实现原理浅析
fishhook使用场景&源码分析

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注