目录
fishhook 基本使用
-
fishhook 简介
fishhook 是一个非常简单的库,它可以对运行在 iOS 模拟器和设备上的 MachO 二进制文件的部分符号进行动态重绑定。fishhook 提供了类似于在 macOS 上使用 DYLD_INTERPOSE 的功能。fishhook 可以用来 hook 系统的 C 函数以进行调试和跟踪(比如解决文件描述符重复回收等问题)
-
fishhook 基本使用
fishhook 源码分析
-
符号地址重绑定的入口函数:rebind_symbols
rebind_symbols
函数是 fishhook 中用于符号地址重绑定的入口函数,它需要传入 2 个参数:- 重绑定信息数组的首地址
struct rebinding rebindings[]
- 重绑定信息数组的长度
size_t rebindings_nel
其中,重绑定信息的定义如下:
rebind_symbols
函数的实现如下:
- 重绑定信息数组的首地址
-
维护用于存储多次重绑定信息的链表:prepend_rebindings
为了将多次调用
rebind_symbols
函数所传递的多个重绑定信息数组(struct rebinding rebindings[]
)组织成一个链式结构,fishhook 自定义了一个结构体链表来支持这个逻辑:
prepend_rebindings
函数用于维护这个结构体链表:
-
dyld 加载镜像的回调:_rebind_symbols_for_image
rebind_symbols
函数在执行完维护结构体链表的函数prepend_rebindings
之后,继续执行如果是第一次调用
rebind_symbols
函数,则_rebindings_head -> next == NULL
,表示还没做过符号地址的替换(即表示_rebind_symbols_for_image
函数还未注册为 dyld 加载镜像的回调),此时会调用_dyld_register_func_for_add_image
函数将_rebind_symbols_for_image
函数注册为 dyld 加载镜像的回调。在调用_dyld_register_func_for_add_image
向 dyld 注册回调函数期间,会对每个已加载的镜像调用注册的回调函数_rebind_symbols_for_image
进行符号地址的重绑定。之后,每当 dyld 加载新镜像时,也会调用注册的回调函数_rebind_symbols_for_image
进行符号地址的重绑定如果不是第一次调用
rebind_symbols
函数,则_rebindings_head -> next != NULL
,表示已经做过符号地址的替换(即表示_rebind_symbols_for_image
函数已经注册为 dyld 加载镜像的回调),此时会遍历所有已加载的镜像并调用_rebind_symbols_for_image
函数进行符号地址的重绑定向 dyld 注册镜像加载的回调的函数的声明为:
// 在调用 _dyld_register_func_for_add_image() 向 dyld 注册回调函数期间,会对每个已加载的镜像调用注册的回调函数 func // 之后,每当加载和绑定新镜像时(此时该镜像还未执行初始化),也会调用注册的回调函数 func extern void _dyld_register_func_for_add_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide));
其规定了所注册的回调函数的参数列表一定是
(const struct mach_header* mh, intptr_t vmaddr_slide)
但是,如果要进行符号地址重绑定
不仅需要const struct mach_header* mh
、intptr_t vmaddr_slide
还需要static struct rebindings_entry* _rebindings_head
因此,
_rebind_symbols_for_image
函数存在的作用是:
-
准备符号地址重绑定过程所需的基地址:rebind_symbols_for_image
第一次遍历当前镜像 Load Commands 区域的目的:
为了找出LC_SEGMENT_64(__LINKEDIT)
、LC_SYMTAB
、LC_DYSYMTAB
这 3 个加载指令
从而计算出当前镜像的基地址、当前镜像中符号表的基地址、当前镜像中字符串表的基地址、当前镜像中间接符号表的基地址第二次遍历当前镜像 Load Commands 区域的目的:
获取当前镜像的LC_SEGMENT_64(__DATA)
和LC_SEGMENT_64(__DTAT_CONST)
这两个段加载指令中的__nl_symbol_ptr
和__la_symbol_ptr
这两个节
从而通过Section->reserved1
字段,获取懒绑定指针表(Lazy Binding Symbol Pointers
)和非懒绑定指针表(Non Lazy Binding Symbol Pointers
)在间接符号表(Dynamic Symbol Table
)中的起始位置 -
进行真正的符号地址重绑定:perform_rebinding_with_section
-
其他函数
get_protection
函数用于查询给定节(Section)的内存保护权限:
rebind_symbols_image
函数用于对给定镜像进行符号地址重绑定:
验证实验:通过 MachOView 验证 fishhook 准备基地址的过程
继续使用上文中 fishhook 基本使用 这一小节的代码示例:
真机运行之后,将其生成的 MachO 文件拖入 MachOView 中进行解析
注意:
因为 MachOView 进行的是静态分析,MachO 文件并未真正地在内存中运行,所以 ASLR 的偏移量 slide = 0
-
计算当前镜像在虚拟内存中的起始地址(linkedit_base)
从 MachO 文件的 Load Commands 区域的
LC_SEGMENT_64 (__LINKEDIT)
加载指令中可以得到:
①linkedit_segment->vmaddr = 0x00000001 00010000
②linkedit_segment->fileoff = 0x00000000 00010000
③linkedit_base = slide + linkedit_segment->vmaddr - linkedit_segment->fileoff = slide + 0x00000001 00000000
-
计算当前镜像的符号表在虚拟内存中的起始地址(symtab)
从 MachO 文件的 Load Commands 区域的
LC_SYMTAB
加载指令中可以得到:
①symtab_cmd->symoff = 0x00010650
②symtab = linkedit_base + symtab_cmd->symoff = slide + 0x00000001 00000000 + 0x00010650 = 0x100010650
通过 MachOView 我们可以看到,当前 MachO 文件中符号表的起始地址确实是0x100010650
-
计算当前镜像的字符串表在虚拟内存中的起始地址(strtab)
从 MachO 文件的 Load Commands 区域的
LC_SYMTAB
加载指令中可以得到:
①symtab_cmd->stroff = 0x000117F8
②strtab = linkedit_base + symtab_cmd->stroff = slide + 0x00000001 0000000 + 0x000117F8 = 0x1000117F8
通过 MachOView 我们可以看到,当前 MachO 文件中字符串表的起始地址确实是0x1000117F8
-
计算当前镜像的间接符号表在虚拟内存中的起始地址(indirect_symtab)
从 MachO 文件的 Load Commands 区域的
LC_DYSYMTAB
加载指令中可以得到:
①dysymtab_cmd->indirectsymoff = 0x00011720
②indirect_symtab = linkedit_base + dysymtab_cmd->indirectsymoff = slide + 0x00000001 0000000 + 0x00011720 = 0x100011720
通过 MachOView 我们可以看到,当前 MachO 文件中间接符号表的起始地址确实是0x100011720
-
计算非懒绑定指针表在间接符号表中的起始位置
从 MachO 文件的 Load Commands 区域的
LC_SEGMENT_64 (__DATA_CONST)
加载指令中的Section64 Header (__got)
节可以得到:
(非懒绑定指针表的第0
个元素)对应(间接符号表的第25
个元素)
因为,间接符号表的起始地址为0x100011720
,每个元素占4
个字节
所以,(非懒绑定指针表的第0
个元素)在(间接符号表)中的起始地址为:0x100011720 + 4 * 25 = 0x100011784
-
计算懒绑定指针表在间接符号表中的起始位置
从 MachO 文件的 Load Commands 区域的
LC_SEGMENT_64 (__DATA)
加载指令中的Section64 Header (__la_symbol_ptr)
节可以得到:
(懒绑定指针表的第0
个元素)对应(间接符号表的第28
个元素)
因为,间接符号表的起始地址为0x100011720
,每个元素占4
个字节
所以,(懒绑定指针表的第0
个元素)在(间接符号表)中的起始地址为:0x100011720 + 4 * 28 = 0x100011790
-
通过间接符号表所存储的符号表索引值,获取符号表中对应的符号,进而获取该符号存储在字符串表中的名称
由上面的分析过程可知:(懒绑定指针表的第
0
个元素)对应(间接符号表的第28
个元素)因为(
NSLog
的懒绑定指针)正好是(懒绑定指针表的第0
个元素)
所以(NSLog
的间接符号)也正好是(间接符号表的第28
个元素)因为,间接符号表中存储的是符号表的索引值
所以,NSLog
的符号在符号表中的索引为0x000000E8(232)
因为,符号表的起始地址为0x100010650
,每个元素占16
个字节
所以,NSLog
的符号在符号表中的起始地址为:0x100010650 + 16 * 232 = 0x1000114D0
由NSLog
在符号表中的符号可知:NSLog
的符号名称在字符串表中的偏移量为0x000000CE(206)
因为,字符串表的起始地址为0x1000117F8
,每个字符占1
个字节
所以,NSLog
的符号名称在字符串表中的起始地址为:0x1000117F8 + 1 * 206 = 0x1000118C6
-
fishhook 官方原理图
经过了上述的源码分析和验证实验,相信你对 fishhook 的实现原理一定有了较为清晰的理解了。在此附上一张 fishhook 的官方图解作为总结:
拓展延伸:fishhook 为何能在第一次调用外部 C 函数之前,获取到外部 C 函数的真实地址
-
Question
请看下面的代码示例以及输出结果:
思考一个问题:根据前面介绍的 MachO 文件的懒绑定机制,在第一次调用
NSLog
函数之前,NSLog
函数的懒绑定指针存储的是指向stub_helper
节的引用。当第一次调用NSLog
函数时,会通过stub_helper
节走到dyld_stub_binder
,然后进行一次真正的符号地址绑定,并将NSLog
函数的懒绑定指针从指向stub_helper
节修改为指向NSLog
函数的真实地址,然后再调用NSLog
函数的真正实现在上面的示例代码中,在第一次调用
NSLog
函数之前,我们使用 fishhook 对NSLog
函数进行了符号地址的重绑定。那么在对NSLog
函数进行符号地址重绑定之后,第一次调用 NSLog 函数之前,NSLog
函数的懒绑定指针存储的应该是指向swizzled_NSLog
函数的引用,并且original_NSLog
函数指针存储的应该是指向stub_helper
节的引用在上面的示例代码中,当我们第一次调用
NSLog
函数时,会走到swizzled_NSLog
的函数实现中。而在swizzled_NSLog
的函数实现中又会去调用original_NSLog
函数指针。而original_NSLog
函数指针存储的是指向stub_helper
节的引用,此时调用original_NSLog
函数指针会触发NSLog
函数的懒绑定流程(通过stub_helper
节走到dyld_stub_binder
,然后进行一次真正的符号地址绑定,并将NSLog
函数的懒绑定指针从指向swizzled_NSLog
函数的实现修改为指向NSLog
的真实地址,然后再调用NSLog
函数的真正实现)。此时控制台会打印输出hook = First
在上面的示例代码中,在调用完
NSLog(@"First");
之后,NSLog
函数的懒绑定指针应该会被重新修改为指向NSLog
函数的真正实现。那么在调用NSLog(@"Second");
时,控制台打印输出的应该是second
,但是实际的输出结果却是hook = Second
。即,此时NSLog
函数的懒绑定指针还处于 hook 状态(即,仍然指向swizzled_NSLog
函数的实现),这又是为什么呢? -
Analyze
让我们回想起 fishhook 源码中向 dyld 注册镜像加载的回调:
/* 以下函数允许你向 dyld 注册回调函数,并且每当 dyld 加载镜像时,dyld 将会调用注册的回调函数 在调用 _dyld_register_func_for_add_image() 向 dyld 注册回调函数期间,会对每个已加载的镜像调用注册的回调函数 func 之后,每当加载和绑定新镜像时(此时该镜像还未执行初始化),也会调用注册的回调函数 func */ extern void _dyld_register_func_for_add_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide));
也就是说,在调用 fishhook 的
rebind_symbols
函数对NSLog
进行符号地址重绑定时,fishhook 不是只修改了主程序 MachO 镜像中NSLog
函数的懒绑定指针,而是修改了 App 进程空间中所包含的全部 MachO 镜像的NSLog
函数的懒绑定指针在 App 的进程空间中,第 0 个镜像为主程序的 MachO,其余镜像为主程序所依赖的动态库。而在这些主程序所依赖的动态库的 MachO 镜像里面,其懒绑定指针表和非懒绑定指针表已经被 dyld 修复为指向真实的符号地址。即,包含在动态库镜像中的
NSLog
函数的懒绑定指针,也被 dyld 修复为指向NSLog
真实的实现地址可以查看 fishhook 源码中
perform_rebinding_with_section
函数的下面部分,红框处的判断代码是笔者添加的
当断点进入到红框判断后的括号里面时,第 0 个是主程序的 MachO 镜像,此时original_NSLog
的值确实是指向stub_helper
节的地址
但是这个判断并非只进入一次,App 在启动时会加载大量的动态库(通常数量会达到数百个),其中有很多都会进入到这个逻辑,比如:Foundation.framework
、CoreFoundation.framework
等等,这些动态库里面的非懒加载指针已经是真实的NSLog
的地址,所以original_NSLog
又被替换成了真实的NSLog
的地址debug 时发现这个逻辑存在大量的重复调用,本身其实只要
original_NSLog
的值不会发生改变就没必要进入下方的赋值操作。因此,笔者加了一处判断,只有当original_NSLog
的值会发生改变时才进入括号后的赋值操作
-
再扩展…
当只针对主程序 MachO 的镜像进行符号地址重绑定时,代码示例以及输出结果如下所示:
巩固练习:通过 lldb 恢复被 fishhook 替换掉的符号地址
为了加强对 MachO 文件懒绑定机制与 fishhook 底层原理的理解,接下来我们来做一个巩固练习
-
代码示例 && 断点
在调试时显示汇编代码:Debug - Debug Workflow - Always Show Disassembly
-
在命中第一个断点时,获取 NSLog 懒绑定指针所存储的 stub_helper 节的地址
// 查看 NSLog 对应的桩代码,位于 Section64(__TEXT, __stubs) (lldb) dis -a 0x1024a6388 FishhookDemo`NSLog: 0x1024a6388 <+0>: nop 0x1024a638c <+4>: ldr x16, #0x5c74 ; (void *)0x00000001024a64cc 0x1024a6390 <+8>: br x16 // 查看 NSLog 对应的懒绑定指针所存储的内容,位于 Section64(__DATA, __la_symbol_ptr) (lldb) memory read/2 (0x1024a638c+0x5c74) 0x1024ac000: 0x024a64cc 0x00000001 // 查看 NSLog 对应的懒绑定指针所指向的 Section64(__TEXT, __stub_helper) (lldb) dis -s 0x00000001024a64cc 0x1024a64cc: ldr w16, 0x1024a64d4 0x1024a64d0: b 0x1024a64b4 0x1024a64d4: udf #0x0 // 查看 Section64(__TEXT, __stub_helper) 头部的汇编代码 (lldb) dis -s 0x1024a64b4 0x1024a64b4: adr x17, #0x7094 ; _dyld_private 0x1024a64b8: nop 0x1024a64bc: stp x16, x17, [sp, #-0x10]! 0x1024a64c0: nop 0x1024a64c4: ldr x16, #0x1b4c ; (void *)0x00000001a395e08c: dyld_stub_binder 0x1024a64c8: br x16 // 查看 dyld_stub_binder 函数的汇编代码 (lldb) dis -s 0x00000001a395e08c libdyld.dylib`dyld_stub_binder: 0x1a395e08c <+0>: stp x29, x30, [sp, #-0x10]! 0x1a395e090 <+4>: mov x29, sp 0x1a395e094 <+8>: sub sp, sp, #0xf0 ; =0xf0 0x1a395e098 <+12>: stp x0, x1, [x29, #-0x10] 0x1a395e09c <+16>: stp x2, x3, [x29, #-0x20] 0x1a395e0a0 <+20>: stp x4, x5, [x29, #-0x30] 0x1a395e0a4 <+24>: stp x6, x7, [x29, #-0x40] 0x1a395e0a8 <+28>: stp x8, x9, [x29, #-0x50] // 过掉第一个断点时,控制台打印输出如下: 2021-09-06 15:55:32.300740+0800 FishhookDemo[34769:7041863] First
-
在命中第二个断点时,查看控制台打印输出结果
// 过掉第二个断点时,控制台打印输出如下: 2021-09-06 15:56:30.811207+0800 FishhookDemo[34769:7041863] hook = Second
-
在命中第三个断点时,设置 NSLog 懒绑定指针所存储的地址为 stub_helper 节
// 查看 NSLog 对应的桩代码,位于 Section64(__TEXT, __stubs) (lldb) dis -a 0x1024a6388 FishhookDemo`NSLog: 0x1024a6388 <+0>: nop 0x1024a638c <+4>: ldr x16, #0x5c74 ; (void *)0x00000001024a5454: swizzled_NSLog at /Users/Airths/Desktop/Training/FishhookDemo/FishhookDemo/ViewController.m:39 0x1024a6390 <+8>: br x16 // 查看 NSLog 对应的懒绑定指针所指向的内容,为 swizzled_NSLog 函数的汇编代码 (lldb) dis -s 0x00000001024a5454 FishhookDemo`swizzled_NSLog: 0x1024a5454 <+0>: sub sp, sp, #0x50 ; =0x50 0x1024a5458 <+4>: stp x29, x30, [sp, #0x40] 0x1024a545c <+8>: add x29, sp, #0x40 ; =0x40 0x1024a5460 <+12>: sub x8, x29, #0x8 ; =0x8 0x1024a5464 <+16>: mov x9, #0x0 0x1024a5468 <+20>: stur x9, [x29, #-0x8] 0x1024a546c <+24>: stur x0, [x29, #-0x18] 0x1024a5470 <+28>: mov x0, x8 // 将 NSLog 的懒绑定指针设置为指向原来的 Section64(__TEXT, __stub_helper) (lldb) memory write -s 8 (0x1024a638c+0x5c74) 0x00000001024a64cc // 再次查看 NSLog 对应的桩代码,位于 Section64(__TEXT, __stubs) // 发现 NSLog 对应的懒绑定指针所存储的内容,已经由指向 swizzled_NSLog 变为指向 Section64(__TEXT, __stub_helper) (lldb) dis -a 0x1024a6388 FishhookDemo`NSLog: 0x1024a6388 <+0>: nop 0x1024a638c <+4>: ldr x16, #0x5c74 ; (void *)0x00000001024a64cc 0x1024a6390 <+8>: br x16 // 过掉第三个断点时,控制台打印输出如下: 2021-09-06 15:59:49.997304+0800 FishhookDemo[34769:7041863] Third
使用 fishhook 进行简单的 hook 防护
-
iOS hook 流程简介
- 获取目标 App 的 IPA 包,并对目标 App 主程序的 MachO 文件进行脱壳
- 对已脱壳的目标 App 进行动态调试与静态分析,还原指定功能的实现过程,寻找 App 中的注入点
- 编写要注入的动态库(
.framework
、.dylib
),并在要注入的动态库中
利用 runtime 的相关函数对指定Objective-C 方法
进行 hook
利用 fishhook 的相关函数对指定外部函数
进行 hook - 把要注入的动态库集成到目标 App 中。修改主程序的 MachO 文件,让其加载要注入的动态库(例如,使用 yololib)
- 对内容发生改变的目标 App 进行重签名(主程序 MachO 文件被修改、frameworks 目录中加入了要注入的动态库)
- 运行目标 App 进行调试与测试
-
Objective-C 中 +load 方法调用顺序大总结
在不同镜像中
① dyld 会根据 App 主程序中对动态库的编译顺序来初始化动态库的镜像(先编译先初始化,后编译后初始化)
② dyld 会优先初始化动态库的镜像,然后再初始化 App 主程序的镜像(App 主程序的镜像最后初始化)
③ 在同一个镜像内,Objective-C 的 +load 方法
会比C++ 的 __attribute__((constructor) 函数
先调用
④ 所有镜像(包括 App 主程序的镜像)中的+load 方法
和__attribute__((constructor) 函数
都会比 主程序的main 函数
先调用在相同镜像中
当 dyld 初始化镜像时,会通过 RunTime 的
load_images
函数(准备和执行)镜像中所有 Class 和 Category 的+load
方法。不管程序中有没有用到这些 Class 和 Category,只要镜像被初始化,这些 Class 和 Category 就都会被加载进内存并调用+load
方法⑤ 系统自动调用
+load
方法的方式为通过函数地址直接调用(IMP
):- 先调用所有类对象的
+load
方法,按照编译顺序调用(先编译先调用,后编译后调用)。并且在调用子类对象的+load
方法之前会先调用父类对象的+load
方法 - 再调用所有分类对象的
+load
方法,按照编译顺序调用(先编译先调用,后编译后调用)
注意:分类对象与宿主类对象同名的普通方法是(后编译先调用)
注意:分类对象+load
方法的调用顺序只与分类对象的编译顺序有关,而与分类对象的继承关系无关 - 系统只会自动调用实现了
+load
方法的 Class 与 Category 中的+load
方法
系统不会自动调用没有实现+load
方法的 Class 与 Category 中的+load
方法 - 系统自动调用
+load
方法时,Class 与 Category 中的+load
方法不会产生覆盖,都会被调用
⑥ 除非开发者手动调用,否则每个 Class 和 Category 的
+load
方法在程序运行期间只会被调用 1 次⑦ 开发者手动调用
+load
方法的方式为通过消息机制间接调用(objc_msgSend
):- 如果子类未实现
+load
方法,则会调用父类的+load
方法 - 如果宿主类与分类同时实现了
+load
方法,则会调用分类的+load
方法 - 如果一个宿主类的多个分类同时实现了
+load
方法,则会调用最后参与编译的分类的+load
方法
- 先调用所有类对象的
-
使用 yololib 注入的动态库执行的优先级最低
使用 yololib 注入动态库之前,MachO 文件为:
使用 yololib 向 MachO 文件分别注入.dylib
、.framework
格式的动态库:# 输出当前目录下所包含的动态库与 MachO 文件 ~/Desktop/YololibDemo > ls -l total 512 drwxr-xr-x 7 Airths staff 224 8 14 14:55 ATool.framework drwxr-xr-x 7 Airths staff 224 8 14 14:55 BTool.framework -rwxr-xr-x@ 1 Airths staff 80000 8 14 14:59 TargetMachO -rwxr-xr-x 1 Airths staff 86880 8 14 14:55 libCTool.dylib -rwxr-xr-x 1 Airths staff 86880 8 14 14:55 libDTool.dylib # 1.向 TargetMachO 注入 libCTool.dylib ~/Desktop/YololibDemo > yololib TargetMachO libCTool.dylib # 2.向 TargetMachO 注入 libDTool.dylib ~/Desktop/YololibDemo > yololib TargetMachO libDTool.dylib # 3.向 TargetMachO 注入 ATool.framework/ATool ~/Desktop/YololibDemo > yololib TargetMachO ATool.framework/ATool # 4.向 TargetMachO 注入 BTool.framework/BTool ~/Desktop/YololibDemo > yololib TargetMachO BTool.framework/BTool
使用 yololib 注入动态库之后,MachO 文件为:
由此可见:
使用 yololib 注入的动态库会按注入顺序以LC_LOAD_DYLIB
命令的形式拼接在原有 MachO 文件Load Commands
区域的末尾 -
使用 fishhook 进行简单的 hook 防护(Objective-C)
① 根据 Objective-C 中 +load 方法调用顺序大总结 对
+load
方法的调用顺序的介绍可知:
如果直接将防护代码写在主程序中,则进攻代码会优先于防护代码执行(即先进行 hook,再进行反 hook),从而导致 hook 防护失败② 根据 使用 yololib 注入的动态库执行的优先级最低 对注入动态库的执行顺序的介绍可知:
(主程序中所依赖的动态库)都优先于(使用 yololib 注入的动态库)加载综上 ① ② 两点所述:
为了使防护代码生效,需要将防护代码写在主程序所依赖的动态库中此外,Objective-C 的 RunTime 中能进行 hook 的函数都需要进行防护:
method_exchangeImplementations
、class_replaceMethod
、method_getImplementation
、method_setImplementation
-
攻克防护的思路
① 修改主程序 MachO,将注入的动态库的执行顺序提前到防护的动态库之前
② 因为 fishhook 重绑定的是进程中所有镜像的懒绑定指针表(
Lazy Symbol Pointers
)和非懒绑定指针表(Non-Lazy Symbol Pointers
)
又因为在 RunTime 函数所属的动态库libobjc.A.dylib
中,RunTime 函数属于导出给外部使用的符号,不存储在懒绑定指针表和非懒绑定指针表中
换句话说,位于libobjc.A.dylib
中的method_exchangeImplementations
、class_replaceMethod
、method_getImplementation
、method_setImplementation
等用于 hook 的函数的实现地址并没有被 fishhook 所替换
所以,可以在注入的动态库中,手动定位到位于libobjc.A.dylib
中的method_exchangeImplementations
、class_replaceMethod
、method_getImplementation
、method_setImplementation
等用于 hook 的函数的实现地址,并进行调用 -
相关 Demo
注意
-
关于 stub
我觉得
stub
(桩)其实应该翻译成存根会更好一点
因为在 MachO 文件中,stub
的作用就是一小段用于获取懒绑定指针(Lazy Symbol Pointer
)或者非懒绑定指针(Non-Lazy Symbol Pointer
)的代码。而在现实生活中,存根的作用也是用于获取存放在某处的东西 -
关于基地址
在 MachO 文件中,符号表(
Symbol Table
)、字符串表(String Table
)、间接符号表(Dynamic Symbol Table
)的偏移量都是相对于 MachO 文件的基地址(Base Address
),而不是相对于链接信息段(LinkEdit Segment
)的基地址 -
关于懒绑定指针表(Lazy Symbol Pointers)和非懒绑定指针表(Non-Lazy Symbol Pointers)
MachO 文件中采用地址无关代码技术(
Position-Independent Code
)对外部符号进行调用。当 MachO 文件调用dyld_stub_binder
、NSLog
、objc_release
等外部符号时,调用指令并没有与外部符号的实现地址进行绑定,而是会通过一小段stub
桩指令,获取位于数据段中的外部符号的实现地址,进而间接地调用外部符号如果外部符号是 MachO 加载和初始化过程中所必须的(比如
dyld_stub_binder
函数),那么在 dyld 初始化 MachO 镜像的阶段就会立即对该外部符号进行地址绑定,这种外部符号的地址存储在 MachO 文件数据段中的非懒绑定指针表(Non-Lazy Symbol Pointers
)里面如果外部符号不是 MachO 加载和初始化过程中所必须的(比如
NSLog
函数、objc_release
函数),那么在 dyld 初始化 MachO 镜像的阶段则不会对该外部符号进行地址绑定,这种外部符号的地址存储在 MachO 文件数据段中的懒绑定指针表(Lazy Symbol Pointers
)里面。懒绑定指针表(Lazy Symbol Pointers
)中存储的函数指针在未进行符号地址绑定时,默认都会指向stub_helper
节,而stub_helper
节作为辅助节,最终都会指向位于非懒绑定指针表(Non-Lazy Symbol Pointers
)中的dyld_stub_binder
。程序在运行过程中,当第一次调用外部符号时,会走到dyld_stub_binder
,然后进行一次真正的符号地址绑定,并将懒绑定指针表(Lazy Symbol Pointers
)中对应的函数指针从指向stub_helper
修改为符号的真实地址,从而实现懒绑定。当后续再调用该外部符号时,就会经由stub
桩指令,拿到位于懒绑定指针表(Lazy Symbol Pointers
)中的该符号的真实地址,不需要再走到dyld_stub_binder
总而言之,不管是懒绑定指针表(
Lazy Symbol Pointers
)还是非懒绑定指针表(Non-Lazy Symbol Pointers
),其本质都是一个用于存储外部符号地址的函数指针表,里面的元素都是一个个的函数指针。这些函数指针决定了当 MachO 文件调用外部函数时,应该跳转到哪个代码段的哪个位置去执行 -
fishhook 只能 hook 外部的 C 函数,不能 hook 内部的 C 函数
fishhook 是通过读取和修改当前 MachO 文件中的懒绑定指针表(
Lazy Symbol Pointers
)和非懒绑定指针表(Non-Lazy Symbol Pointers
)的方式来达到 hook 当前 MachO 文件的外部 C 函数的目的当前 MachO 文件的内部 C 函数在编译时已经进行了符号地址绑定,虽然当前 MachO 文件在调用内部 C 函数时仍然会采用地址无关代码技术,但是当前 MachO 文件在调用内部 C 函数时采取的是直接调用的方式。而不是将内部 C 函数的地址存储在懒绑定指针表或者非懒绑定指针表中,并使用 stub 桩代码进行间接调用
因此,fishhook 只能 hook 外部的 C 函数,不能 hook 内部的 C 函数
-
打印进程中所有镜像的信息
-(void)printImagesInfo { uint32_t count = _dyld_image_count(); for (uint32_t index = 0; index < count; index++) { const char * name = _dyld_get_image_name(index); intptr_t slide = _dyld_get_image_vmaddr_slide(index); const struct mach_header * header = _dyld_get_image_header(index); NSLog(@"index = %03d", index); NSLog(@"name = %s", name); NSLog(@"slide = 0x%lx", slide); NSLog(@"magic = 0x%x, cputype = 0x%x, cpusubtype = 0x%x, filetype = 0x%x, ncmds = %u, sizeofcmds = %u, flags = 0x%x", header->magic, header->cputype, header->cpusubtype, header->filetype, header->ncmds, header->sizeofcmds, header->flags); NSLog(@""); } }
-
向 dyld 注册镜像加载和卸载的回调
-(void)dyldRegisterCallback { _dyld_register_func_for_add_image(dyld_add_image_callback); _dyld_register_func_for_remove_image(dyld_remove_image_callback); } static void dyld_add_image_callback (const struct mach_header* header, intptr_t slide) { static int addIndex = 0; NSLog(@"addIndex = %03d", addIndex++); NSLog(@"magic = 0x%x, cputype = 0x%x, cpusubtype = 0x%x, filetype = 0x%x, ncmds = %u, sizeofcmds = %u, flags = 0x%x", header->magic, header->cputype, header->cpusubtype, header->filetype, header->ncmds, header->sizeofcmds, header->flags); NSLog(@"slide = 0x%lx", slide); NSLog(@""); } static void dyld_remove_image_callback (const struct mach_header* header, intptr_t slide) { static int removeIndex = 0; NSLog(@"removeIndex = %03d", removeIndex++); NSLog(@"magic = 0x%x, cputype = 0x%x, cpusubtype = 0x%x, filetype = 0x%x, ncmds = %u, sizeofcmds = %u, flags = 0x%x", header->magic, header->cputype, header->cpusubtype, header->filetype, header->ncmds, header->sizeofcmds, header->flags); NSLog(@"slide = 0x%lx", slide); NSLog(@""); }