11 KiB
fishhook 原理
hook 分类
- Method Swizzle:利用 OC runtime,动态改变 SEL(方法编号)、IMP(方法实现)的对应关系,达到 OC 方法调用流程改变的目的,主要用于 OC 方法
- fishhook:是 Facebook 提供的一个动态修改链接 Mach-o 文件的工具,利用 Mach-O 文件加载原理,通过修改懒加载和非懒加载2个表的指针,达到 c 函数 hook 的目的。
- Cydia Substrate: 原名为 Mobile Substrate,主要作用是针对 OC 方法、C 函数以及函数地址进行 hook,当然并不是仅针对 iOS 而设计,Android 也可使用。官方地址:http://www.cydiasubstrate.com
hook只有二种:
inline hook:直接修改函数入口代码或函数内某处代码跳转到自己代码- 地址替换:包含入口表地址替换、出口表地址替换、结构体内地址替换等。这类最简单但不一定有效,不通过地址表的调用 hook 不到
应用
经常会遇到 hook oc 方法,但是遇到像 NSLog、objc_msgSend 等方法的时候 OC Runtime 就不满足了 ,有了 fishhook 神器,hook “c 函数”已不是难题。
为什么对 “c 函数”加了引号,带着问题往下看
Hook 系统 c 函数
以 NSLog 为例。
可以看到 hook 成功了。
struct rebinding {
const char *name; // 需要 hook 的函数名称,c 字符串
void *replacement; // 新函数地址
void **replaced; // 原始函数地址的指针
};
Hook 自定义 c 函数
自定义一个 c 函数 handleTouchAction ,发现没有 hook 成功。
不禁令人好奇,同样是 c 函数,为什么系统 c 函数可以 hook,自定义的 c 函数无法 hook?带着问题继续探究
原理窥探
FishHook 是 FaceBook 提供的一个可以动态修改链接 Mach-O 文件的工具。利用 Mach-O 文件的加载原理,通过修改懒加载和非懒加载2个表的指针,达到 hook 系统 C 函数的目的。
Mach-O 文件权限
Mach-O 分为代码段、数据段...:
- 代码段:可读、可执行、不可写
- 数据段:可读、可写、不可执行
系统共享缓存
我们知道 NSLog 的函数实现在 Foundation 库中,而我们开发自己写的其他函数则在自身可执行文件中,也就是 Mach-O。
iOS 共享缓存:在iOS 3.1及以后的版本中,Apple 将系统库文件打包合并成一个大的缓存文件,存放在 /System/Library/Caches/com.apple.dyld/ 目录下,以减少冗余并优化内存使用
- 所有的进程均可访问
- 缓存文件的架构特定性:共享缓存文件根据不同的处理器架构有不同的版本,例如
dyld_shared_cache_arm64针对的是 ARM 64 位架构的处理器 - 动态库的加载优化:通过共享缓存,iOS 系统可以在程序运行时更高效地加载动态库,因为不需要每个应用程序重复加载相同的库文件,从而加快了启动速度并提高了性能
App 对应的 Mach-O 被 dyld 装载进内存的时候,NSLog 地址还不确定,其位于 Foundation 框架中,也就是位于共享缓存中。
这带来一个问题:代码在 Mach-O 文件上策马奔腾,在愉快的执行,当遇到 NSLog 的时候,该如何确定位于 Foundation 框架中 NSLog 真实函数地址呢?编译阶段,clang 可以知道任何设备(iPhone14、iPhone6s)任何架构(arm64、armv7)上 Foundation 真实的内存吗?显然不可能。
这里稍微展开谈谈静态链接和动态链接。
链接分为静态链接和动态链接。早期计算机都是采用静态链接这种方式的。静态链接存在缺点:
-
对于计算机内存和磁盘浪费很严重。想象下,每个程序内部保留了 printf、scanf 等公用库函数,还有很多其他库函数和所需要的数据结构。
-
程序的开发和发布很不方便。比如应用 A,使用的 Lib.o 是一个第三方厂商提供的,当 lib.o 修复 bug 或者升级,开发都需要将应用 A 重新链接再发布,整个周期很不方便。
要解决空间浪费和更新困难最简单的办法就是把程序的模块拆分,形成独立文件,而不再将他们静态地链接在一起。而是等到程序运行起来才进行链接,也就是动态链接。
动态链接涉及运行时的链接以及多个文件的装载,必须有操作系统级别的支持。此时还有个角色叫做动态链接库。所有应用都可以在运行时使用它。
程序与 lib 动态库之间的链接工作是由动态链接器完成的,而不是静态链接器 ld 完成的。也就是动态链接是把链接这个过程由程序装载前被推迟到了装载的时候。
但也带来了坏处,因为都是程序每次装载的时候进行重新链接。有解决方案,叫做延迟绑定(Lazy binding),可使得动态链接对性能的影响减的最小。据估算,动态链接相比静态链接,存在大约5%的性能损失,但换来程序在空间上的节省和程序构建和升级的灵活性,是值得的。
如何解决?
编译阶段给 NSLog 一个默认地址,在应用启动时,通过重定向,把真正的地址重新写入到 Mach-O 中,但效率很低。 后来诞生了 PIC 技术
PIC 技术
装载时重定位是解决动态模块中有绝对地址引用的方案之一。但其存在一个很大缺点,是指令部分无法在多个进程间共享,这样就是失去动态链接节省内存这一优势。
我们还需要一种更优雅的方案,希望程序模块中的共享指令部分在装载时不需要因为装载地址改变而改变,所以实现的基本想法就是把指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本,这个方案就是 地址无关代码(Position Independent Code)PIC 技术。
Position-Independent Code,即位置无关代码。这是一种编译技术,允许生成的代码在内存中的任何位置执行,而不需要任何修改。这在动态链接库(dylib)中非常重要,因为它允许系统动态地将库加载到内存中的不同位置,而不需要重新链接。
优点是:
- 共享代码:多个应用程序可以共享同一个动态库的实例,节省内存并减少启动时间。
- 动态链接器(dyld)可以优化符号绑定过程,提高应用程序的启动速度
- 支持代码重定位,使得应用程序更新和修补更加灵活
写的业务代码里面假如某一行调用了 NSLog,那么在编译阶段,使用 NSLog 只是 IDE 提供了功能,让你可以看到声明而已。编译后的可执行文件,还是不知道 NSLog 的具体函数地址。这个底层是如何工作的呢?
有了 PIC 之后,工作流程为:
-
编译期,在 Mach-O 中数据段生成一块区域,该区域叫符号表。数据段可读可写。
工程中所有引用了动态库共享缓存区中的系统符号,其指向的地址设置成符号地址。比如工程中 NSLog,那么编译时就会在 Mach-O 中创建一个 NSLog 符号,工程中的 NSLog 就指向这个符号
-
dyld 加载 Mach-O,做符号绑定。
当 dyld 将 Mach-O 加载到内存中时,读取 header 中 load command 信息,找出需要加载哪些库文件,去做绑定的操作。比如 dyld 会找到 Foundation 中的 NSLog 的真实地址写到 _DATA 段的符号表中 NSLog 对应的符号上。
实践探索
做个实验验证下,完整流程
第一步:可以看到 NSLog 位于 Lazy Symbol Pointers 里的第一个。lazy 说明只有在用到的时候才去绑定。下断点验证下
第二步:在 NSLog 处打断点,触发后 LLDB 模式下 image list 查看所有的 image。可以看到第一个 image 就是 App 主程序镜像,且 image 开始地址为 0x0000000100da5000
第三步:进入 LLDB 模式,根据 image base 地址 + offset 计算 NSLog 的地址。即 memory read 0x0000000102eec000+0xC000,查看下内存信息
第四步:断点过下一行,即执行过一次 NSLog。然后再通过 LLDB 根据地址查看汇编代码,dis -s addr 查看
第五步:继续过断点,等执行完 rebind_symbols 再看看内存信息。可以看到再 rebind 之后,地址变了。然后根据地址查看汇编代码,发现已经是我们自定义的函数了。
第一步: 在 Lazy Symbol Pointers 懒加载符号表中看到第一个符号 NSLog,索引为1。
第二步:根据索引,在 Dynamic Symbol Tables 动态符号表中看到第一条数据,是 NSLog 相关的。其 Data 值 00000084 是十六进制的,换算为十进制就是 132。
第三步:根据第二步得到的角标,在 Symbol Table 符号表中查找第132个位置。可以看到其 Data 值 000000AA 是偏移值。
第四步:在 String Table 中,第一个位置 0000CFE4 加上偏移值 0xAA ,等于 0xD08E,如下图所示,就是 NSLog 符号真实的地址。
NSLog、dispatch_once 等,stub 代码指向 lazy Symbol Pointers 部分,lazy Symbol Pointers 中又指向 stub_helper,默认又到了 dyld_stub_binder,在首次调用时再绑定真实调用地址。
fishhook 正是利用上面这点,将 lazy Symbol Pointer 中的符号替换成自己的函数,从而实现 hook,这也是为什么 fishhook 不能 hook 二进制文件中自定义的 C 函数
fishhook 做的事情就是将系统的符号表,将符号表中的特定符号对应的地址,修改为自定义的函数地址。起到了 hook 作用。也就是说外部的 c 函数,在 iOS 中的调用属于动态调用。
知道了 fishhook 的工作原理,我们就知道 fishhook 是 hook 不了自己写的 c 函数了。因为自定义函数是没有通过符号去寻找函数真正地址的这个过程。而系统库是通过符号去绑定真实地址的。