前言
虽然写 fishhook
原理的文章有很多,但是总觉得不够简单直观。大部分都是罗列大堆源码进行讲解,看得人云里雾里。
因此,本文将完全抛开源码,旨在简单清晰,直击要害,带你彻底弄清 fishhook
的原理。
hook 本质
函数是如何被调用的?
我们所写的代码最终都会编译为机器码。程序运行时,cpu
会顺序执行一条条的指令,根据不同类型的指令和数据,执行不同的操作。所有的符号都有自己的地址,包括数据和函数符号。
而函数调用是一条跳转指令,如下所示。在做完一些前置保存工作之后,就会跳转到函数所在地址的第一条指令开始执行。
// x86
call xxx
// arm64
br xxx
试想,如果我们想要 hook
某个函数,那是不是将其地址替换下就可以呢?没错,原理就是这么简单,将原函数地址替换为新函数地址。
其实在使用 fishhook
时,我们也能看到一些影子。下面这段代码中,传入了待修改的函数名 NSLog
、新函数 my_log
以及存放原函数的指针 orig_nslog
。
rebind_symbols((struct rebinding[1]){{"NSLog", my_log, (void *)&orig_nslog}}, 1);
从传入的几个参数来看,我们不妨先做个简单的猜想:它可能是在某个地方取到一些函数符号值,对比与传入的函数名是否相等,然后做替换。
当然,这个猜想并不完善,因为还涉及到一些未解的问题:
- 函数符号值存在哪
- 原函数地址又存在哪
- 如何修改原函数地址
那,究竟是不是这个思路呢?下面,我们带着这些问题一一来解答。
Mach-O 结构
在 iOS
中,可执行文件是个 Mach-O
结构。国际惯例,先了解一下它的组成结构,请看下图。
Mach-O
大体上由如下几部分构成:
Header
,描述Macho-O
文件的一些元信息,比如魔数、支持的cpu
类型等。-
Load Commands
,加载命令,也就是告诉系统如何去处理不同的加载信息。常用的命令有:
-
LC_SEGMENT
,表示加载segment
,系统会将其映射到进程的虚拟地址空间中,比如TEXT
代码段、DATA
数据段。 -
LC_DYLIB
,表示加载动态库,会带有动态库的路径信息。 -
LC_LOAD_DYLINKER
,表示动态链接器的信息,里面有dyld
的路径。 - ...
-
Sections
,编译器对不同类型的资源在逻辑上的划分,比如.text
,.data
,.symtab
等。
这里,我们只需要要关注几个特定的 Load Command
和 Section
就好,这对了解 fishhook
的原理就足够了。
如果想要详细了解 mach-o
的具体构成,可参考 osx-abi-macho-file-format-reference。
Load Command
Load Command
表示加载命令,里面包含一些数据信息,不同的加载命令中包含的数据不太一样。
今天我们需要了解的只有如下几种:
LC_SEGMENT (__DATA_CONST)
,加载常量数据段。包括一系列的section header
,也就是section
相关的信息,比如地址、大小、section 名称等。这里我们知道__got section header
在里面就好。对应的结构为segment_command
。LC_SEGMENT (__DATA)
,加载可写数据段。同样包括一系列的section header
,这里我们只需记住有lazy_symbol_ptr section header
。LC_SEGMENT (__LINK_EDIT)
,加载__LINK_EDIT
段。该段中包含了符号表、字符串表、重定位符号表等,主要供dyld
使用。LC_SYMTAB
,包含了符号表、字符串表的偏移量,对应结构为symtab_command
。LC_DYSYMTAB
,包含了间接表(动态库符号表)的偏移量,对应结构为dysymtab_command
。
觉得文字啰里啰嗦的同学,可以直接看下面这张图。
Section
在 section
这部分,我们只要关注数据段的 __got
和 __la_symbol_ptr
两个 section
即可。这两部分的结构一样,都是一个表,里面存储了动态库符号的地址。以下我们统称为 got
表。
__got
中存储数据符号地址,__la_symbol_ptr
中存储函数符号地址,下面我们只分析一种就好。
一看到符号地址
这几个词,有没有突然间精神抖擞。到这里,我们又可以猜想一下,改变符号地址,就是修改 got
表中的值。
为什么符号地址会存放在单独的表中?这就与动态库共享有关。
动态库符号地址
为了做到动态库代码段共享,符号地址不能写死。因此,程序中使用 got
中间表来存放动态库符号地址,代码段中的符号地址都从 got
中获取,从而解决了符号地址固定的问题。
got
在数据段中,可读可写。那么,我们就可以利用这个特性,通过修改 got
表中符号的地址,以达到替换的目的。
关于 got
的说明,可以参看我之前写的文章 图解 Mach-O 中的 got ,对动态库符号地址及链接有个大概的了解。
PS:如果没空看的同学,也无大碍,记住 got
表中存储的是符号地址就好。
关键 Load Command 查找
前面我们提到过,__link_edit
中存放了一些重要的信息,比如符号表、字符串表、重定位表。那么如何获取到这些表呢?
在 Load Command
一节,我们介绍了几个需要留意的加载命令。现在再来回顾一下:
LC_SEGMENT (__LINK_EDIT)
-
LC_SYMTAB
,存放符号表和字符串表的偏移。 -
LC_DYSYMTAB
,存放间接符号表的偏移。
这里,出现了符号和字符串字眼,那么一定跟符号查找有关系。现在知道的信息是几张表的偏移量。那如果取到了真实的基址(加上 ASLR
随机偏移后),再加上偏移量不就可以获取到这几张表了吗?
所以,第一步做的事情就是从 Load Commands
中找到这些个命令,计算出 Mach-O
在虚拟地址空间的实际基址,然后得到以下三张表:
- 符号表
- 字符串表
- 间接符号表
如下图所示,最右边的三张表。
此时已经知道了符号表信息,那么如何将 got
表中的地址跟它所对应的符号关联起来呢?
间接符号表
在说间接符号表之前,我们先了解一下符号表中存的是啥。
是具体符号值吗?♂️,不不不,其实是符号在字符串表中的下标。由于字符串表存储了所有的符号值,那么自然的,符号表中只需存下标就可以,省时省心又省力。
间接符号表,也就是动态库符号表,存放了动态库符号的信息。间接,顾名思义,说明表中的数据不是字符串表的下标,而是该符号在符号表中的下标,中间多了一层。
如下图所示:
got
中的符号是动态库符号,肯定会涉及到间接符号表。那么它们究竟是如何对应上的呢?下面就请 got section header
闪亮登场。
got section header
由于 __got section header
和 lazy_symbol_ptr secton header
结构是完全一样的,下面以 lazy_symbol_ptr secton header
举例说明。
其结构如下图所示:
它里面包含了两个非常关键的字段:
-
address
,表示它所属的section
地址。由于section
中存放是的got
表,所以它也就是got
表地址。 -
reserved1
,表示got
表中第一个符号在间接表中的下标。
第二点有点绕,举个。
假设 got
中总共有 3
个符号,reserved1 = 2
,那么说明第一个符号在间接表中的下标为 2
,第二个符号的下标为 3
,第三个符号的下标为 4
。如下图所示:
这样,如果有了在间接表的下标 → 得到在符号表的下标 → 得到在字符串表的下标 → 得到字符串。
完整的动态符号查找路径如下图所示:
由于我们在使用 fishhook
的时候会传入函数名称,假设为 A
,那么当查找到某项 got
符号字符串值 B
后,那么只需对比一下 A、B
是否相等即可。如果相等,更新该项符号在 got
中的地址即可。
注意
got
中的数据项实际上是个间接指针,也就是指向指针的指针,指针的值才是符号地址。
根据上图,我们可以推断出一个初始的替换雏形:
- 根据
address
字段,获取got
表地址。 - 遍历
got
表,根据当前是第几项,查找当前符号在间接表中的下标。 - 从间接表中获取到符号表下标。
- 从符号表中获取到字符串表下标。
- 得到符号字符串,与待替换函数名称进行比较。若相等,则更新
got
表中的值。
举个,假设将 NSLog
替换为 my_log
自定义实现。整个处理过程如下图所示:
注意:更新
got
第一项符号的地址时,是*got[0] = my_log
,因为表中每一项是二级指针。
至此,如果你能理解上述所讲的知识点,那么恭喜你,已经搞懂了 fishhook
的原理。
加点东西
rebinding 结构
以上只是一个简易模型。在具体实现中,fishhook
将所要替换的函数封装成了一个链表结构,每个节点中有个数组,存放了待修改的函数们。如下图所示:
-
rebindings_entry
表示链表节点。 - 节点字段
rebindings
表示待替换函数数组。 -
struct rebinding
表示函数替换信息。
那么相应的,我们的雏形也得稍微改进一下:
- 根据
address
字段,获取got
表地址。 - 遍历
got
表,根据当前是第几项,查找当前符号在间接表中的下标。 - 从间接表中获取到符号表下标。
- 从符号表中获取到字符串表下标。
- 得到符号字符串,与待替换函数名称进行比较。
- 遍历链表节点
- 遍历当前节点中的函数替换数组
- 若找到了相同的函数符号值,则更新
got
表
全局替换
由于动态库中的符号很可能被可执行文件或多个动态库使用。因此,对于每个加载的 image
,都要做如上的操作。也就是说,这是一项全局性的替换。
打个比方,假设我们将 NSLog
替换为自己的实现。在 A、B、C
三个动态库(不论是系统库还是自己编写的)中都用到了 NSLog
,那么,它们 got
表中的值都要被更新,以达到全局替换的目的。
限制
由于 fishhook
修改的是 got
中的符号地址,那么它也只能对动态库中的 c
函数进行替换。
总结
这篇文章中,我主要讲述了 fishhook
所涉及到的最小知识点,比如动态库符号地址在哪,如何查找符号值,以及如何匹配修改符号地址等。以图代码,期望以最简单直接的方式探究其本质原理。希望对你有帮助~
PS:如果你看懂了原理,再去看源码,应当是非常轻松了。