fishhook & PIC
资料说明
原仓库:
地址: https://github.com/facebook/fishhook (最后更新时间是 2020.04.21)
目前
main分支的代码在 iOS 14.7 真机上(arm64)运行会 crash ,在模拟器上(x86_64)能正常运行。
我 fork 的仓库:
改动点:
合入了 pull/87,解决了在 iOS 14.7 真机上 crash 的问题;
添加了 fishhook-Example 工程,包含 hook
open(),close(),printf(),NSLog()的示例;整理了 README.md 的格式,方便阅读。
简介
fishhook 是一个非常简单的库,支持对 iOS 模拟器和真机上(实际上 macOS 平台也支持)运行的 Mach-O 二进制文件进行动态地重绑定符号 (dynamically rebinding symbols) 。这个功能和 macOS 中的 DYLD_INTERPOSE 类似。
在 Facebook ,开发者使用 fishhook 来 hook libSystem 中的调用以进行调试、追踪(比如对文件描述符被关闭两次的问题进行审计)。
DYLD_INTERPOSE 使用示例
源码:https://opensource.apple.com/source/dyld/dyld-852.2/include/mach-o/dyld-interposing.h.auto.html
上文提到的 DYLD_INTERPOSE 实际上是 dyld 中的一个宏(宏的结尾包含了分号,调用的时候不用再加分号了):
源码中给出的示例是使用自定义的 my_open() 替换 open() 函数 :
fishhook 使用示例
示例一:重绑定 open() 和 close()
open() 和 close()输出示例:
示例二:重绑定 printf()
printf()输出示例:
注意:在实现 my_printf 时,需要使用 va_start 和 va_end 取出 printf() 的第二个参数(这是个变长参数),然后存入到 va_list 类型的变量中,最后传递给 vprintf 函数的第二个参数。可参考:
GNU
glibc的printf.chttps://code.woboq.org/userspace/glibc/stdio-common/printf.c.htmlApple
libc的printf.c:https://opensource.apple.com/source/Libc/Libc-1439.100.3/stdio/FreeBSD/printf.c.auto.html
示例三:重绑定 NSLog()
NSLog()输出示例:
调用动态库中的 C 函数的不同之处
提问:调用动态库中的 C 函数与调用自己源码中的 C 函数有何不同?
示例一:调用动态库中的 C 函数
源码:https://github.com/Huang-Libo/fishhook/blob/main/Symbol-Example-1/HelloWorld.c
这里以 C 标准库中的 printf() 函数的调用为例,演示源码中引用的动态库中的函数的调用方式。
先看一段简单的 C 代码,在 main() 函数中只调用了 printf() 函数:
使用 clang 编译,生成可执行文件 a.out :
nm -n a.out 输出:
可以看出 _printf 符号类型是 undefined ;此外,还有一个名为 dyld_stub_binder 的符号也是 undefined 类型,这个符号稍后介绍。
符号表查看工具 - nm 命令简介:
nm 命令可列出 mach-o 文件中的符号 (list symbols from object files) 。可在终端中使用 man nm 查看其文档。
nm 的输出包含 3 列:
第 1 列是 The symbol value ,即符号的地址(未加偏移量的相对地址),默认使用 16 进制;
第 2 列是 The symbol type ,即符号的类型;
U:表示undefined,即未定义,因此没有对应的地址;T:表示符号位于__TEXT段,即代码所在区域;d:表示符号在已初始化的数据区;
第 3 列是 The symbol name ,即符号的名称。
示例二:调用自己源码中的 C 函数
源码:https://github.com/Huang-Libo/fishhook/blob/main/Symbol-Example-2/Symbol-Example/main.c
接下来在上述源码中添加一个 my_hello 函数:
调用 clang HelloWorld.c 重新编译后,再输入 nm -n a.out 查看 a.out 中的符号列表:
C 函数对应的符号名,是在函数名前加一个下划线。
可以看到我们自定义的 my_hello 函数对应的符号 _my_hello 是有地址的,且在 __TEXT 段中。
小结
自己源码中的 C 函数在编译时就确定了函数地址(未加偏移量的相对地址),而动态库中的 C 函数在编译时没有确定函数地址。
1. 使用 Hopper 探索 printf 的调用流程
接下来对上述源码生成的 Mach-O 进行详细分析。
_main
使用 Hopper 打开 Symbol-Example 项目生成的可执行文件。入口是位于 (__TEXT,__text) 的 _main :

在 _main 中可以看到在 0x100003f5f 地址上执行了 call ,对应的符号是 imp___stubs__printf ,注释是 printf ,说明这一行汇编对应的就是 main() 函数内的 printf() 函数调用 :
imp___stubs__printf
imp___stubs__printf双击 imp___stubs__printf 跳入其定义中:

可看到它位于 (__TEXT,__stubs) ,入口地址是 0x100003f72 ,在其内出现了新的符号 _printf_ptr :
_printf_ptr
双击 _printf_ptr ,跳入其定义中:

可看到它位于 (__DATA,__la_symbol_ptr) 中,它的内部存储的是 Lazy Symbol Pointer ,也就是说这里面存储的符号在第一次被调用时才执行绑定。
可看到其内有一个 extern 的 _printf 符号:
编者注:感觉 Hopper 生成的汇编中的 (__DATA,__la_symbol_ptr) 内少了一些数据,导致 _printf 的调用链路断在后面将讲到的外部符号所在区域了。实际上,用 MachOView 查看 (__DATA,__la_symbol_ptr) ,可以看到 _printf 符号还有个属性是 Data ,其值是 0x100003F88 ,这个地址位于 (__TEXT,__stub_helper) 内,这个地址值很重要,通过这个地址值,就能把 _printf 和 dyld_stub_binder 关联起来了,稍后将详细介绍。

_printf
双击 _printf ,会跳入到其定义:

这里显示的是外部符号 (External Symbols) , 在 Hopper 生成的汇编中,printf() 函数的调用链路就断在这里了,如之前所述,应该是因为 (__DATA,__la_symbol_ptr) 内有些信息没有显示。
从地址值上看,外部符号位于所有符号的最后面(疑问❓:在 MachOView 中没有这个专门展示外部符号的地方,这两个地址值 0x100014000 和 0x100014008 也较大,在 MachOView 中没有显示相应的区域):

这两个外部符号对应的汇编是:
_printf 和 dyld_stub_binder 的注释分别是:
从注释中可看出:
1)这两个符号都来自 /usr/lib/libSystem.B.dylib 。
2)printf() 的调用流程是:
3)dyld_stub_binder 的调用流程是:
_printf 和 dyld_stub_binder 是强相关的,但根据目前的线索还看不出来它俩的联系。
接下来先详细查看 dyld_stub_binder 这个外部符号的调用流程。
(__TEXT,__stub_helper)
(__TEXT,__stub_helper)顺着上面的外部符号 dyld_stub_binder 的注释给出的地址 0x100003f81 ,可在 (__TEXT,__stub_helper) 中可以看到这一行出现了新符号 dyld_stub_binder_100004000 :

在 0x100003f81 左侧有一个蓝色箭头指向下方,实际上就是指向的 dyld_stub_binder_100004000 。
另外,在左侧可以看到一个红色箭头。在上图的地址中,我们再次看到了上文提到的 0x100003f88 ,这个地址也就是 _printf 符号中的 Data 字段存储的值。在 0x100003f88 执行了 push 指令后,接着执行了 jmp 指令跳转到开头处 0x100003f78。
在 0x100003f78 出现了新符号 __dyld_private ,暂不讨论。最后会执行 0x100003f81 中的指令,跳转到 dyld_stub_binder_100004000 符号所在地址。
上图中的汇编:
dyld_stub_binder_100004000
dyld_stub_binder_100004000后面的100004000实际上是dyld_stub_binder在当前 Mach-O 中的地址值,在别的 Mach-O 中会是其它值。
双击 dyld_stub_binder_100004000 跳入到其定义中,可看到它位于 (__DATA,__got) 中,它的内部存的是 Non-Lazy symbol pointer ,也就是应用在启动的 pre-main 阶段就会被绑定的符号:

dyld_stub_binder
双击 dyld_stub_binder 跳入其定义中,就来到了老地方,External Symbols :

小结
综上所述,我们可以得出一个重要结论:在 Mach-O 中,_printf 符号指向的是 __stub_helper 区域,在执行一系列指令后,最终指向了 dyld_stub_binder 符号。
printf() 函数第 1 次调用时的流程:
dyld_stub_binder 是 dyld 中的一个辅助函数,职责是绑定外部符号。比如,外部符号 _printf 在 (__DATA,__la_symbol_ptr) 中的 Data 初始值是 0x100003f88 ,也就是说 _printf 最初指向的是 (__TEXT,__stub_helper) 内的 0x100003f88,在调用一系列指令后,最终调用了 dyld_stub_binder 。
dyld_stub_binder 会去内存中查找 _printf 符号的实际地址,找到后将 (__DATA,__la_symbol_ptr) 中 _printf 的 Data 值由 0x100003f88 替换为 _printf 的实际地址,下次调用 _printf 时,就能直接调用其函数的实现,而无需再调用 dyld_stub_binder 。
printf() 函数第 n 次 (n >= 2) 调用时的流程:
2. 使用 MachOView 探索 printf 的调用流程
上一节使用 Hopper 对编译生成的 Mach-O 文件进行详细分析,接下来再用 MachOView 分析一遍,大多数时候可以将这两个工具结合起来使用。
说明:在 Debug 环境中加载 Mach-O 时,Mach-O 的偏移量是固定值 0x100000000(可在 lldb 中使用 image list 查看 image 的起始值)。
(疑问❓:为何 Mach-O 内部的有些地址值也加上了 0x100000000 ?Mach-O 中的地址应该是不需要加偏移量的吧?)
(__TEXT,__text)
(__TEXT,__text)先查看 (__TEXT,__text) 中的汇编代码,在 0x3F5F 地址中调用了 callq ,对应的地址是 0x3F72 。

(__TEXT,__stubs)
(__TEXT,__stubs)0x3F72 位于 (__TEXT,__stub_helper) ,这个 section 存储的是所有的符号桩。(疑问❓:调用链断在这里了,根据之前在 Hopper 中的分析,接下来应该要跳转到 (__DATA,__la_symbol_ptr) 区域中。另外,这里的 Data 中存在的 0xFF2588400000 是什么值?)

(__DATA,__la_symbol_ptr)
(__DATA,__la_symbol_ptr)在 (__DATA,__la_symbol_ptr) 中,可以看到 _printf 对应条目的 Data 值是 0x100003F88 。

(__TEXT,__stub_helper)
(__TEXT,__stub_helper)0x100003F88 位于 (__TEXT,__stub_helper) ,在执行 pushq 指令之后,接着执行 jmp 指令跳转到 0x3F78 ,最终将执行 0x3F81 中的 jmp 指令。
在之前的 Hopper 分析中,我们可以看到 0x3F81 实际上是跳转到了 (__DATA_CONST,__got) 的 dyld_stub_binder 。但在 MachOView 中不那么直观。
0x3F81 的汇编指令(有点看不懂 😅 ):
这条指令应该是在计算 dyld_stub_binder 符号的地址。在使用 Xcode 调试时,汇编代码中有相关注释,详情请看后面的章节。

(__DATA_CONST,__got)
(__DATA_CONST,__got)最后来到了 (__DATA_CONST,__got) ,这个 section 存储的是 Non-Lazy Symbol Pointers ,也就是启动时就会绑定的符号。 dyld_stub_binder 就位于这个区域。

小结
这一节使用 MachOView 追溯了 printf() 函数的调用流程,中间有些调用链不太明确,需要结合之前在 Hopper 中的找到的信息来追溯。
Hopper 和 MachOView 的对比:
使用 Hopper 查看函数的调用流程很方便,双击就能执行跳转,且生成的汇编代码更易读。
MachOView 的包含一些 Hopper 没有的信息,但生成的汇编代码可读性略差。可以把它们结合起来使用。
3. 使用 Xcode GUI 探索 printf 的调用流程
Xcode GUI 中调试汇编代码的技巧
要在 Xcode 中打断点时查看对应的汇编代码,需要勾选 Always Show Disassembly :

在汇编中调试的技巧:按住 Control 键再点击调试按钮,就能以汇编指令为单位进行调试了。
1)单步,跳到下一个汇编指令 (Step over Instruction) :
说明:在 lldb 中输入 si 也可以。

2)跳入汇编指令的方法调用 (Step into Instruction) :

第一次调用 printf 的流程
在 main() 函数调用 printf() 的地方打断点,运行项目后就能断在对应的汇编代码中。然后单步执行到 0x100003f5f ,可以看到这一行汇编调用了 callq,对应的地址是 0x1003f72 ,注释是 symbol stub for: printf 。由之前 Hopper 和 MachOView 中的分析也可得知,这个地址位于 (__TEXT,__stubs) ,存储的是符号桩:

执行 step into instruction ,可看到 printf 的内容,汇编指令是 jmpq *0x4088(%rip) ,虽看不太懂 😅 ,但后面的注释出现了一个熟悉的地址 0x100003f88 :

再跳入 0x100003f88 ,由之前 Hopper 和 MachOView 中的分析也可得知,这个地址位于 (__TEXT,__stub_helper) ,且最终会调用到 dyld_stub_binder 。这里又有一个 jmp 指令,地址是 0x100003f78 :

跳入 0x100003f78 ,可以看到在地址 0x100003f81 中的汇编代码是 jmpq *0x79(%rip) ,后面的注释给出了一个以 0x7fff 开头的很大的地址,并注明是 dyld_stub_binder :

再跳入这个地址中,可以看到这里是 dyld_stub_binder 的实现,由开头的 libdyld.dylib`dyld_stub_binder 可以看出,dyld_stub_binder 属于 libdyld.dylib 这个动态库:

上面已探索到 dyld_stub_binder 的实现了,这时我们需要 step out ,看看执行完 dyld_stub_binder 之后,_printf 符号的地址值是什么。
但在汇编中点击 step out 按钮无反应,原因暂不明,我们暂且先重新运行项目,并单步执行到 callq 0x100003f72 的下一行,此时已完成第一次 printf 的调用,可以在 lldb 中查看此时 (__DATA,__la_symbol_ptr) 中 _printf 符号的 Data 值。
在 lldb 中输入 x 0x100000000+0x8000 ,可以看到 _printf 中的 Data 值已变成了以 0x7fff 开头的很大的地址;再使用 dis -s 查看该地址上的汇编,发现此地址指向的内容就是 printf 函数的实现:

第二次调用 printf 的流程
由上一节的内容可知,完成第一次 printf() 函数的调用后,(__DATA,__la_symbol_ptr) 中 _printf 符号的 Data 值已填入了 printf() 的函数实现的地址。
接下来我们在源码中再加一行 printf() 函数,看看第二次调用 printf() 的流程:

由上面分析已知第一次调用 _printf 符号时,会先调用到 dyld_stub_binder 符号。这次我们单步执行到第二个 _printf 符号的调用(由于修改了代码,_printf 符号的地址变成了 0x100003f62 ,不过没关系,不影响后续探索):

然后再执行 step into instruction ,可以看到 printf 的注释中给的是一个以 0x7fff 开头的很大的地址,这个值明显不属于当前 Mach-O ,这就是 printf() 函数的实现在内存中的真实地址。

综上所述,_printf 在第一次调用时,会先调用到 dyld_stub_binder ,dyld_stub_binder 获取到 _printf 符号的真实地址后,将指针值填入 (__DATA,__la_symbol_ptr) 对应的条目中,此时就能完成第一次 _printf 的调用。第二次及之后的 _printf 调用就是直接调用 _printf 在内存中的函数实现了。
说明:在上述案例中,出现了不同截图中 dyld_stub_binder 的函数实现地址值不一样的情况,这是正常的。这些图是笔者在不同日期截取的,而重启系统后,动态库每次都会被加载到不同的地址,因此动态库中函数的地址也是不固定的。(_printf 也是同样的情况)
4. 使用 LLDB 探索 printf 的调用流程
获取基地址 (base address)
在 lldb 中输入 image list ,在输出的结果中,第一个就是我们的 Symbol-Example ,可看到它的基地址是 0x100000000 。
从 MachOView 中获取符号的偏移量 (offset)
在之前的分析中,我们可知 _printf 在 (__DATA,__la_symbol_ptr) 中,且最初指向 (__TEXT,__stub_helper) 。可以看到 _printf 符号的偏移量是 0x8000 :

第一次调用 printf
回顾:1 字节是 8 位,而 1 个 16 进制数字可以表示 4 位,所以两个 16 进制数可以表示 8 位,也就是 1 字节。
根据从 MachOView 中获取的信息,我们可以在 lldb 中可以查看 _printf 符号中存储的内容。输入 x 0x100000000+0x8000(x 是 memory read 的简写)。
(__DATA,__la_symbol_ptr) 中存储的实际上是指针数组,因此 _printf 符号的值占用 8 字节。由于是小端,因此实际地址是 0x100003f88 。
下图中操作流程的解释:
输入
dis -s 0x100003f88查看该地址上的汇编,可以看到第二行执行jmp指令跳转到0x100003f78;输入
dis -s 0x100003f78查看该地址上的汇编,在0x100003f81上的汇编是jmpq *0x79(%rip),注释中给了一个以0x7fff开头的很大的地址,并注明是dyld_stub_binder;同理,再使用
dis命令查看这个以0x7fff开头的这个地址上的汇编,可看到这个地址就是dyld_stub_binder的实现。

第二次调用 printf
当第一次调用 printf() 完成后,再查看这个位置上的汇编代码,发现 _printf 符号对应的指针值变成了一个以 0x7fff 开头的很大的地址,且后面紧随了一行 libsystem_c.dylib`printf ,说明此时 (__DATA,__la_symbol_ptr) 中的 _printf 符号中已存储了 printf() 的函数实现的地址:

小结
Xcode GUI 操作起来比较直观,界面的可读性更强,也能跟踪断点熟悉流程。
使用 lldb 查看汇编的内容,比使用 Xcode GUI 操作更快捷,免去了 step over instruction 和 step into instruction 的操作。
大多数时候可以把它俩结合起来使用。
PIC : 位置无关代码
前面的章节中出现了许多 stub 相关的内容,那么 stub 到底是什么呢?
The static linker is responsible for generating all stub functions, stub helper functions, lazy and non-lazy pointers, as well as the indirect symbol table needed by the dynamic loader (dyld). ---摘自 Apple 文档 。
由文档可知,静态连接器 (static linker) 负责生成了所有的 stub functions, stub helper functions, lazy pointers, non-lazy pointers, 以及 dyld 会用到的 indirect symbol table(可用于查询符号来自于哪个 dylib )。
由于系统的动态库会被加载到任意位置,如果代码调用了系统动态库中的 C 函数,编译器在生成 Mach-O 可执行文件时无法知道该函数的实际地址,因此会插入一个 stub ,或称作符号桩,这样的代码也被称为位置无关代码 (Position independent code, PIC)。
在启动应用时或者第一次使用该符号时再由 dyld 去查找符号对应的实现,将实际的函数指针值填入到 (__DATA_CONST,__got) 或 (__DATA,__la_symbol_ptr) 对应的符号的 Data 中。
fishhook 的适用范围
根据上述分析,我们可以得知 fishhook 的适用范围:
内部符号无法被 hook ,比如自己源码中实现的 C 函数、静态库中的 C 函数。
因为内部符号的地址偏移量在编译时就确定了,存储在 Mach-O 文件的
__TEXT段。由于__TEXT段是只读的,且会进行代码签名验证,因此是不能修改的。(启动阶段 dyld 执行 rebase 的时候,dyld 给指针地址加上偏移量就是指针的真实地址。这个过程是在 pre-main 阶段由 dyld 执行的,我们无法干预。)
外部符号可以被 hook ,比如系统动态库的 C 函数。
如果代码中有外部符号,由于编译器在生成 Mach-O 可执行文件时无法知道该函数的实际地址,因此会插入一个 stub(符号桩)。stub 存储在 Mach-O 文件的
(__DATA,__la_symbol_ptr)或(__DATA_CONST,__got)中。其中,__la_symbol_ptr在第一次调用符号时,会通过dyld_stub_binder去查找符号的真实地址并完成符号绑定 (symbol bind)。
fishhook 源码分析
在我 Fork 的项目中可以查看带注释的源码。
基于前面的分析,我们来看看 fishhook 是如何替换 (__DATA_CONST,__got) 或 (__DATA,__la_symbol_ptr) 中的外部符号的地址的。
公开接口:rebind_symbols()
rebind_symbols()常用的入口函数是:
其中 struct rebinding 结构体的声明是:
单链表:rebindings_entry
rebindings_entry在 fishhook 内部维护了一个单链表,链表节点的声明是:
并声明了链表的头结点 _rebindings_head :
fishhook 内部维护一个单链表的原因:
如果不保存重绑定信息,当新的 image 载入时,之前的设置的符号重绑定就对新载入的 image 不起作用了。
因此每次调用
rebind_symbols()时,都需要把传入的重绑定信息(也就是struct rebinding数组) 存在链表中,当有新的 image 载入时,就能遍历链表对新载入的 image 进行 hook 。
构建单链表:prepend_rebindings()
prepend_rebindings()每次调用 rebind_symbols() 时,会先调用 prepend_rebindings() 来创建链表节点,且新节点添加到链表的前面:
rebind_symbols() 的实现
rebind_symbols() 的实现再看 rebind_symbols() 的具体实现。
链表中只有一个节点,说明是第一次进行重绑定,因此需要对已加载的
链表中有多个节点,说明是
_dyld_register_func_for_add_image():为每个现有的 image 调用回调函数。此后,在加载和绑定每个新 image 时调用该回调函数。这里给传入的回调函数是_rebind_symbols_for_image()。_rebind_symbols_for_image():
rebind_symbols_for_image
rebind_symbols_for_imagerebind_symbols 最终会调用 _rebind_symbols_for_image 函数,而它又调用了 rebind_symbols_for_image 函数:
Mach-O 中的数据结构
xnu 的
以 64-bit 为例。
mach_header_64 结构体:
segment_command_64 结构体:
section_64 结构体:
nlist_64 结构体:
参考资料
参考
https://blog.csdn.net/shengpeng3344/article/details/105179721
https://huang-libo.github.io/posts/App-Startup-Time-dyld/#%E6%80%A5%E5%88%87%E7%9A%84%E7%AC%A6%E5%8F%B7%E8%A7%A3%E6%9E%90
https://www.bilibili.com/video/BV1Qv411i7mi
https://zhuanlan.zhihu.com/p/349723714
https://zhuanlan.zhihu.com/p/349725265
https://zhuanlan.zhihu.com/p/349725692
https://ctinusdev.github.io/2017/08/20/Mach-OBasis_ASLR/
https://juejin.cn/post/6844904175625568270
https://zhangbuhuai.com/post/fishhook.html
https://juejin.cn/post/6988867705637961742
Last updated