一.啥是fishhook
fishhook是一个运行在IOS的模拟器和真机环境,在MACH-O文件中能够动态重新绑定符号的简单的库。
二.咋用呢
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSLog(@"");//先执行一次系统函数,共享函数库里才会加载进这个方法 -> 待确定
// struct rebinding {
// const char *name; //需要hook的函数名称,c字符串
// void *replacement; //新函数的地址
// void **replaced; //原函数的地址
// };
struct rebinding logRebinding;
logRebinding.name = "NSLog";
logRebinding.replacement = myLog;
logRebinding.replaced = (void *)&sys_nslog;
struct rebinding rebs[1] = {logRebinding};
//rebs: 存放rebinding结构体的数组
//1: 数组的长度
rebind_symbols(rebs, 1);
NSLog(@"originLog");
return YES;
}
static void (*sys_nslog)(NSString *format, ...);
void myLog(NSString *format, ...) {
format = @"newLog";
sys_nslog(format);
}
//2022-05-17 14:31:10.058392+0800 fishhookdemo[2753:1073295] newLog
当我们打印NSLog(@"originLog");
时,输出了newLog
,很神奇
三.为啥这么神奇?
道理很简单,就是找到NSlog
的位置,将其替换成我们自己的函数地址就行,只是说这个找寻过程有点麻烦而已。
这就需要我们具备一定的铺垫知识。
3.1 动态库共享缓存(dyld shared cache)
在iOS系统中,每个程序依赖的动态库都需要通过dyld(位于/usr/lib/dyld)一个一个加载到内存,然而如果在每个程序运行的时候都重复的去加载一次,势必造成运行缓慢,为了优化启动速度和提高程序性能,共享缓存机制就应运而生。所有默认的动态链接库被合并成一个大的缓存文件,放到/System/Library/Caches/com.apple.dyld/目录下,按不同的架构保存着.
NSlog
就在共享动态库里面
3.2 懒加载符号表
- 我们把项目里的
NSLog
函数都注释掉
- 打开
NSLog
函数,使用machoview看下,发现NSLog
函数是存放在Lazy Symbol Pointers
即懒加载段里面, 偏移量为00008010
在NSLog
之前打一个断点,然后使用image list
拿到基地址0x0000000100814000
- 基地址+偏移量按理说应该可以拿到
NSLog
函数的内存地址
16进制打印
- 查看
NSLog
函数的指针,反汇编一下,发现并不是NSLog
函数
- 跳过
NSLog
这个断点,重新看下0x000000010081c018
地址存放的数据,发现地址变了
- 反编译下
震惊,发现该地址是NSLog
函数
- 结论: 懒加载表中的函数,是在函数运行一次后才会确定函数的地址
所以使用fishhook
需要先调用下要hook的函数
3.3 符号绑定& 重绑定符号
NSLog
函数属于共享缓存库的函数,所以在编译期间,是不能确定它的地址的。只有在运行时,dyld
才会从共享缓存库拿到NSLog
函数并且确定它的地址,写入到懒加载符号表里
四.咋实现的呢
4.1 记录下简单的步骤
- 找到字符串表
symtab_cmd->stroff
symtab_segment - 找到符号表
LC_SYMTAB
segment - 找到动态符号表
LC_DYSYMTAB
segment -
indirect_symtab = LC_DYSYMTAB->indirectsymoff
拿到间接符号表indirect_symtab
,间接符号表其实就是动态库的符号表 - 拿到懒加载section
__la_symbol_ptr
,这里面就有NSLog
函数的地址 -
indirect_symbol_indices = indirect_symtab + __la_symbol_ptr.reserved1
得到__la_symbol_ptr
模块在indirect_symtab
表的起始位置 7 8 9 - for循环取出
LC_SYMTAB.7.n_un.n_strx
拿到其在字符串表symtab_segment
中的位置, 取出字符串NSLog
- 判断要替换的字符串和取出的字符串是否一样,一样的话将
__la_symbol_ptr
替换成我们的函数
4.2 搞个超简版demo,来替换NSLog
调用下面的函数,你会发现不管你NSLog什么,打印出来都是"替换NSLog成功啦"
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSLog(@"");//先执行一次系统函数,共享函数库里才会加载进这个方法 -> 因为是懒
//验证
replaceNSLog(youNSLog);
NSLog(@"");
return YES;
}
static void youNSLogFuc() {
printf("替换NSLog成功啦");
}
void (*youNSLog)(void) = youNSLogFuc;
fishcook.h
#ifndef fishcook_h
#define fishcook_h
#include <stdio.h>
#ifdef __cplusplus
extern "C" {
#endif //__cplusplus
//替换成你自己的函数
int replaceNSLog(void *youNSLog);
#ifdef __cplusplus
}
#endif //__cplusplus
#endif /* fishcook_h */
fishcook.m
//
// fishcook.c
// fishhookdemo
//
// Created by liyongkai on 2022/7/21.
//
#include "fishcook.h"
#include <mach-o/dyld.h>
#include <mach/mach.h>
#include <mach-o/loader.h>
#include <mach-o/nlist.h>
void *_youNSLog;
/// 替换函数
/// @param symtab 符号表
/// @param strtab 字符串表
/// @param section __la_symbol_ptr
/// @param indirect_symtab 间接符号表
static void replaceImp(intptr_t vmaddr_slide,
struct nlist_64 *symtab,
char *strtab,
struct section_64 *section,
uint32_t *indirect_symtab) {
//拿到__la_symbol_ptr section里面的内容在间接表中的起始位置
uint32_t *indirect_symtab_indices = indirect_symtab + section->reserved1;
for (uint i = 0; i < section->size / sizeof(void *); i ++) {
uint32_t tabIndex = indirect_symtab_indices[i];
//通过符号表拿到nslog在字符串表中的位置
uint32_t strtab_offset = symtab[tabIndex].n_un.n_strx;
//拿到字符串
char *symbol_name = strtab + strtab_offset;
//如果一样就替换
if (strcmp(&symbol_name[1], _youNSLog)) {
void **la_symbol_prt_address = (void **)(vmaddr_slide + section->addr);
la_symbol_prt_address[i] = _youNSLog;
return;
}
}
}
static void allImages(const struct mach_header* header, intptr_t vmaddr_slide) {
//1.能到符号表segment和动态符号表segment
//记录当前的segmnet
struct segment_command_64 *cur_seg_cmd;
//符号表LC_SYMTAB
struct symtab_command* symtab_cmd = NULL;
//动态符号表LC_DYSYMTAB
struct dysymtab_command* dysymtab_cmd = NULL;
//第一个segment的地址
uintptr_t cur = (uintptr_t )header + sizeof(struct mach_header_64);
//循环遍历,找到 符号表LC_SYMTAB, 动态符号表LC_DYSYMTAB
for (uint i = 0; i < header->ncmds; i ++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (struct segment_command_64 *)cur;
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) {
// 符号表或者动态符号表的segment为空,说明没有动态库,return
return;
}
//2.通过segment拿到符号表section, string table, 间接符号表
uintptr_t mh_addr = (uintptr_t)_dyld_get_image_header(0);
//拿到符号表section
struct nlist_64 *symtab = (struct nlist_64 *) (mh_addr +symtab_cmd->symoff);
//拿到string table
char *strtab = (char *)(mh_addr + symtab_cmd->stroff);
//拿到间接符号表, 也就是动态库的符号表
uint32_t *indirect_symtab = (uint32_t *)(mh_addr + dysymtab_cmd->indirectsymoff);
//找到__got section 和 la_symbol_ptr section, 他们都在__DATA segment里面
cur = (uintptr_t)header + sizeof(struct mach_header_64);
for (uint i = 0; i < header->ncmds; i ++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (struct segment_command_64 *)cur;
if (cur_seg_cmd->cmd == LC_SEGMENT_64) {
if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
strcmp(cur_seg_cmd->segname, "__DATA_CONST") != 0) {
continue;
}
for (uint j = 0; j < cur_seg_cmd->nsects; j ++) {
struct section_64 *sect = (struct section_64 *)(cur + sizeof(struct segment_command_64)) + j;
if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
// 懒加载符号表
replaceImp(vmaddr_slide, symtab, strtab, sect, indirect_symtab);
return;
}
}
}
}
}
int replaceNSLog(void *youNSLog) {
_youNSLog = youNSLog;
//监听各个image
_dyld_register_func_for_add_image(allImages);
return 0;
}
五.总结
说白了,fishhook代码看似复杂,主要mach-O内部结构体复杂,找来找去很是麻烦。但如果有一天你对mach-O了如执掌,那么fishhook也就不攻自破了。
- fishhook只能hook动态库里面的函数,包含__got和la_symbol_ptr段
- 使用fishhook需要先调用下要hook的函数
- 将hook代码尽量提前
六.Q
总结哪些函数属于OC函数,哪些属于共享缓存库