打开姿势很重要
早些时候,iOS中一提到“黑魔法”、HOOK,很多人第一时间想到的就是 AOP RunTime MethodSwizzling 这些不明觉厉的东西,它们的基本用法其实都不难,真正难的是如何在合适的地方用好它们。
任何事物都有两面性,越强大其可能带来的隐患也越具有毁灭性。苹果提供的运行时机制固然大有用处,但如果在项目中滥用(更不是用来当做面试提升逼格的),很多时候只会适得其反,详细误区请参考iOS界的毒瘤-MethodSwizzling。
关于 MethodSwizzling 的用法在之前的文章中也有过讲解,请参考MethodSwizzling的几种姿势。 该方式更多的用于性能监测、 crash 的兼容和上报、反破解防护等一些工具的开发中,而在逆向中,在面对有相应安全防护措施的应用时,其用武之地比较有限。
无独有偶,“黑魔法”可不只有 RunTime ,今天我们来聊聊在逆向中常用的另一种HOOK方式:fishhook。
fishhook 背后的故事
(一)实现原理
fishhook 是 FaceBook 开源的可以动态修改 MachO 符号表的工具。fishhook 的强大之处在于它可以 HOOK 系统的静态 C 函数。
大家都知道 OC 的方法之所以可以 HOOK 是因为它的运行时特性,OC 的方法调用在底层都是 msg_send(id,SEL)的形式,这为我们提供了交换方法实现(IMP)的机会,但 C 函数在编译链接时就确定了函数指针的地址偏移量(Offset),这个偏移量在编译好的可执行文件中是固定的,而可执行文件每次被重新装载到内存中时被系统分配的起始地址(在 lldb 中用命令image List
获取)是不断变化的。运行中的静态函数指针地址其实就等于上述 Offset + Mach0 文件在内存中的首地址:


既然 C 函数的指针地址是相对固定且不可修改的,那么 fishhook 又是怎么实现 对 C 函数的 HOOK 呢?其实内部/自定义的 C 函数 fishhook 也 HOOK 不了,它只能HOOK Mach-O 外部(共享缓存库中)的函数。fishhook 利用了 MachO 的动态绑定机制(不清楚的同学看这里:MachO 文件结构详解、dyld背后的故事&源码分析 ):苹果的共享缓存库不会被编译进我们的 MachO 文件,而是在动态链接时才去重新绑定。苹果采用了PIC(Position-independent code)技术成功让 C 的底层也能有动态的表现:
- 编译时在 Mach-O 文件 _DATA 段的符号表中为每一个被引用的系统 C 函数建立一个指针(8字节的数据,放的全是0),这个指针用于动态绑定时重定位到共享库中的函数实现。
- 在运行时当系统 C 函数被第一次调用时会动态绑定一次,然后将 Mach-O 中的 _DATA 段符号表中对应的指针,指向外部函数(其在共享库中的实际内存地址)。
fishhook 正是利用了 PIC 技术做了这么两个操作:
- 将指向系统方法(外部函数)的指针重新进行绑定指向内部函数/自定义 C 函数。
- 将内部函数的指针在动态链接时指向系统方法的地址。
这样就把系统方法与自己定义的方法进行了交换,达到 HOOK 系统 C 函数(共享库中的)的目的。
(二)用汇编解析过程
为了更好的理解 fishhook 是如何 HOOK 系统的 C 函数,我们以 HOOK NSLog 为例,从汇编着手来一步步去分析,为大家扒开 fishhook 实现 HOOK 系统 NSLog 的全过程。
注:对于非懒加载符号表,dyld 会在动态链接时就链接动态库
对于懒加载符号表,dyld 会在运行时函数第一次被调用时动态绑定一次
NSLog 在懒加载表中
1.验证系统的动态绑定:
新建一个空工程,写下这两行代码:


在两个 NSLog 处分别加上断点,将工程 Run 起来,把 Debug -> Debug Workflow -> Always Show Disassembly 勾选上,用于查看汇编信息,断点断住后获取 MachO 在内存中的首地址:


- 拿到该指针当前保存的值,iOS 的 CPU 是小端序,当前机型为 64 位 CPU,所以倒序读 8 个字节就是指针的值:0x010b0f89a0
- dis -s 是反汇编命令,我们发现此时该指针指向的函数正在调用系统动态绑定的函数
- 进一步查看调用函数详细信息:libdyld.dylib`dyld_stub_binder
这是在干嘛?没错,这就是第一次调用 NSLog 时系统去重新绑定位懒加载符号表中 NSLog 对应的指针所指向的位置。
接下来我们过掉第一次断点,让断点断在第二个 NSLog 处,再次查看符号表中该指针(依然是 0x3028+0x000000010b0f7000 这个地址)所指向的地址,

2.验证 fishhook 的重绑定:
我们将 fishhook文件拖入工程,并添加一个简单的绑定:

我们运行起来之后点击屏幕进入上图所示断点,查看符号表中原本指向系统 NSLog 的指针指向:



(三)fishhook 是如何根据字符串对应在符号表中的指针,找到其在共享库的函数实现的?
fishhook 官方给了这张图:

- 在 Lazy Symbol Pointers 中该字符串的顺序就是其在 Dynamic Symbols Table -> Indirect Symbols 表中的位置(这里是第一个)
在实际计算地址中用到了 Load Commands 中对应头信息的 Reserved1 的 value (section基地址+ 偏移量 value = 其在 Indirect Symbols 中对应的 offset):下篇源码分析有详细说明。 - Dynamic Symbols Table -> Indirect Symbols 表中的第一个对应的 Data 值(0x7A=122)就是其在Symbols Table -> Symbols 中的索引。
- 在 Symbols Table -> Symbols 中索引为 122 的位置对应的 Data = 0x9B:
- 上表中的 Data(0x9B) + String Table的起始地址(0x4F04)就是目标函数实现的地址:
总结
今天我们结合 iOS 的共享缓存库中采用的 PIC 技术,介绍了 fishhook 对系统外部函数实现 HOOK 基本原理和具体过程,并通过反汇编命令一一验证了 iOS 的动态绑定过程和 fishhook 的重新绑定机制,最后把 fishhook 在符号表中根据符号指针寻找函数实现的步骤做了演示。 愿你有所收获! 水平有限,请多指教~
鉴于篇幅过长会影响大家的阅读体验,fishhook 的源码分析与应用场景以及安全防护的分享,我们这里继续。