15 - fishhook源码解析

fishhook使用

struct rebinding reb;
reb.name="NSLog";
reb.replaced = (void *)&sys_NSLog;
reb.replacement = my_NSLog;

struct rebinding rebs[] = {reb};
rebind_symbols(rebs, 1);

使用说明:

  1. 定义一个rebinding结构体
    1. rebinding结构体中name参数表示原函数名称
    2. rebinding结构体中replaced参数表示原函数地址
    3. rebinding结构体中replacement参数表示新的函数地址
  2. 调用rebind_symbols函数,将新函数的地址替换懒加载符号表中原函数的地址。

fishhook原理

rebind_symbols函数

int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
    int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
    if (retval < 0) {
    return retval;
  }
  if (!_rebindings_head->next){
    _dyld_register_func_for_add_image(_rebind_symbols_for_image);
  } else {
    uint32_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;
}

分析:
1、prepend_rebindings函数,每一次调用rebind_symbols,都会将rebindings数组,以头插法的形式添加到,_rebindings_head链表中。
2、根据_rebindings_head->next是否为空判断rebind_symbols函数是否是第一次调用。
若是第一次调用,则调用_dyld_register_func_for_add_image注册监听方法。那么已经被dyld加载的image会立刻进入回调,若没被dyld加载的image会在dyld装载的时候触发回调。
若不是第一次调用,则遍历已经加载的image,然后进行的hook。

_rebind_symbols_for_image函数

static void _rebind_symbols_for_image(const struct mach_header *header, intptr_t slide) {
    rebind_symbols_for_image(_rebindings_head, header, slide);
}

static void rebind_symbols_for_image(struct rebindings_entry *rebindings, const struct mach_header *header, intptr_t slide) {
  Dl_info info;
  if (dladdr(header, &info) == 0) {
    return;
  }

  segment_command_t *cur_seg_cmd;
  segment_command_t *linkedit_segment = NULL;
  struct symtab_command* symtab_cmd = NULL;
  struct dysymtab_command* dysymtab_cmd = NULL;

  uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
  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) {
      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;
  }

  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);

  uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);

  cur = (uintptr_t)header + sizeof(mach_header_t);
  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;
      }
        
      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);
        }
      }
    }
  }
}

分析:
1、进程可通过dladdr()获取有关最近定义给定address的符号的信息。dladdr()可确定指定的address是否位于构成进程的进址空间的其中一个加载模块(可执行库或共享库)内。如果某个地址位于在其上面映射加载模块
的基址和为该加载模块映射的最高虚拟地址之间(包括两端),则认为该地址在加载模块的范围内。如果某个加载模块符合这个条件,则会搜索其动态符号表,以查找与指定的address最接近的符号。

2、从Mach-O中获取符号表地址、字符表地址以及动态符号表地址
符号表地址(代码中所用符号的信息) = 程序基址(ASLR) + 符号表偏移量
字符表地址(存储代码中的所有字符) = 程序基址(ASLR) + 字符串表偏移量
动态符号表地址(存储各符号在符号表中的标号) = 程序基址(ASLR) + 动态符号表偏移量
其中:
符号表偏移量Load Commands找到LC_SYMTAB,其中symoff就是符号表偏移量
字符串表偏移量Load Commands找到LC_SYMTAB,其中stroff就是字符串表偏移量
动态符号表偏移量: Load Commands找到LC_DYSYMTAB,其中indirectsymoff就是动态符号表偏移量

3、遍历所有的Load Commands,定位至__DATA段__DATA_CONST段,并遍历__DATA段__DATA_CONST段的所有section,找到section type分别为S_LAZY_SYMBOL_POINTERSS_NON_LAZY_SYMBOL_POINTERSsection,将section信息和符号表地址、字符表地址以及动态符号表地址一起作为perform_rebinding_with_section函数的入参。

perform_rebinding_with_section函数

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) {
  uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
  void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
  for (uint i = 0; i < section->size / sizeof(void *); i++) {
    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;
    }
      uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
      char *symbol_name = strtab + strtab_offset;
      bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1];
      struct rebindings_entry *cur = rebindings;
      while (cur) {
          for (uint j = 0; j < cur->rebindings_nel; j++) {
              if (symbol_name_longer_than_1 &&
                  strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
                  if (cur->rebindings[j].replaced != NULL &&
                      indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
                      *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
                  }
                  indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
                  goto symbol_loop;
        }
      }
      cur = cur->next;
    }
  symbol_loop:;
  }
}

分析:接下来的分析以S_LAZY_SYMBOL_POINTERSsection为例,S_NON_LAZY_SYMBOL_POINTERSsection与之类似。

1、从section中获取reserved1,表示懒加载符号位于动态符号表的起始索引值,并使用indirect_symbol_indices指针指向动态符号中懒加载符号的起始位置。

2、从section中获取addr,该地址indirect_symbol_bindings指向一个数组,数组中的内容是所有懒加载函数实现地址。在该数组中,懒加载函数实现地址的顺序与动态符号表中懒加载符号的顺序是一致的。例如:懒加载函数实现地址数组中第一个为NSLog的地址。那在动态符号表的第reserved1个就是NSLog的符号。

3、遍历section中所有符号,使用indirect_symbol_indices取下标方法得到符号在动态符号表中的值(symtab_index),这个值就是符号在符号表中的索引号

4、以symtab_index作为下标,访问符号表,可以得到符号的名称。

5、遍历rebindings_entry链表,对比HOOK函数的名称与当前定位的符号表中符号的名称。若一致,则将indirect_symbol_bindings中符号对应的函数实现地址修改成rebindings_entry链表中replacement指定的地址。并将原函数地址赋值给replaced

至此,函数HOOK完成。当调用待HOOK的函数时,查找indirect_symbol_bindings中的函数实现地址时,已经被HOOK的函数的地址了。

总结

  • 【第一步】通过接口设置需要HOOK的函数名称,以及需要替换的函数地址。
  • 【第二步】从Mach-O中得到符号表、字符表、动态符号表的地址
  • 【第三步】从section中获取懒加载符号在动态符号表中的起始地址。
  • 【第四步】从第三步的得到的起始索引值开始遍历动态符号表,得到符号位于符号表的索引值。
  • 【第五步】从符号表中获取符号位于字符串表的偏移值。
  • 【第六步】从字符串表中获取符号名称,并与设置HOOK函数的名称进行对比,若一致,则修改符号对应的函数实现地址为replacement指定的地址,并将原函数地址赋值给replaced

流程图如下:

16214137877191.jpg
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容