一、概述
在上一篇文章整理了在插件开发中如何 HOOK 动态语言 Objective-C 中的方法,实际上静态语言 C 语言中的函数也是有办法 HOOK 的,这也说明了绝对的静态语言是不存在的。
为了实现 HOOK C 语言中的函数,我们需要用到 Facebook 的一个开源框架 fishhook,通过 fishhook 我们可以很轻松的 HOOK C 语言中的函数,从而达到修改函数功能的目的。
我在参考了 fishhook 官方 demo 和 Draveness 的文章后,发现对 C 函数的 HOOK 也是非常简单的。
在开始之前需要先简单了解两个概念:
Mach-O:对于每个操作系统中的可执行程序都是有格式的,如 ELF 是 Linux 下可执行文件的格式,PE32/PE32+ 是 windows 的可执行文件的格式,那么对于 OS X 和 iOS 来说 Mach-O 是其可执行文件的格式。 OS X 和 iOS 开发中的可执行文件、库文件、Dsym 文件、动态库、动态连接器都是这种格式的。
镜像:在 Mach-O 文件系统中,所有的可执行文件、dylib 以及 Bundle 都是镜像。
二、fishhook的使用
我们先通过一个简单的 demo 去了解一下 fishhook 的使用,fishhook GitHub链接:https://github.com/facebook/fishhook
下载下来 fishhook 后你会发现这个框架非常简单,只有两个文件“fishhook.h”和“fishhook.c”。
我们打开文件“fishhook.h”会发现只有一个结构体和两个方法:
1 | struct rebinding { |
我们先看 rebinding
结构体,结构体中 name
是一个原始函数(要被替换的函数)名字符串,replacement
是替换后的新的函数指针,replaced
是我们自己创建的一个与原始函数签名相同(参数的个数、类型、顺序相同)的函数的指针的指针。关于 rebinding
暂且先不要纠结,后面看过代码就知道如何使用了。
rebind_symbols
函数和 rebind_symbols_image
函数是用来 HOOK 函数的两个方法,只不过参数不同而已,前者比较简单,两个参数一个是 rebinding
数组,一个是数组中 rebinding
个数。后者就稍微复杂点,根据源码中的注释说明,该函数是在仅指定镜像的时候使用。所以,我们这里直接使用 rebind_symbols
函数就可以了。
C 语言中有个 strlen
函数,用来获取字符串的长度,如下:
1 | // |
运行结果:
接下来我们就修改 strlen
函数的返回值,使无论字符串真实长度是什么,都返回 666。我们使用前面说到的 rebind_symbols
函数去实现。
首先我们要声明一个与 strlen
函数签名相同的函数,方法名任意,我们定义为 original_strlen
,如下:
1 | static int (*original_strlen)(const char *__s); |
然后再定义一个替换后的函数,使其不管参数是什么直接返回 666,方法名也任意,我们定义为 new_strlen
,如下:
1 | int new_strlen(const char *__s) { |
接着我们就使用 rebind_symbols
函数进行绑定:
1 | struct rebinding strlen_rebinding = { "strlen", new_strlen, (void *)&original_strlen }; |
上面这些操作完成之后再调用 strlen
函数无论字符串真实长度是什么都会直接返回 666。完整代码如下:
1 | // |
运行结果:
可以看到我们已经达到了 HOOK C 函数的目的,已经理解的可以自己尝试 HOOK 一些其他的函数去实现一些更复杂的功能。
三、fishhook的原理
1、Mach-O
前面提到了 Mach-O 是 OS X 和 iOS 可执行文件的格式,我们这里再来简单看下 Mach-O 文件格式的结构,无需深究。
每一个 Mach-O 文件都会被分为不同的 Segments,比如 __TEXT
, __DATA
, __LINKEDIT
:
Mach-O 中的 segment_command
(32 位与 64 位不同):
1 | struct segment_command_64 { /* for 64-bit architectures */ |
每一个 segment_command
中又包含了不同的 section
:
1 | struct section_64 { /* for 64-bit architectures */ |
2、dyld 与动态链接
dyld(the dynamic link editor)是 Apple 的动态链接器(GitHub地址:dyld),系统 kernel 做好启动程序的初始准备后,交给 dyld 负责,关于其作用顺序,可参考文章《dyld: Dynamic Linking On OS X》,相关部分翻译内容如下:
(1)从kernel留下的原始调用栈引导和启动自己
(2)将程序依赖的动态链接库递归加载进内存,当然这里有缓存机制
(3)non-lazy符号立即link到可执行文件,lazy的存表里
(4)运行可执行文件的静态初始化程序
(5)找到可执行文件的main函数,准备参数并调用
(6)程序执行中负责绑定lazy符号、提供runtime dynamic loading services、提供调试器接口
(7)程序main函数return后执行static terminator
(8)某些场景下main函数结束后调libSystem的_exit函数
一句话总结就是:负责将各种各样程序需要的镜像加载到程序运行的内存空间中!
其作用的时间是 OC 运行时初始化之前!
dyld 加载镜像后会执行相关回调函数,当一个镜像被动态链接时,都会执行回调 void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide)
,传入文件的 mach_header
以及一个虚拟内存地址 intptr_t
。
我们先使用 Xcode 新建一个简单的 C 项目,项目名为 test ,项目新建后默认 main.c 文件内容如下:
1 | // |
我们打开终端 cd 到 main.c 文件目录,使用 gcc 命令编译 main.c 源文件生成可执行文件,执行完成后会生成名为 a.out 的可执行文件。之后通过 nm 命令查看可执行文件中的符号:
从上图可以看出,_printf
这个符号是未定义(undefined
)的,换句话说,编译器还不知道这个符号对应什么东西。
那如果我们自己增加一个函数:
1 | // |
那结果是什么样的呢?如下:
可见我们手动添加的 test
函数所对应的符号 _test
并不是为定义的,它包含一个内存地址以及 __TEXT
段。
为了更深入理解,我们需要用到一个神器 Hopper Disassembler ,这是一个类似于 IDA 的反汇编工具,个人感觉它比 IDA 好用的多,感兴趣的可以自己从网上下载,它最新图标是下面这样的:
我们使用该工具分析一下之前的 a.out 的可执行文件:
可以发现 nm
打印出的另一个符号 dyld_stub_binder
对应另一个同名函数。dyld_stub_binder
会在目标符号(例如 printf
)被调用时,将其链接到指定的动态链接库 libSystem
,再执行 printf
的实现(printf
符号位于 __DATA
端中的 lazy
符号表中)。
每一个镜像中的 __DATA
端都包含两个与动态链接有关的表,其中一个是 __nl_symbol_ptr
,另一个是 __la_symbol_ptr
:
__nl_symbol_ptr
中的 non-lazy 符号是在动态链接库绑定的时候进行加载的__la_symbol_ptr
中的符号会在该符号被第一次调用时,通过 dyld 中的dyld_stub_binder
过程来进行加载
在上述代码调用 printf
时,由于符号是没有被加载的,就会通过 dyld_stub_binder
动态绑定符号:
3、fishhook 的原理
dyld 通过更新 Mach-O 二进制文件 __DATA
段中的一些指针来绑定 lazy
和 non-lazy
的符号;而 fishhook 先确定某一个符号在 __DATA
段中的位置,然后保存原符号对应的函数指针,并使用新的函数指针覆盖原有符号的函数指针,实现重绑定。
对于前面我们 HOOK strlen
函数的例子,过程如下图示:
其中最复杂的部分就是从二进制文件中寻找某个符号的位置,在 fishhook 的 README 中,有这样一张图:
这张图初看很复杂,不过它演示的是寻找符号的过程,我们根据这张图来分析一下这个过程:
从
__DATA
段中的lazy
符号指针表中查找某个符号,获得这个符号的偏移量 1061,然后在每一个 section_64 中查找 reserved1,通过这两个值找到 Indirect Symbol Table 中符号对应的条目在 Indirect Symbol Table 找到符号表指针以及对应的索引 16343 之后,就需要访问符号表
然后通过符号表中的偏移量,获取字符串表中相关函数的符号
- 本文章采用 知识共享署名 4.0 国际许可协议 进行许可,完整转载、部分转载、图片转载时均请注明原文链接。