@qidiandasheng
2022-08-29T09:15:27.000000Z
字数 11089
阅读 1851
iOS运行时
fishhook
是 FaceBook 开源的可以动态修改 MachO 符号表的工具。fishhook
的强大之处在于它可以 HOOK 系统的静态 C 函数。
我们在链接器:符号是怎么绑定到地址上的?中有讲到可执行文件调用动态库的方法时,符号是在第一次动态库加载或者调用的时候绑定的。
苹果采用了PIC(Position-independent code)技术成功让 C 的底层也能有动态的表现:
fishhook 正是利用了 PIC 技术做了这么两个操作:
这样就把系统方法与自己定义的方法进行了交换,达到 HOOK 系统 C 函数(共享库中的)的目的。
fishhook
可以用来Hook C函数,但他也可以用来防止HOOK OC函数,基本思路如下:
#import "NSObject+fishHook.h"
#import <objc/runtime.h>
#import "fishhook.h"
@implementation NSObject (fishHooklog)
+ (void)load{
// rebinding结构体
struct rebinding ex;
// 要hook的方法
ex.name = "method_exchangeImplementations";
// 函数指针指向我们新实现的方法
ex.replacement = myExchange;
// 指向指针的指针(&exchangeP:函数指针的地址)
ex.replaced = (void *)&exchangeP;
// rebinding结构体数组
struct rebinding rebs[1] = {ex};
rebind_symbols(rebs, 1);
}
// exchangeP:函数指针
void(*exchangeP)(Method _Nonnull m1, Method _Nonnull m2);
// Hook的方法
void myExchange(Method _Nonnull m1, Method _Nonnull m2){
SEL methodName = method_getName(m1);
NSString *log = [NSString stringWithFormat:@"❗️发现了非法操作有人想要交换方法:%@",NSStringFromSelector(methodName)];
NSLog(@"%@", log);
//调用原始的方法
exchangeP(m1,m2);
}
@end
这里利用fishhook主要hook了method_exchangeImplementations
函数,在我们自定义的方法里输出被hook的方法。
我看可以设置一个白名单,在白名单里的函数允许被hook,即调用exchangeP(m1,m2);
以下代码hook失败:
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
struct rebinding rb;
rb.name = "printHelloWorld";
rb.replacement = hook_printHelloWorld;
rb.replaced = (void *)&app_printHelloWorld;
struct rebinding rbs[] = {rb};
rebind_symbols(rbs, 1);
}
void printHelloWorld() {
NSLog(@"Hello World!");
}
static void (*app_printHelloWorld)(void);
void hook_printHelloWorld() {
NSLog(@"hook成功");
// 调用原始的printHelloWorld函数
app_printHelloWorld();
}
原因:
系统定义的C函数,由于具体的函数实现是在系统共享库中,因此在程序编译期间是无法获取到这个C函数的实现地址,只能通过一种叫符号绑定的方法动态链接到函数名。
NSLog
当我们编译的时候,NSLog
函数实现的地址是undefined
Foundation
库加载到内存中NSLog
函数调用时,就会到Foundation
库的MachO文件中查询NSLog
函数的实现地址。然后将函数的实现地址和符号表中的NSLog
字符串进行绑定自定义的C函数,由于函数的实现和函数的调用是在同一个MachO文件(App本身的MachO文件)中,因此在编译链接期间,xcode就直接将函数调用语句和函数的实现地址进行了链接,也就不会有系统C函数的那些步骤了。
// rebinding结构体
struct rebinding ex;
// 要hook的方法
ex.name = "method_exchangeImplementations";
// 函数指针指向我们新实现的方法
ex.replacement = myExchange;
// 指向指针的指针(&exchangeP:函数指针的地址)
ex.replaced = (void *)&exchangeP;
// rebinding结构体数组
struct rebinding rebs[1] = {ex};
rebind_symbols(rebs, 1);
rebinding
类型的结构体变量,其源码如下:
struct rebinding {
const char *name;
void *replacement;
void **replaced;
};
void **replaced
是指向指针的指针,可以理解为一个存着另一个指针地址的指针,在上述示例中, *replaced
取出的就是一个指向共享库中 method_exchangeImplementations
函数实现的指针,再对其取值,**replaced
得到的就是共享库中 method_exchangeImplementations
函数实现的首地址。
// exchangeP:函数指针
void(*exchangeP)(Method _Nonnull m1, Method _Nonnull m2);
// Hook的函数
void myExchange(Method _Nonnull m1, Method _Nonnull m2){
SEL methodName = method_getName(m1);
NSString *log = [NSString stringWithFormat:@"❗️发现了非法操作有人想要交换方法:%@",NSStringFromSelector(methodName)];
NSLog(@"%@", log);
//调用原始的方法
exchangeP(m1,m2);
}
把上面声明好的结构体变量放入数组中,调用rebind_symbols
开始重绑定符号(如果绑定成功返回 0,否则返回 -1)。传入参数为结构体数组和数组长度。
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
if (retval < 0) {
return retval;
}
// 根据_rebindings_head->next是否为空判断是不是第一次调用
if (!_rebindings_head->next) {
// 第一次调用的话,调用_dyld_register_func_for_add_image注册监听方法
// 已经被dyld加载过的image会立刻进入回调,之后的image会在dyld装载的时候触发回调,回调方法是_rebind_symbols_for_image
_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;
}
把需要绑定的数据信息放入链表中备用。
static int prepend_rebindings(struct rebindings_entry **rebindings_head,
struct rebinding rebindings[],
size_t nel) {
struct rebindings_entry *new_entry = (struct rebindings_entry *) malloc(sizeof(struct rebindings_entry));
if (!new_entry) {
return -1;
}
new_entry->rebindings = (struct rebinding *) malloc(sizeof(struct rebinding) * nel);
if (!new_entry->rebindings) {
free(new_entry);
return -1;
}
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;
}
_rebindings_head
被声明为一个指向 rebindings_entry
类型结构体的静态指针变量,那 &_rebindings_head
就是取出这个指针的地址,再看该函数的参数声明 struct rebindings_entry **
,没错这又是一个指向指针的指针。
struct rebindings_entry {
struct rebinding *rebindings;
size_t rebindings_nel;
struct rebindings_entry *next;
};
结构体 rebindings_entry
的三个成员分别是:
rebindings
:指向 rebinding
类型结构体的指针(用来指向传入结构体数组的首元素地址)、rebindings_nel
:记录此次要重绑定的数量(用于开辟对应大小的空间)、next
:指向下一个 rebindings_entry
类型的结构体(记录下一次需要重绑定的数据)这就是典型的数据结构——链表的一种实现。_rebindings_head
就是指向该链表的指针。
最后形成的链表如下图所示,调用的顺序为rebind_symbols({rebinding4},1)
、rebind_symbols({rebinding3},1)
、rebind_symbols({rebinding1,rebinding2},2)
:
一句话总结 prepend_rebindings
函数的目的:将新加入的 rebindings
数组不断的添加到 _rebindings_head
这个链表的头部成为新的头节点。
fishhook
的代码执行时间非常早,所以第一次执行时要 hook 的库可能还没完成装载,因此这里如果是第一次调用会通过一个函数对库的装载完成注册监听和回调的方法:
_dyld_register_func_for_add_image(_rebind_symbols_for_image);
已经被dyld加载过的image会立刻进入回调,之后的image会在dyld装载的时候触发回调,回调方法是_rebind_symbols_for_image
。
第二次调用的时候镜像文件已经全部装载完成了,所以调用一下方法遍历即可:
// 遍历已经加载的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));
}
当回调到 _rebind_symbols_for_image
时,会将存着待绑定函数信息的链表作为参数传入,用于符号查找和函数指针的交换,第二个参数 header
是当前 image
的头信息,第三个参数 slide
是 ASLR
的偏移:
static void _rebind_symbols_for_image(const struct mach_header *header,
intptr_t slide) {
rebind_symbols_for_image(_rebindings_head, header, slide);
}
主要通过image里的Load Commands
来查找链接时程序的基址、符号表地址、字符串表地址、动态符号表地址、懒加载表、非懒加载表。
查找链接时程序的基址
查找符号表地址、字符串表地址
查找动态符号表地址
查找懒加载表、非懒加载表
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
const struct mach_header *header,
intptr_t slide) {
Dl_info info;
// 这个dladdr杉树就是在程序里面找header
if (dladdr(header, &info) == 0) {
return;
}
// 定义好几个变量,然后从MachO里面去找并一一赋值
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的大小,找loadCommand
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
// header->ncmds:loadCommand的数量
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
// 当前的loadCommand
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;
}
// Find base symbol/string table addresses
// 链接时程序的基址 = __LINKEDIT.VM_Address - __LINKEDIT.File_Offset + slide的改变值
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);
// Get indirect symbol table (array of uint32_t indices into symbol table)
// 动态符号表地址 = 基址 + 动态符号表偏移量
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
// 跳过header的大小,找loadCommand
cur = (uintptr_t)header + sizeof(mach_header_t);
// header->ncmds:loadCommand的数量
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;
}
// cur_seg_cmd->nsects:在segment里的section的数量
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);
}
}
}
}
}
这张图主要在描述如何由一个字符串(比如 "NSLog"),跟着它在 MachO 文件的懒加载表中对应的指针,一步步的找到该指针指向的函数实现地址,大致步骤如下:
得到Indirect Symbols
Indirect Symbols
在indirect symbol table
中的地址 = 懒加载表或非懒加载表的reserved1 + 动态符号表基址。
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
找到符号在Symbol Table
表中的索引
遍历section里的每一个符号,在indirect symbol table->Indirect Symbols
找到符号在Symbol Table
表中的索引,下图所示NSLog
是第一个,Symbol Table
表中的索引为0xA=10
。
uint32_t symtab_index = indirect_symbol_indices[i];
找到符号在String Table
中的偏移
从第二步中得到的索引10在Symbol Table
中查找,从上往下数第11个,如下图所示,索引10的位置对应的 Data = 0x44
。
//strtab_offset就是0x44
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
在String Table
中找到符号地址
String Table的起始地址(0x3300)+ 第三步中的 Data(0x44)= 就是符号的地址(0x3344),可以得到对应的符号名:5F 4E 53 4C 6F 67
,对应ASCII就是_NSLog
。
char *symbol_name = strtab + strtab_offset;
通过符号名相等确定函数地址
遍历section
里的每一个符号,通过以上步骤得到符号名,判断符号名跟我们需要hook的函数符号名相等,得到函数实现对应的索引,然后就可以开始Hook替换函数地址为自定义函数地址了。以下为主要函数替换部分的代码:
// slide+section->addr 就是符号对应的存放函数实现的数组,用来寻找到函数的地址
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
// 让rebindings[j].replaced保存indirect_symbol_bindings[i]的函数地址
*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
// 将替换后的方法给原先的方法,也就是替换内容为自定义函数地址
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
完整实现代码:
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) {
// 懒加载表或非懒加载表的reserved1 + 动态符号表基址 = Indirect Symbols在indirect symbol table中的地址
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
// slide+section->addr 就是符号对应的存放函数实现的数组,用来寻找到函数的地址
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
// 遍历section里的每一个符号
for (uint i = 0; i < section->size / sizeof(void *); i++) {
// 找到符号在Symbol 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;
// 判断是否函数的名称有两个字符,为啥是两个,因为函数前面有个_,所以方法的名称最少要1个
if (strnlen(symbol_name, 2) < 2) {
continue;
}
// 遍历最初的链表,逐一进行hook
struct rebindings_entry *cur = rebindings;
while (cur) {
for (uint j = 0; j < cur->rebindings_nel; j++) {
// 判断symbol_name[1]与rebindings中对应的函数名是否相等,相等即为目标hook函数
if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
// 判断replaced的地址不为NULL以及我方法的实现和rebindings[j].replacement的方法不一直,避免重复交换和空指针
if (cur->rebindings[j].replaced != NULL &&
indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
// 让rebindings[j].replaced保存indirect_symbol_bindings[i]的函数地址
*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
}
// 将替换后的方法给原先的方法,也就是替换内容为自定义函数地址
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
goto symbol_loop;
}
}
cur = cur->next;
}
symbol_loop:;
}
}