Fishhook & mach-O

hook 在 iOS 开发中又叫 Swizzle Method, 简单说就是交换两个函数/方法的地址, 一般是交换某个系统方法和自己实现的方法, 用于实现对系统行为的监控或者边界保护.
iOS 开发中有多种 hook 的方法, 本文主要聚焦于 fishhook 的实现与应用

iOS 开发中的 hook

  1. Method Swizzling, iOS 里最基础最原生的 Hook 方法, 本质上就是交换两个方法的 IMP(函数指针)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    + (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    Class class = [self class];

    SEL originalSelector = @selector(loopLogWithCount:);
    SEL swizzledSelector = @selector(hook_loopLogWithCount:);

    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

    BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (success) {
    class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
    method_exchangeImplementations(originalMethod, swizzledMethod);
    }
    });
    }


    - (void)hook_loopLogWithCount:(NSInteger)count {
    [self hook_loopLogWithCount:count];
    NSLog(@"hook after count: %d", count);
    }
  2. Message Forwarding 消息转发
  3. fishhook, 对 C 方法进行 hook,
    • OOMDetector 实现内存分配的统计就是这么做的
  4. libffi
  5. 静态库插桩

本文主要聚焦于 fishhook 的实现与应用.

fishhook 使用

fishhook 是由 Facebook 开源的一个重新绑定符号的库, 其主要作用是在程序运行时动态修改绑定符号指针. 像 OOMDetector 也是使用 fishhook hook 了多个关键的系统内存申请方法来达成内存监控的目的的, 关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void hookMalloc()
{
if(!isPaused){
beSureAllRebindingFuncBeenCalled();

orig_malloc = malloc;
orig_calloc = calloc;
orig_valloc = valloc;
orig_realloc = realloc;
orig_block_copy = _Block_copy;
rebind_symbols_for_imagename(
(struct rebinding[5]){
{"realloc",(void*)new_realloc,(void**)&orig_realloc},
{"malloc", (void*)new_malloc, (void **)&orig_malloc},
{"valloc",(void*)new_valloc,(void**)&orig_valloc},
{"calloc",(void*)new_calloc,(void**)&orig_calloc},
{"_Block_copy",(void*)new_block_copy,(void**)&orig_block_copy}},
5,
getImagename());
}
else{
isPaused = false;
}

}

一个简单的使用如下(在 iOS 15 出现崩溃, 见文章“被冰封的Bug:Fishhook Crash修复纪实”):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 修复崩溃
pod 'fishhook', :git => 'https://github.com/facebook/fishhook.git', :branch => 'main'
// 然后执行 pod install

#import "fishhook.h"

- (void)viewDidLoad
{
[super viewDidLoad];
struct rebinding nslog;
nslog.name = "NSLog";//要hook的函数名称
nslog.replacement = myNSLog;//这里是函数的指针, 也就是函数名称
nslog.replaced = &sys_nslog;
// rebinding 结构体数组
struct rebinding rebs[1] = {nslog};
/***
存放rebinding 结构体数组
数组长度
*/
rebind_symbols(rebs, 1);

NSLog(@"测试");
}

static void(*sys_nslog)(NSString * format,...);
//定义一个新函数
void myNSLog(NSString *format,...){
format = [format stringByAppendingString:@"~~~~~hook 到了!"];
sys_nslog(format);
}

// 输出:
测试~~~~~hook 到了!

完成了对系统库中NSLog的 hook, 并添加自定义的日志后缀.

fishhook 原理

Mach-O 基础知识

  1. 镜像(image): 在 Mach-O 文件系统中, 所有的可执行文件 / dylib 以及 Bundle 都是镜像
  2. 动态加载(dyld): dyld的全称是dynamic loader, 它的作用是加载一个进程所需要的image
  3. PIC(Position Indepent Code, 位置无关代码): 使用 PIC 的 Mach-O 文件, 在引用符号(比如 printf )的时候, 并不是直接去找到符号的地址(编译期并不知道运行时 printf 的函数地址), 而是通过在__DATA Segment上创建一个指针, 等到启动的时候, dyld动态的去做绑定(bind), 这样__DATA Segment上的指针就指向了printf的实现。
  4. _TEXT段(代码段)可读可执行, _DATA段(数据段)可读可写
  5. Load Commands, 是加载指令, 描述的是文件的加载信息, 内容包括区域的位置/符号表/动态符号表等, 我们可以从这里获取到符号表和字符串表的偏移量等
  6. Symbol Table, 符号表, 符号表是将地址和符号联系起来的桥梁, 符号表并不能直接存储符号, 而是存储符号位于字符串表的位置
  7. String Table, 字符串表所有的变量名/函数名等, 都以字符串的形式存储在字符串表中
  8. Dynamic Symbol Table, 动态符号表存储的是动态库函数位于符号表的偏移信息. (__DATA,__la_symbol_ptr) section 可以从动态符号表中获取到该section位于符号表的索引数组, 动态符号表并不存储符号信息, 而是存储其位于符号表的偏移信息
  9. Lazy Symbol Pointers, 懒加载符号表, 所谓懒加载是指在程序运行时需要访问这些符号的时候再去绑定, 这些符号一般来自程序依赖的动态库
  10. Non Lazy Symbol Pointers 非懒加载符号表, 所谓非懒加载是指在程序一加载就绑定好的, 这些符号一般来自程序依赖的动态库
  11. Symbol Stubs, 翻译过来就是符号桩. 它与Lazy Symbol Pointers是一一对应的, 每次访问外部符号时都会先访问Symbol Stubs, 然后执行桩代码, 最后去Lazy Symbol Pointers找到相应的符号地址执行下一步操作
  12. LC_SECMENT_64(__LINKEDIT): 动态链接器需要使用的信息, 包括重定位信息, 绑定信息, 懒加载信息等
  13. LC_SYMTAB: 为文件定义符号表和字符串表, 在链接文件时边链接器使用, 同时也用于调试映射符号到源文件. 符号表定义的本地符号仅用于调试, 而已定义和为定义的external符号被链接器使用
  14. LC_DYSYMTAB: 将符号表中给出符号的额外符号信息提供给动态链接器
  15. __DATA段中, 有两个Sections和动态符号绑定有关
    • __nl_symbol_ptr, 存储了non-lazily绑定的符号, 这些符号在Mach-O加载的时候绑定
    • __la_symbol_ptr 存储了lazy绑定的符号(方法), 这些方法在第一次调用的时候, 由dyld_stub_binder来绑定, 所以可以看到, 每个Mach-O的non-lazily绑定符号都有dyld_stub_binder.
  16. dyld_stub_binder 符号绑定函数,
  17. Indirect Symbol Table 间接符号表, 存储符号的偏移值

通过dyld的API我们可以很容易找到这些Symbols指针, 但是并不知道这些指针具体代表哪种函数. 所以, 只要找到这些指针代表的字符串, 和当前的要替换的进行比较, 如果一样的话, 就替换当前指针的实现就可以Hook了.

iOS中的符号绑定机制(懒加载)

在iOS中符号绑定机制具体是怎么运作的?简单描述下

  1. 第一次调用, 会跳转到桩代码处
    1. 0x10265c6a0(pc 指针的值) + 0x3988 = 0x102660028, 对应Mach-O文件的 0xc028 处, 所属 __DATA.__la_symbol_ptr 段, 也是 __stub_helper 开始和回写数据处
    2. 后续根据指令执行 _dyld_stub_binder 绑定真正的地址并回写到 __DATA.__la_symbol_ptr
  2. 第二次调用, 此时 __DATA.__la_symbol_ptr 已经被回写为真实的运行时地址, 可以直接 br 跳转到函数执行

fishhook 原理简介

上面介绍的PIC是fishhook能够工作的核心. finshhook 通过rebind_symbols修改 __DATA Segment 上的符号指针指向, 来动态的 hook C函数.

fishhook 官方流程

上图就是演示了寻找符号的过程, 我们根据这张图来分析一下这个过程:

  1. 从 Mach-O 文件的 __DATA 段中的 lazy 符号指针表中查找某个符号, 获得这个符号的偏移量 1061, 然后在每一个 section_64 中查找 reserved1, 通过这两个值找到 Indirect Symbol Table 中符号对应的条目
  2. Indirect Symbol Table 找到符号表指针以及对应的索引 16343 之后, 就需要访问符号表.
  3. 然后通过符号表中的偏移量, 获取字符串表中的符号 _close

源码解析

接口函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 this was the first call, register callback for image additions (which is also invoked for
// existing images, otherwise, just run on existing images
if (!_rebindings_head->next) {
//第一次调用, 注册回调方法
_dyld_register_func_for_add_image(_rebind_symbols_for_image);
} else {
//遍历已经加载的image, 进行实际的hook
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;
}
  • _dyld_register_func_for_add_image, 这个函数是用来注册回调, 当dyld链接符号时, 调用此回调函数
  • rebind_symbols_for_image 做了具体的替换和填充

实现过程

  1. 遍历Load Command, 找到__LINKEDIT段, indirect symbol tablesymbol table
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    segment_command_t *cur_seg_cmd; //当前指针
    segment_command_t *linkedit_segment = NULL; //__LINKEDIT段
    struct symtab_command* symtab_cmd = NULL; //symbol table
    struct dysymtab_command* dysymtab_cmd = NULL; //indirect symbol table

    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) { //找到__LINKEDIT段
    if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
    linkedit_segment = cur_seg_cmd;
    }
    } else if (cur_seg_cmd->cmd == LC_SYMTAB) { //symbol table
    symtab_cmd = (struct symtab_command*)cur_seg_cmd;
    } else if (cur_seg_cmd->cmd == LC_DYSYMTAB) { //indirect symbol table
    dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
    }
    }
  2. 找到symbol tablestring table的base地址
    1
    2
    3
    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);
  3. 获取indriect table的数据(uint32_t类型的数组)
    1
    uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
  4. 再一次遍历laod command, 这一次遍历__DATA段中的Sections, 对S_LAZY_SYMBOL_POINTERSS_NON_LAZY_SYMBOL_POINTERS段中的指针进行 rebind, 调用函数perform_rebinding_with_section
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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_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);
    }
    }
    }
    }
  5. 最后在字符串表中获得符号的名字 char *symbol_name, 有了符号名称以后, 剩下的代码会遍历整个 rebindings_entry 数组, 在其中查找匹配的符号, 完成函数实现的替换
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    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) {
    //读取indirect table中的数据(uint32_t)的数组
    uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
    void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);

    //遍历indirect table
    for (uint i = 0; i < section->size / sizeof(void *); i++) {
    //读取indirect 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_name
    char *symbol_name = strtab + strtab_offset;
    bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1];
    //遍历最初提到链表, 来一个个hook
    struct rebindings_entry *cur = rebindings;
    while (cur) {
    for (uint j = 0; j < cur->rebindings_nel; j++) { //每一个链表的结点包括一个hook的C数组
    if (symbol_name_longer_than_1 && strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) { //如果名称一致
    kern_return_t err;

    if (cur->rebindings[j].replaced != NULL && indirect_symbol_bindings[i] != cur->rebindings[j].replacement) //如果没有被替换, 并且数据合法, 则进行替换
    *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i]; // 保存当前位置, 当做原函数指针

    /**
    * 1. Moved the vm protection modifying codes to here to reduce the
    * changing scope.
    * 2. Adding VM_PROT_WRITE mode unconditionally because vm_region
    * API on some iOS/Mac reports mismatch vm protection attributes.
    * -- Lianfu Hao Jun 16th, 2021
    **/
    err = vm_protect (mach_task_self (), (uintptr_t)indirect_symbol_bindings, section->size, 0, VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY);
    if (err == KERN_SUCCESS) {
    /**
    * Once we failed to change the vm protection, we
    * MUST NOT continue the following write actions!
    * iOS 15 has corrected the const segments prot.
    * -- Lionfore Hao Jun 11th, 2021
    **/
    indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
    }
    goto symbol_loop;
    }
    }
    cur = cur->next;
    }
    symbol_loop:;
    }
    }
  6. 在之后对某一函数的调用(例如 open), 当查找其函数实现时, 都会查找到 new_open 的函数指针, 在 new_open调用 origianl_open 时, 同样也会执行原有的函数实现, 因为我们通过 *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i] 将原函数实现绑定到了新的函数指针上

图解 fishhook

使用下方变化来说明懒加载以及 fishhook 工作的流程:

  1. load command中_DATA segement中__la_symbol_ptr section 结构图,说明该section和动态符号表对应的起始索引是146

  2. 数据区域中__la_symbol_ptr的结构,可以看出该section的起始地址是0x00240B0, 图2和图3是为了找malloc函数指针的位置

  3. 数据区域中__la_symbol_ptr的结构, 偏移了一定的位置。文件0x000242B0地址出存储的是malloc函数指针。

  4. 转到数据区域中动态符号表的起始位置处, 该图说明动态符号表的起始地址是0x3B0A4

  5. 计算la_symbol_ptr 对应的符号在动态符号表中的位置: 0x3B060 + 146*4 = 0x3B060 + 0x248 = 0x3B2A8, 地址0x0003B2A8处后面的符号和la_symbol_ptr中的条目对应。

  6. 查找64个偏移后的动态符号表的地址:0x3B2A8 + 0x40*x4 = 0x3B3A8, 地址0x3B3A8中存储的值是符号表中的索引, 为0xb32。由于machoviewer看不到符号表, 所以用代码查看符号表中索引为0xB32 的符号信息。查看代码:struct nlist_64 const * mallocNlist =[self getSymbol64ByIndex:0xB32];

  7. 代码查看符号表示意图。符号表的索引为0xB32的符号的信息。可以看出n_strx的值是0x2B07, 这个值指的是string表中的偏移量

  8. string 表, string表的起始地址是0x3B498

  9. string 表, 偏移0x2B07 后的结果—— 0x3B498 + 0x2B07 = 0x3DF9F, 这个地址存储的字符串就是图3中 地址为0x10001ef10的函数指针的名称, 名称是malloc。这个名称和要替换的名称一致, 所以替换图3中的 0x10001ef10, 替换成新指定函数指针地址。实现替换

fishhook 的局限性

fishhook 能够工作的原理还是 PIC (Position Independ Code), 从dyld的角度来说, 就是 Mach-O 外部符号(像printf引用其他动态库)绑定的过程. 对于内部符号, fishhook 是无法进行 hook 的. 内部 C函数在编译后, 函数的实现会在Mach-O__TEXT (代码段) 里. 编译后, 当前调用处的指针, 是直接指向代码段中的地址的. 这个地址是由 Mach-O 的 base+offset 获得的, 其中offset是一定的. dyld 在装载的时候, 只需要对这些符号进行 rebase 即可(修改地址为 newbae+offset).

MachO 在组件化中的运用

组件化时候, 会需要将各个组件的接口注册到 Router 中,

一般有三种方式

  1. + load 各自注册协议
  2. 集中注册(把各个+ load 中的注册代码在启动时统一执行)
  3. Data 段注册 (也叫二进制注册, 业界通用方案, 参考alibaba/BeeHive)

  • used的作用是告诉编译器, 我声明的这个符号是需要保留的. 被used修饰以后, 意味着即使函数没有被引用, 在Release下也不会被优化。如果不加这个修饰, 那么Release环境链接器会去掉没有被引用的段
  • 编译时会将一个字符串写入__Data 数据段的 “ProtocolClass” 段中,
  • header 是 mach_header, 一个section 的header头
  • 执行时是加载 Mach-O 的回调中, 当加载 “ProtocolClass” 这个数据段的回调中使用 getsectiondata 得到数据

参考文档

  1. fishhook
  2. OOMDetector
  3. matrix
  4. Fishhook-源码分析
  5. iOS逆向 —- fishhook使用及原理探索
  6. MachO文件编译链接常见的三大认知误区
  7. 深入理解MachO结构与运行时系统
  8. 深入浅出 MachO
  9. 被冰封的Bug:Fishhook Crash修复纪实
  10. iOS中的Mach-O&重定向&符号绑定&符号重绑定
  11. alibaba/BeeHive
-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道