- 一个iOS程序员的自我修养(一)编译和链接
- 一个iOS程序员的自我修养(二)Mach-O里面有什么
- 一个iOS程序员的自我修养(三)Mach-O文件静态链接
- 一个iOS程序员的自我修养(四)可执行文件的装载
- 一个iOS程序员的自我修养(五)Mach-O文件动态链接
- 一个iOS程序员的自我修养(六)动态链接应用:fishhook原理
- 一个iOS程序员的自我修养(七)静态链接应用:静态库插桩原理
- 一个iOS程序员的自我修养(八)内存
前言
有一个很经典的面试题,为什么 fishhook 只能 hook 动态库中的 c 函数?或者可以反过来问,为什么 fishhook 不能 hook OC 方法和我们自己写的 c 函数?
首先看一下 fishhook 对工作原理的一段描述:
dyld binds lazy and non-lazy symbols by updating pointers in particular sections of the __DATA segment of a Mach-O binary. fishhook re-binds these symbols by determining the locations to update for each of the symbol names passed to rebind_symbols and then writing out the corresponding replacements.
fishhook 通过动态绑定的方式来对 Mach-O 的 __DATA 段上的 lazy 和 non-lazy 符号进行 hook。一旦定位到 Mach-O 中这些符号的位置便对其进行重新绑定,然后让其调用自定义的替换函数。
这下再来回答上面的两个问题:
- fishhook 为什么不能 hook OC 方法?所有的 OC 方法最终都会编译为 c 方法,还记得上文动态链接例子中
testPrint
方法吗,最终它被编译为了objc_msgSend
函数调用,存放到了 DATA 段的Lazy Symbol Pointers
表中。由于 PLT 机制,在绑定的时候,最终它会指向 got 段中的 dyld_stub_binder 函数,这个过程在上一篇动态链接中已经分析过了。当然 hook objc_msgSend 函数是可行的,DATA 段可读写,通过修改 Mach-O 文件中的 objc_msgSend 函数指针指向可以对其进行 hook,这也是 hook 动态库的 c 函数核心原理,但对于 OC 方法,无能为力。 - fishhook 为什么不能 hook 我们自己写的 c 函数?还记得动态链接中介绍 PIC 时讲到的第一种情况吗,在生成 Mach-O 文件时,内部函数跳转地址通过 PC 寻址已经确定,指令都被存放在 TEXT 段,权限只读无法修改。
fishhook源码分析
fishhook 代码很少,整个 fishhook.c 仅有 257 行代码,下面结合对 Mach-O 文件结构和动态链接的理解,分析下 fishhook 源码的完整流程。在分析入口函数 rebind_symbols
之前,需要先了解下 fishhook 内部的数据结构,fishhook 内部维护了一个单向链表,链表的节点结构如下:
struct rebindings_entry {
struct rebinding *rebindings;//重新绑定的 rebindings 结构体
size_t rebindings_nel;//重新绑定的函数数量
struct rebindings_entry *next;//链表的下一节点
};
rebindings 结构体数组里面存储的 rebinding 结构体就是我们在使用 fishhook 时定义的,它的结构如下:
struct rebinding {
const char *name;// 函数名称
void *replacement;// 新的函数指针
void **replaced;// 保存原始函数地址的变量的指针
};
然后在通过 prepend_rebindings
看下 fishhook 内对数据结构的处理过程:
static int prepend_rebindings(struct rebindings_entry **rebindings_head,
struct rebinding rebindings[],
size_t nel) {
//申请一个 rebindings_entry 结构体的内存空间
struct rebindings_entry *new_entry = (struct rebindings_entry *) malloc(sizeof(struct rebindings_entry));
if (!new_entry) {
return -1;
}
//申请 nel 个 rebinding 结构体的内存空间
new_entry->rebindings = (struct rebinding *) malloc(sizeof(struct rebinding) * nel);
if (!new_entry->rebindings) {
free(new_entry);
return -1;
}
//将 rebindings 的值拷贝到 new_entry->rebindings
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;
}
了解了内部数据结构之后再重新回到入口函数 rebind_symbols:
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
//将 rebindings 数组插入到链表结构中的头节点
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
//头节点没有下一个节点那说明当前链表只有刚插入的节点,这说明是第一次调用。
//_dyld_register_func_for_add_image:动态库被加载会调用这个监听,同时已经加载的库也会回调。
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;
}
什么也没做,直接透传给 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);
}
// rebind_symbols_for_image 就是定位 Mach-O 中 __nl_symbol_ptr 和__la_symbol_ptr 所在 section 以及 符号表、动态符号表、字符串表的过程:
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;
//header 的偏移+header 的大小。目的是跳过Mach-O中的header部分寻找Segment。
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
//header->ncmds:加载命令数量
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
//当前的 Load Command
cur_seg_cmd = (segment_command_t *)cur;
//类型是LC_SEGMENT
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
//找到__LINKEDIT段
//为什么要寻找__LINKEDIT?fishhook 通过 __LINKEDIT 在内存地址中的偏移 - __LINKEDIT在文件中的偏移 + slide = 文件加载的基地址。
//那么slide是什么?因为 ASLR 的缘故,Mach-O 文件加载的时候地址随机,Mach-O 文件每次加载进内存的时候地址都不一样,这个slide就是本次加载在内存上的偏移。
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_base即前面提到的基地址,通过基地址+各种表在文件中的偏移=各种表在内存中的位置。
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
//找到符号表。
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
//还记得符号表的结构吗?
/*
struct symtab_command {
uint32_t cmd; /* LC_SYMTAB */
uint32_t cmdsize; /* sizeof(struct symtab_command) */
uint32_t symoff; /* symbol table offset */
uint32_t nsyms; /* number of symbol table entries */
uint32_t stroff; /* string table offset */
uint32_t strsize; /* string table size in bytes */
};
*/
//stroff:字符串表的偏移。
//找到字符串表。
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);
cur = (uintptr_t)header + sizeof(mach_header_t);
//再次遍历 Load Commands
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和__DATA_CONST的section。
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的数量
//遍历所有section
for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
section_t *sect =
(section_t *)(cur + sizeof(segment_command_t)) + j;
//找到__la_symbol_ptr,需要延迟绑定的符号
if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
//寻找__nl_symbol_ptr,非延迟绑定的符号
if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
}
}
}
}
定位 __nl_symbol_ptr 和 __la_symbol_ptr 以及对其中的具体函数进行重绑定工作:
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) {
const bool isDataConst = strcmp(section->segname, SEG_DATA_CONST) == 0;
//还记得 reserved1 吗,nl_symbol_ptr和la_symbol_ptrsection中的reserved1表示在动态符号表中的起始index。
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
//找到__nl_symbol_ptr和__la_symbol_ptr里面的函数指针存放的地方。
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
vm_prot_t oldProtection = VM_PROT_READ;
if (isDataConst) {
oldProtection = get_protection(rebindings);
mprotect(indirect_symbol_bindings, section->size, PROT_READ | PROT_WRITE);
}
//遍历section中的符号。
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;
}
//symtab[symtab_index]:对应符号表中的符号,
//找到该符号在字符串表中的偏移。
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];
//fishhook 内部做了单向链表来存储所有的勾子结构体。
struct rebindings_entry *cur = rebindings;
while (cur) {
//遍历所有的节点。
for (uint j = 0; j < cur->rebindings_nel; j++) {
//判断符号表中取出来的和外部传入要hook的符号名称是否一致。
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) {
//indirect_symbol_bindings[i]:__nl_symbol_ptr和__la_symbol_ptr里面的函数指针之一。
//用 cur->rebindings[j].replaced 保存原指针指向。
*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
}
//将原函数指针替换为 cur->rebindings[j].replacement。这样调用原函数就变成了调用我们指定的替换函数。
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
goto symbol_loop;
}
}
cur = cur->next;
}
symbol_loop:;
}
if (isDataConst) {
int protection = 0;
if (oldProtection & VM_PROT_READ) {
protection |= PROT_READ;
}
if (oldProtection & VM_PROT_WRITE) {
protection |= PROT_WRITE;
}
if (oldProtection & VM_PROT_EXECUTE) {
protection |= PROT_EXEC;
}
mprotect(indirect_symbol_bindings, section->size, protection);
}
}
到这里函数的 hook 工作就完成了,下面通过实际的例子再验证一下 fishhook 的 hook 结果。
fishhook过程分析
新建 testfishhook 工程代码如下,通过 lldb 在运行时下进一步分析符号的替换过程:
@implementation ViewController
static void(*sys_nslog)(NSString * format,...);
//替换 NSLog 的函数
void newNslog(NSString * format,...){
format = [format stringByAppendingString:@"我被勾上了!\n"];
//调用原 NSLog
sys_nslog(format);
}
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"我是NSLog");
// Do any additional setup after loading the view.
struct rebinding nslog;
nslog.name = "NSLog";// 要替换的函数名称
nslog.replacement = newNslog; // 新函数指针
nslog.replaced = (void *)&sys_nslog;// 指向原始函数地址的指针
//rebinding结构体数组
struct rebinding rebs[1] = {nslog};
rebind_symbols(rebs, 1);
NSLog(@"我是NSLog");
}
@end
在 Xcode 中执行 command+b 后得到 Products 目录下的 Mach-O 文件:
通过 MachOView 反汇编: Lazy Symbol Pointers 中的 NSLog 符号在文件中的偏移为 0x5000。
通过 image list
命令可以查看所有的 image 文件,list 中第一个即 testfishhook 的 Mach-O 文件,在内存中真实的偏移地址为 0x000000010d59d000。分析源码的时候提到过,因为 ASLR 的缘故,Mach-O 文件加载的时候地址随机,Mach-O 文件每次加载进内存的时候地址都不一样。
Lazy Symbol Pointers 中的 NSLog 符号真实偏移:0x000000010d59d000+0x5000,通过 x 0x000000010d59d000+0x5000
可查看地址内存储的指令。通过 dis -s
命令反汇编可以看到该指令正是 NSLog 函数。
然后将断点下到第二个 NSLog:
通过反汇编的结果看到,NSLog 跳转的指令已经是 newNslog
了。
总结
fishhook 的代码称得上是短小精悍,通过对 fishhook 原理的探究,能够让我们更全面的理解动态链接的过程以及 Mach-O 文件内部的结构,利用这个非常经典的库可以做很多黑魔法的工作。