在上篇文章不知 MachO 怎敢说自己懂 DYLD https://www.jianshu.com/p/95896fb96a03 中已经详细介绍了 MachO, 并且由 MachO 引出了 dyld, 再由 dyld 讲述了 App 的启动流程, 而在 App 的启动流程中又说到了一些关键的名称如: LC_LOAD_DYLINKER,LC_LOAD_DYLIB 以及 objc 的回调函数_dyld_objc_notify_register 等等. 并且在末尾提出了 MachO 中还有一些符号表, 而有哪些符号表, 这些符号表又有些什么用呢? 笔者在这篇文章就将一一道来.
老规矩, 片头先上福利: 点击下载 demo https://github.com/dengbin9009/FishhookDemo ,demo 中有笔者给 fishhook 每句代码加的详细注释!!! 这篇文章会用到的工具有:
https://github.com/facebook/fishhook
在开始正文之前, 假设面试官问了一个问题:
都知道 Objective-C 最大的特性就是 runtime, 大家可以用使用 runtime 对 OC 的方法进行 hook, 那么 C 函数能不能 hook?
有兴趣回答的朋友可以先行在评论区回答, 答完之后再继续阅读或者预先偷窥一下文末的答案, 看看这被炒了无数次冷饭的 runtime 自己是否真的了然于胸.
本将从以下几方面回答上面所提的问题:
Runtime 的 Hook 原理
为什么 C 不能 hook
如何利用 MachO"玩坏" 系统 C 函数
fishhook 源码分析
绑定系统 C 函数过程验证
一, Runtime 的 Hook 原理
Runtime, 从名称上就知道是运行时, 也是它造就了 OC 运行时的特性, 而要想彻底明白什么是运行时, 那么就需要将之与 C 语言有相比较.
今天咱们就从汇编的角度看一看 OC 和 C 在调用方法 (函数) 上有什么区别.
注: 笔者使用的是 iPhone 7 征集调试, 所有一下汇编都是基于 arm64, 所以以下所有汇编默认为基于 arm64.
新建一个工程取名为: FishhookDemo
敲入两个 OC 方法 mylog 和 mylog2, 挂上断点, 如图:
开启汇编断点, 如图:
运行工程, 会跳转到如下图的汇编断点:
从上图可以看的出来调用了两个 objc_msgSend, 这两个很像是 我们的 mylog 和 mylog2, 但现在还不能确定.
想一想 objc_msgSend 的定义:
- OBJC_EXPORT void
- objc_msgSend(void /* id self, SEL op, ... */ )
- OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
第一个参数是 self, 第二个参数是 SEL, 所以可以知道 SEL 是放在 x1 的寄存器里面(什么是 x1? 继续关注作者, 之后的文章会有相关的汇编的专门篇章).
马不停蹄, 挂上两个汇编断点, 查看一下两个 x1 中存放的到底是什么, 如图:
这也就验证了咱们 OC 方法都是消息转发(objc_msgSend). 而同一个 C 函数的地址又都是一样的(笔者这次运行的地址就是 0x1026ce130) .
所以在每次调用 OC 方法的时候就让我们有了一次改变消息转发「目标」的机会.
这里稍微提一下 runtime 的源码分析流程: Step 1, 方法查找 1 汇编快速查找缓存 2 C/C++ 慢速查找: self->super->NSObject->找到换缓存起来 Step 2, 动态方法解析: _class_resolveMethod 1 _class_resolveInstanceMethod 2 _class_resolveClassMethod Step 3, 消息转发 1 _forwardingTargetForSelector 2 _methodSignatureForSelector 3 _forwardInvocation 4 _doesNotRecognizeSelector
二, 为什么 C 不能 hook
同样我们从汇编的角度切入.
敲入代码一些 C 函数, 挂上断点, 如图:
运行工程:
会看到断点断到如下汇编:
可以看到每个 NSLog 对应跳转的地址都是 0x10000a010, 每个 printf 对应跳转的地址都是 0x10000a184, 也就是说每个 C 的函数都是一一对应着一个真实的地址空间. 每次在调用一个 C 函数的时候都是执行一句汇编 bl 0xXXXXXXXX.
所以上面讲述到的消息转发的机会没有了, 也就是没有了利用 runtime 来 Hook 的机会了.
三, 如何利用 MachO"玩坏" 系统 C 函数
既然如此, 那么是否 C 函数就真的那么牢不可破, 无法对他进行 Hook 呢?
答案肯定是否定的!
想要从根上理解这个问题, 首先要了解: 我们的 C 函数分为系统 C 函数和我们自定义的 C 函数.
1, 自定义的 C 函数
在上面的步骤中我们已经了解到所有 C 函数的调用都是跳转到一个「固定的地址」, 那么就可以推断得出这个「固定的地址」其实是在编译期已经被生成好了, 所以才能快速, 直接的跳转到这个地址, 实现函数调用.
C 语言被称之为是静态语言也就是这么个理.
2, 系统的 C 函数
在上篇文章不知 MachO 怎敢说自己懂 DYLD https://www.jianshu.com/p/95896fb96a03 已经提到了在 dyld 启动 App 的第二个步骤就是加载共享缓存库, 共享缓存库包括 Foundation 框架, NSLog 是被包含在 Foundation 框架的. 那么就可以确定一件事情, 在我们将自己工程打包出的 MachO 文件中是不可能预先确定 NSLog 的地址的.
但是又因为 C 语言是静态的特性, 没法在运行的时候实时获取共享缓存库中 NSLog 的地址. 而共享缓存库的存在好处太大, 既能节省大量内存, 又能加快启动速度提升性能, 不能弃之而不用.
为了解决这个问题, Apple 使用了 PIC(Position-independent code)技术, 在第一次使用对应函数 (NSLog) 的时候, 从系统内存中将对函数 (NSLog) 的内存地址取出, 绑定到 App 中对应函数 (NSLog) 上, 就可以实现正常的 C 函数 (NSLog) 调用了.
既然有这么个过程, iOS 系统可以动态的绑定系统 C 函数的地址, 那么咱们就也能.
四, fishhook 源码分析
1,fishhook 的总体思路
Facebook 的开源库 https://github.com/facebook/fishhook 就可以完美的实现这个任务.
先上一张官网原理图:
总体来说, 步骤是这样的:
先找到四张表 Lazy Symbol Pointer Table,Indirect Symbol Table,Symbol Table,String Table.
MachO 有个规律: Lazy Symbol Pointer Table 中第 index 行代表的函数和 Indirect Symbol Table 中第 index 行代表的函数是一样的.
Indirect Symbol Table 中 value 值表示 Symbol Table 的 index.
找到 Symbol Table 的中对应 index 的对象, 其 data 代表 String Table 的偏移值.
用 String Table 的基值, 也就是第一行的 pFile 值, 加上 Symbol Table 的中取到的偏移值, 就能得到 Indirect Symbol Table 中 value(这个 value 代表函数的偏移值)代表的函数名了.
1, 验证 NSLog 地址
下面就来验证一下在 NSLog 的地址是不是真的就存在 Indirect Symbol Table 中. 同样在 NSLog 处下好断点, 打开汇编断点, 运行代码. 会发现断点断在如下入位置:
注: 笔者的工程重新 build 了, MachO 也重新生成, 所以此处的截图和上文中断住 NSLog 的截图的地址不一样, 这是正常情况.
可以发现 NSLog 的地址是 0x104d36010, 先记住这个值.
然后查看我们 App 在内存中的偏移值.
利用 image list 命令列出所有 image, 第一个 image 就是我们 App 的偏移值, 也就是内存地址.
可以看到 App 在内存中的偏移值为 0x104d30000.
接着打开 MachOView 查看 MachO 中的 Indirect Symbol Table 中的 value, 如图:
其值为 0x100006010, 去除最高位得到的 0x6010 就是 NSLog 在 MachO 中的偏移值. 最后将 NSLog 在 MachO 中的偏移值于 App 在内存中的偏移值相加就得到 NSLog 真实的内存地址:
0x6010+0x104d30000=0x104d36010
最终证明, 在 Indirect Symbol Table 的 value 中的值就是其对应的函数的地址!!!
2, 根据 MachO 的表查找对应的函数名和函数地址
咱们还是用 NSLog 来距离查找.
1,Indirect Symbol Table
取出其 data 值 0000010A, 用 10 进制表示, 结果为 266, 如图:
2,Symbol Table
在 Symbol Table 中找到下标 (offset) 为 266 的的对象, 取出其 data0x124, 如图:
2,String Table
将在 Symbols 中得到的偏移值 0x124 加上 String Table 的首个地址 DC6C, 得到值 DD90, 然后找到 pFile 为 DD90 的值, 如下两图:
上述就是根据 MachO 的表查找对应的函数名和函数地址全过程了.
3, 源码分析
fishhook 的源码总共只有 250 行左右, 所以结合 MachO 慢慢看, 其实一点也不费劲, 在笔者的 demo https://github.com/dengbin9009/FishhookDemo 中有对其每一句函数的详细注释. 当然也有对 fishhook 使用的 demo.
所以笔者就不在此处对 fishhook 做太过详细的介绍了. 只对其中一些关键参数和关键函数做介绍.
fishhook 为维护一个链表, 用来储存需要 hook 的所有函数
- // 给需要 rebinding 的方法结构体开辟出对应的空间
- // 生成对应的链表结构(rebindings_entry), 并将新的 entry 插入头部
- static int prepend_rebindings(struct rebindings_entry **rebindings_head,
- struct rebinding rebindings[],
- size_t nel)
根据 linkedit 的基值, 找到对应的三张表: symbol_table,string_table 和 indirect_symtab :
- // 找到 linkedit 的头地址
- uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
- // 获取 symbol_table 的真实地址
- nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
- // 获取 string_table 的真实地址
- char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
- // Get indirect symbol table (array of uint32_t indices into symbol table)
- // 获取 indirect_symtab 的真实地址
- uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
最核心的一个步骤, 查找并且替换目标函数:
- // 在四张表 (section,symtab,strtab,indirect_symtab) 中循环查找
- // 直到找到对应的 rebindings->name, 将原先的函数复制给新的地址, 将新的函数地址赋值给原先的函数
- static void perform_rebinding_with_section(struct rebindings_entry *rebindings,
- section_t *section,
- intptr_t slide,
- nlist_t *symtab,
- char *strtab,
- uint32_t *indirect_symtab)
五, 绑定系统 C 函数过程验证
上面说了这么多, 那么咱们来验证一下系统 C 函数是不是真的会这样被绑定起来, 并且看一看, 是在什么时候绑定的.
同样, 在第一次敲入 NSLog 函数的地方加上断点, 在第二个 NSLog 处也加上断点:
运行工程后, 使用 dis -s 命令查看该函数的汇编代码, 并且继续查看其中第一次 b 指令, 也就是函数调用的汇编, 如图:
从上图就可以看到, 在我们第一次调用 NSLog 的时候, 系统确实会默认的调用 dyld_stub_binder 函数对 NSLog 进行绑定.
继续跳过这个断点, 进入下一个 NSLog 的汇编断点处, 同样利用 dis -s 命令查看该汇编:
得到答案:
系统确实会在第一次调用系统 C 函数的时候对其进行绑定!
还记得正文开始的时候的那个问题吗?
那么是不是系统 C 函数可以 hook, 而自定义的 C 函数就绝对不能 hook 了呢?
很显然, 国内外大神那么多, 肯定是能做到的, 有兴趣的读者可以自行查阅 Cydia Substrate.
这篇文章利用了一些 LLDB 命令行看了许多我们想看的内容, 如 image list,register read 还有 dis -s, 在我们正向开发中, LLDB 就是一把利器, 而在我们玩逆向的时候, LLDB 就成为了我们某些是后的唯一途径了! 所以, 在下一篇文章中, 笔者将会对 LLDB 进行更加详细的讲解, 让大家看到 LLBD 的伟大.
来源: https://juejin.im/post/5c969a595188252d554204a5