一、概述
峰哥在上一篇文章整理了在插件开发中如何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”会发现只有一个结构体和两个方法:
struct rebinding { const char *name; void *replacement; void **replaced; }; FISHHOOK_VISIBILITY int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel); FISHHOOK_VISIBILITY int rebind_symbols_image(void *header, intptr_t slide, struct rebinding rebindings[], size_t rebindings_nel);
我们先看 rebinding 结构体,结构体中 name 是一个原始函数(要被替换的函数)名字符串,replacement 是替换后的新的函数指针,replaced 是我们自己创建的一个与原始函数签名相同(参数的个数、类型、顺序相同)的函数的指针的指针。关于 rebinding 暂且先不要纠结,后面看过代码就知道如何使用了。
rebind_symbols 函数和 rebind_symbols_image 函数是用来 HOOK 函数的两个方法,只不过参数不同而已,前者比较简单,两个参数一个是 rebinding 数组,一个是数组中 rebinding 个数。后者就稍微复杂点,根据源码中的注释说明,该函数是在仅指定镜像的时候使用。所以,我们这里直接使用 rebind_symbols 函数就可以了。
C 语言中有个 strlen 函数,用来获取字符串的长度,如下:
// // main.m // FishHookDemo // // Created by 李峰峰 on 2017/7/2. // Copyright ? 2017年 李峰峰. All rights reserved. // #import int main(int argc, const char * argv[]) { @autoreleasepool { char *str = "imlifengfeng"; long result = strlen(str); printf("结果:%ld/n",result); } return 0; }
运行结果:
接下来我们就修改 strlen 函数的返回值,使无论字符串真实长度是什么,都返回 666。我们使用前面说到的 rebind_symbols 函数去实现。
首先我们要声明一个与 strlen 函数签名相同的函数,方法名任意,我们定义为 original_strlen,如下:
static int (*original_strlen)(const char *__s);
然后再定义一个替换后的函数,使其不管参数是什么直接返回 666,方法名也任意,我们定义为 new_strlen,如下:
int new_strlen(const char *__s) { return 666; }
接着我们就使用 rebind_symbols 函数进行绑定:
struct rebinding strlen_rebinding = { "strlen", new_strlen, (void *)&original_strlen }; rebind_symbols((struct rebinding[1]){strlen_rebinding}, 1);
上面这些操作完成之后再调用 strlen 函数无论字符串真实长度是什么都会直接返回 666。完整代码如下:
// // main.m // FishHookDemo2 // // Created by 李峰峰 on 2017/7/2. // Copyright ? 2017年 李峰峰. All rights reserved. // #import #import "fishhook.h" static int (*original_strlen)(const char *__s); int new_strlen(const char *__s) { return 666; } int main(int argc, const char * argv[]) { @autoreleasepool { struct rebinding strlen_rebinding = { "strlen", new_strlen, (void *)&original_strlen }; rebind_symbols((struct rebinding[1]){strlen_rebinding}, 1); char *str = "imlifengfeng"; long test = strlen(str); printf("结果:%ld/n",test); } return 0; }
运行结果:
可以看到我们已经达到了 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 位不同):
struct segment_command_64 { /* for 64-bit architectures */ uint32_t cmd; /* LC_SEGMENT_64 */ uint32_t cmdsize; /* includes sizeof section_64 structs */ char segname[16]; /* segment name */ uint64_t vmaddr; /* memory address of this segment */ uint64_t vmsize; /* memory size of this segment */ uint64_t fileoff; /* file offset of this segment */ uint64_t filesize; /* amount to map from the file */ vm_prot_t maxprot; /* maximum VM protection */ vm_prot_t initprot; /* initial VM protection */ uint32_t nsects; /* number of sections in segment */ uint32_t flags; /* flags */ };
每一个 segment_command 中又包含了不同的 section:
struct section_64 { /* for 64-bit architectures */ char sectname[16]; /* name of this section */ char segname[16]; /* segment this section goes in */ uint64_t addr; /* memory address of this section */ uint64_t size; /* size in bytes of this section */ uint32_t offset; /* file offset of this section */ uint32_t align; /* section alignment (power of 2) */ uint32_t reloff; /* file offset of relocation entries */ uint32_t nreloc; /* number of relocation entries */ uint32_t flags; /* flags (section type and attributes)*/ uint32_t reserved1; /* reserved (for offset or index) */ uint32_t reserved2; /* reserved (for count or sizeof) */ uint32_t reserved3; /* reserved */ };
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 文件内容如下:
// // main.c // test // // Created by 李峰峰 on 2017/7/4. // Copyright ? 2017年 李峰峰. All rights reserved. // #include int main(int argc, const char * argv[]) { // insert code here... printf("Hello, World!/n"); return 0; }
我们打开终端 cd 到 main.c 文件目录,使用 gcc 命令编译 main.c 源文件生成可执行文件,执行完成后会生成名为 a.out 的可执行文件。之后通过 nm 命令查看可执行文件中的符号:
从上图可以看出,_printf 这个符号是未定义(undefined)的,换句话说,编译器还不知道这个符号对应什么东西。
那如果我们自己增加一个函数:
// // main.c // test // // Created by 李峰峰 on 2017/7/4. // Copyright ? 2017年 李峰峰. All rights reserved. // #include void test(){ } int main(int argc, const char * argv[]) { // insert code here... printf("Hello, World!/n"); return 0; }
那结果是什么样的呢?如下:
可见我们手动添加的 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 之后,就需要访问符号表
然后通过符号表中的偏移量,获取字符串表中相关函数的符号
原创文章,转载请注明: 转载自李峰峰博客
本文链接地址: iOS逆向工程之fishhook