一、fishhook原理
1.1 fishhook代码分析
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
//prepend_rebindings的函数会将整个 rebindings 数组添加到 _rebindings_head 这个链表的头部
//Fishhook采用链表的方式来存储每一次调用rebind_symbols传入的参数,每次调用,就会在链表的头部插入一个节点,链表的头部是:_rebindings_head
int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
//根据上面的prepend_rebinding来做判断,如果小于0的话,直接返回一个错误码回去
if (retval < 0) {
return retval;
}
//根据_rebindings_head->next是否为空判断是不是第一次调用。
if (!_rebindings_head->next) {
//第一次调用的话,调用_dyld_register_func_for_add_image注册监听方法.
//已经被dyld加载的image会立刻进入回调。
//之后的image会在dyld装载的时候触发回调。
_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 void _rebind_symbols_for_image(const struct mach_header *header,
intptr_t slide) {
rebind_symbols_for_image(_rebindings_head, header, slide);
}
- 1、Fishhook采用链表的方式来存储每一次调用rebind_symbols传入的参数,每次调用,就会在链表的头部插入一个节点,链表的头部是:_rebindings_head, prepend_rebindings的函数会将整个 rebindings 数组添加到 _rebindings_head 这个链表的头部
- 2、根据上面的prepend_rebinding来做判断,如果小于0的话,直接返回一个错误码回去
- 3、根据_rebindings_head->next是否为空判断是不是第一次调用。
- 3.1 第一次调用的话,调用_dyld_register_func_for_add_image注册监听方法.
- 3.2 已经被dyld加载的image会立刻进入回调。
- 3.3 之后的image会在dyld装载的时候触发回调。
- 3.4 不是第一次调用,则 遍历已经加载的image,进行的hook
//回调的最终就是这个函数! 三个参数:要交换的数组 、 image的头 、 ASLR的偏移
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
const struct mach_header *header,
intptr_t slide) {
/*dladdr() 可确定指定的address 是否位于构成进程的进址空间的其中一个加载模块(可执行库或共享库)内,如果某个地址位于在其上面映射加载模块的基址和为该加载模块映射的最高虚拟地址之间(包括两端),则认为该地址在加载模块的范围内。如果某个加载模块符合这个条件,则会搜索其动态符号表,以查找与指定的address 最接近的符号。最接近的符号是指其值等于,或最为接近但小于指定的address 的符号。
*/
/*
如果指定的address 不在其中一个加载模块的范围内,则返回0 ;且不修改Dl_info 结构的内容。否则,将返回一个非零值,同时设置Dl_info 结构的字段。
如果在包含address 的加载模块内,找不到其值小于或等于address 的符号,则dli_sname 、dli_saddr 和dli_size字段将设置为0 ; dli_bind 字段设置为STB_LOCAL , dli_type 字段设置为STT_NOTYPE 。
*/
//这个dladdr函数就是在程序里面找header
Dl_info info;
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);
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_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 + silde的改变值
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
// printf("地址:%p\n",linkedit_base);
//符号表的地址 = 基址 + 符号表偏移量
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);
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) {
//寻找到data段
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);
}
}
}
}
}
1.2 Fishhook源码汇总分析
- rebind_symbols
- rebindings数组添加到链表
- 根据链表判断是否第一次调用.这么做的目的时保证注册方法只会调用一次.两种情况都是为了回调_rebind_symbols_for_image
- 第一次:利用 _dyld_register_func_for_add_image注册监听方法
- 如果不是第一次,循环遍历已经加装的image. 进行 _rebind_symbols_for_image回调
- rebind_symbols_for_image
- 第一步,拿到那三张表再内存中的地址
- 符号表的地址: symtab
- 字符串表的地址: strtab
- 动态(间接)符号表的地址: indirect_symtab
- 第二步、找懒加载和非懒加载表
- 第三步、调用perform_rebinding_with_section: 要交换的数组、懒加载和非懒加载表、三个表地址;
- 第一步,拿到那三张表再内存中的地址
- perform_rebinding_with_section
- 1、得到indirect_symbol_bindings
- 2、遍历间接符号表最终找到符号的过程
- 3、判断是否是需要HOOK的函数
- 4、保存函数指针,然后替换懒加载符号表里面的函数地址,完成HOOK
二、Dobby框架
- 内联钩子,所谓InlineHook就是直接修改目标函数的头部代码.让它跳转到我们自定义的函数里面执行我们的代码,从而达到Hook的目的.这种Hook技术一般用在静态语言的HOOK上面.
2.1 编译Dobby
- 首先将代码clone下来
- depth用于指定克隆深度,为1表示只克隆最近一次commit
$ git clone https://github.com/jmpews/Dobby.git --depth=1
- 注意它是跨平台的,所以项目并不是一个Xcode工程,我们要使用cmake将这个工程编译为Xcode工程.进入Dobby目录,创建一个文件夹,然后cmake编译工程
- 最新的源码中没有ios.toolchain.cmake文件、所以从老项目里边拖了一个过来
cd Dobby && mkdir build_for_ios_arm64 && cd build_for_ios_arm64
cmake .. -G Xcode \
-DCMAKE_TOOLCHAIN_FILE=cmake/ios.toolchain.cmake \
-DPLATFORM=OS64 -DARCHS="arm64" -DCMAKE_SYSTEM_PROCESSOR=arm64 \
-DENABLE_BITCODE=0 -DENABLE_ARC=0 -DENABLE_VISIBILITY=1 -DDEPLOYMENT_TARGET=9.3 \
-DDynamicBinaryInstrument=ON -DNearBranch=ON -DPlugin.SymbolResolver=ON -DPlugin.Darwin.HideLibrary=ON -DPlugin.Darwin.ObjectiveC=ON
-
- 该文件存放在Dobby/cmake文件夹下
- 也可以采用官方建议的构建方法、因为本质上也就是获取动静态库.
- 为iphoneos构建
python3 scripts/platform_builder.py --platform=iphoneos --arch=all
- 得到构建产物:动态库和静态库
2.2 Dobby应用
- 将DobbyX.framework添加进一个新的iOS项目中、依次添加拷贝进项目
- DobbyHOOK函数
// function inline hook
// arg1: 需要HOOK的函数地址
// arg2: 新函数地址
// arg3: 原始函数指针的地址
int DobbyHook(void *address, dobby_dummy_func_t replace_func, dobby_dummy_func_t *origin_func);
- 编写测试代码
- (void)viewDidLoad {
[super viewDidLoad];
DobbyHook(sum, mySum, (void *)&sum_p);
}
int sum(int a, int b) {
return a+b;
}
//函数指针用于保留原来的执行
static int (*sum_p)(int a,int b);
//新函数地址
int mySum(int a,int b) {
NSLog(@"HOOK success,原来的结果是 %d",sum_p(a,b));
return a-b;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"output result %d",sum(10, 20));
}
- 运行成功,点击屏幕的输出结果为
2023-04-10 20:06:45.004089+0800 InlineHookDemo[17503:615561] HOOK success,原来的结果是 30
2023-04-10 20:06:45.004274+0800 InlineHookDemo[17503:615561] output result -10
- 由此可见,已成功HOOK
- 通过汇编真机调试可以看到
- 前三句代码被替换
- 拉伸栈空间的代码没有了
- 因此
- 静态函数的HOOK,并没有在原始函数中增加代码,而是将拉伸栈空间的三句代码进行了替换
- 当调用原始函数,才会拉伸栈平衡.然后在原始函数的代码中,恢复栈平衡
2.3 HOOK函数地址
- 上面的案例存在着问题、当实际逆向开发中,三方会剥离符号表,我们无法获得符号名称,所以HOOK的一定是地址、
- 应用每次启动时,ASLR偏移地址都不同,所以不能直接HOOK地址
- 正确的做法: 先找到函数在MachO中的偏移地址,加上PAGEZERO的0x100000000,再加上本次启动的ASLR偏移地址
- 运行程序,显示汇编,点击屏幕,触发断点、
- 此时的 0x10114a360 地址即是要跳转的函数地址A
- 通过image list 在控制台获取 ASLR地址 0x0000000101144000 : B
- A - B = 0x6360 偏移量, 查看再MachO中代码段的内容为
- 对应的正是sum函数的汇编
static uintptr_t sumP = 0x100006328;
- (void)viewDidLoad {
[super viewDidLoad];
//获取ASLR,相当于rebase
uintptr_t aslr = _dyld_get_image_vmaddr_slide(0);
sumP += aslr;
NSLog(@"sump: %p\n sum:%p",(void *)sumP,sum);
DobbyHook((void *)sumP, mySum, (void *)&sum_p);
}
- 然而这里我们对代码进行了修改,那么sum地址会发生改变,这个时候查看6360已经不是刚才的代码了
- 很明显,变成了6328, 结合输出打印的地址 0x100d8a328、减去ASLR、得到0x6328
- 修改测试案例、得到HOOK成功的日志输出
- 因此: 在代码不修改的情况下,地址不会发生改变.所以在逆向开发中,分析第三方应用,不会出现上述出现的这种情况
2.4 Dobby总结
- Dobby原理: 运行时对目标函数的汇编代码替换,修改的是内存中MachO的代码段
- Dobby替换汇编代码时,对原始函数的调用,会影响栈的拉伸和平衡
- 在真实HOOK场景中,我们拿不到符号名称,只能对地址进行HOOK
- HOOK地址时,需要加上PAGEZERO和ASLR