hook 在 iOS 开发中又叫 Swizzle Method, 简单说就是交换两个函数/方法的地址, 一般是交换某个系统方法和自己实现的方法, 用于实现对系统行为的监控或者边界保护.
iOS 开发中有多种 hook 的方法, 本文主要聚焦于 fishhook 的实现与应用
iOS 开发中的 hook
- 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);
} - Message Forwarding 消息转发
- 比较有名的库 Aspect 以及 MessageThrottle 还有JSPatch 都是使用消息转发实现的
- 参考文章Aspect 与 消息转发
- fishhook, 对 C 方法进行 hook,
- OOMDetector 实现内存分配的统计就是这么做的
- libffi
- 静态库插桩
本文主要聚焦于 fishhook 的实现与应用.
fishhook 使用
fishhook 是由 Facebook 开源的一个重新绑定符号的库, 其主要作用是在程序运行时动态修改绑定符号指针. 像 OOMDetector 也是使用 fishhook hook 了多个关键的系统内存申请方法来达成内存监控的目的的, 关键代码如下:
1 | void hookMalloc() |
一个简单的使用如下(在 iOS 15 出现崩溃, 见文章“被冰封的Bug:Fishhook Crash修复纪实”):
1 | // 修复崩溃 |
完成了对系统库中NSLog
的 hook, 并添加自定义的日志后缀.
fishhook 原理
Mach-O 基础知识
- 镜像(image): 在 Mach-O 文件系统中, 所有的可执行文件 / dylib 以及 Bundle 都是镜像
- 动态加载(dyld): dyld的全称是
dynamic loader
, 它的作用是加载一个进程所需要的image - PIC(Position Indepent Code, 位置无关代码): 使用 PIC 的 Mach-O 文件, 在引用符号(比如
printf
)的时候, 并不是直接去找到符号的地址(编译期并不知道运行时printf
的函数地址), 而是通过在__DATA Segment
上创建一个指针, 等到启动的时候,dyld
动态的去做绑定(bind), 这样__DATA Segment
上的指针就指向了printf
的实现。 _TEXT
段(代码段)可读可执行,_DATA
段(数据段)可读可写Load Commands
, 是加载指令, 描述的是文件的加载信息, 内容包括区域的位置/符号表/动态符号表等, 我们可以从这里获取到符号表和字符串表的偏移量等Symbol Table
, 符号表, 符号表是将地址和符号联系起来的桥梁, 符号表并不能直接存储符号, 而是存储符号位于字符串表的位置String Table
, 字符串表所有的变量名/函数名等, 都以字符串的形式存储在字符串表中Dynamic Symbol Table
, 动态符号表存储的是动态库函数位于符号表的偏移信息. (__DATA,__la_symbol_ptr
) section 可以从动态符号表中获取到该section位于符号表的索引数组, 动态符号表并不存储符号信息, 而是存储其位于符号表的偏移信息Lazy Symbol Pointers
, 懒加载符号表, 所谓懒加载是指在程序运行时需要访问这些符号的时候再去绑定, 这些符号一般来自程序依赖的动态库Non Lazy Symbol Pointers
非懒加载符号表, 所谓非懒加载是指在程序一加载就绑定好的, 这些符号一般来自程序依赖的动态库Symbol Stubs
, 翻译过来就是符号桩. 它与Lazy Symbol Pointers
是一一对应的, 每次访问外部符号时都会先访问Symbol Stubs
, 然后执行桩代码, 最后去Lazy Symbol Pointers
找到相应的符号地址执行下一步操作LC_SECMENT_64(__LINKEDIT)
: 动态链接器需要使用的信息, 包括重定位信息, 绑定信息, 懒加载信息等LC_SYMTAB
: 为文件定义符号表和字符串表, 在链接文件时边链接器使用, 同时也用于调试映射符号到源文件. 符号表定义的本地符号仅用于调试, 而已定义和为定义的external符号被链接器使用LC_DYSYMTAB
: 将符号表中给出符号的额外符号信息提供给动态链接器- 在
__DATA
段中, 有两个Sections和动态符号绑定有关__nl_symbol_ptr
, 存储了non-lazily绑定的符号, 这些符号在Mach-O加载的时候绑定__la_symbol_ptr
存储了lazy绑定的符号(方法), 这些方法在第一次调用的时候, 由dyld_stub_binder
来绑定, 所以可以看到, 每个Mach-O的non-lazily绑定符号都有dyld_stub_binder
.
dyld_stub_binder
符号绑定函数,Indirect Symbol Table
间接符号表, 存储符号的偏移值
通过dyld的API我们可以很容易找到这些Symbols指针, 但是并不知道这些指针具体代表哪种函数. 所以, 只要找到这些指针代表的字符串, 和当前的要替换的进行比较, 如果一样的话, 就替换当前指针的实现就可以Hook了.
iOS中的符号绑定机制(懒加载)
在iOS中符号绑定机制具体是怎么运作的?简单描述下
- 第一次调用, 会跳转到桩代码处
- 0x10265c6a0(pc 指针的值) + 0x3988 = 0x102660028, 对应Mach-O文件的
0xc028
处, 所属__DATA.__la_symbol_ptr
段, 也是__stub_helper
开始和回写数据处 - 后续根据指令执行
_dyld_stub_binder
绑定真正的地址并回写到__DATA.__la_symbol_ptr
- 0x10265c6a0(pc 指针的值) + 0x3988 = 0x102660028, 对应Mach-O文件的
- 第二次调用, 此时
__DATA.__la_symbol_ptr
已经被回写为真实的运行时地址, 可以直接br
跳转到函数执行
fishhook 原理简介
上面介绍的PIC是fishhook能够工作的核心. finshhook 通过rebind_symbols
修改 __DATA Segment
上的符号指针指向, 来动态的 hook C函数.
fishhook 官方流程
上图就是演示了寻找符号的过程, 我们根据这张图来分析一下这个过程:
- 从 Mach-O 文件的
__DATA
段中的 lazy 符号指针表中查找某个符号, 获得这个符号的偏移量 1061, 然后在每一个section_64
中查找reserved1
, 通过这两个值找到Indirect Symbol Table
中符号对应的条目 - 在
Indirect Symbol Table
找到符号表指针以及对应的索引 16343 之后, 就需要访问符号表. - 然后通过符号表中的偏移量, 获取字符串表中的符号
_close
源码解析
接口函数
1 | int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) { |
_dyld_register_func_for_add_image
, 这个函数是用来注册回调, 当dyld链接符号时, 调用此回调函数rebind_symbols_for_image
做了具体的替换和填充
实现过程
- 遍历
Load Command
, 找到__LINKEDIT
段,indirect symbol table
和symbol table
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18segment_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;
}
} - 找到
symbol table
和string table
的base地址1
2
3uintptr_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); - 获取
indriect table
的数据(uint32_t类型的数组)1
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
- 再一次遍历
laod command
, 这一次遍历__DATA
段中的Sections
, 对S_LAZY_SYMBOL_POINTERS
和S_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
20cur = (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);
}
}
}
} - 最后在字符串表中获得符号的名字
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
58static 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:;
}
} - 在之后对某一函数的调用(例如
open
), 当查找其函数实现时, 都会查找到new_open
的函数指针, 在new_open
调用origianl_open
时, 同样也会执行原有的函数实现, 因为我们通过*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i]
将原函数实现绑定到了新的函数指针上
图解 fishhook
使用下方变化来说明懒加载以及 fishhook 工作的流程:
load command
中_DATA segement中__la_symbol_ptr
section 结构图,说明该section和动态符号表对应的起始索引是146
数据区域中
__la_symbol_ptr
的结构,可以看出该section的起始地址是0x00240B0
, 图2和图3是为了找malloc函数指针的位置
数据区域中
__la_symbol_ptr
的结构, 偏移了一定的位置。文件0x000242B0
地址出存储的是malloc
函数指针。
转到数据区域中动态符号表的起始位置处, 该图说明动态符号表的起始地址是
0x3B0A4
计算
la_symbol_ptr
对应的符号在动态符号表中的位置:0x3B060 + 146*4 = 0x3B060 + 0x248 = 0x3B2A8
, 地址0x0003B2A8
处后面的符号和la_symbol_ptr
中的条目对应。
查找64个偏移后的动态符号表的地址:
0x3B2A8 + 0x40*x4 = 0x3B3A8
, 地址0x3B3A8
中存储的值是符号表中的索引, 为0xb32
。由于machoviewer看不到符号表, 所以用代码查看符号表中索引为0xB32
的符号信息。查看代码:struct nlist_64 const * mallocNlist =[self getSymbol64ByIndex:0xB32];
代码查看符号表示意图。符号表的索引为
0xB32
的符号的信息。可以看出n_strx
的值是0x2B07
, 这个值指的是string表中的偏移量
string 表, string表的起始地址是
0x3B498
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 中,
一般有三种方式
+ load
各自注册协议- 集中注册(把各个
+ load
中的注册代码在启动时统一执行) - Data 段注册 (也叫二进制注册, 业界通用方案, 参考alibaba/BeeHive)
used
的作用是告诉编译器, 我声明的这个符号是需要保留的. 被used
修饰以后, 意味着即使函数没有被引用, 在Release下也不会被优化。如果不加这个修饰, 那么Release环境链接器会去掉没有被引用的段- 编译时会将一个字符串写入
__Data
数据段的 “ProtocolClass” 段中, - header 是 mach_header, 一个section 的header头
- 执行时是加载 Mach-O 的回调中, 当加载 “ProtocolClass” 这个数据段的回调中使用
getsectiondata
得到数据