李峰峰博客

iOS 逆向工程之 fishhook

2017-07-06

一、概述

在上一篇文章整理了在插件开发中如何 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
2
3
4
5
6
7
8
9
10
11
12
13
14
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 函数,用来获取字符串的长度,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//
// main.m
// FishHookDemo
//
// Created by 李峰峰 on 2017/7/2.
// Copyright © 2017年 李峰峰. All rights reserved.
//

#import <Foundation/Foundation.h>

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,如下:

1
static int (*original_strlen)(const char *__s);

然后再定义一个替换后的函数,使其不管参数是什么直接返回 666,方法名也任意,我们定义为 new_strlen,如下:

1
2
3
int new_strlen(const char *__s) {
return 666;
}

接着我们就使用 rebind_symbols 函数进行绑定:

1
2
struct rebinding strlen_rebinding = { "strlen", new_strlen, (void *)&original_strlen };
rebind_symbols((struct rebinding[1]){strlen_rebinding}, 1);

上面这些操作完成之后再调用 strlen 函数无论字符串真实长度是什么都会直接返回 666。完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//
// main.m
// FishHookDemo2
//
// Created by 李峰峰 on 2017/7/2.
// Copyright © 2017年 李峰峰. All rights reserved.
//

#import <Foundation/Foundation.h>
#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 位不同):

1
2
3
4
5
6
7
8
9
10
11
12
13
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//
// main.c
// test
//
// Created by 李峰峰 on 2017/7/4.
// Copyright © 2017年 李峰峰. All rights reserved.
//

#include <stdio.h>

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)的,换句话说,编译器还不知道这个符号对应什么东西。

那如果我们自己增加一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//
// main.c
// test
//
// Created by 李峰峰 on 2017/7/4.
// Copyright © 2017年 李峰峰. All rights reserved.
//

#include <stdio.h>

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 段中的一些指针来绑定 lazynon-lazy 的符号;而 fishhook 先确定某一个符号在 __DATA 段中的位置,然后保存原符号对应的函数指针,并使用新的函数指针覆盖原有符号的函数指针,实现重绑定。

对于前面我们 HOOK strlen 函数的例子,过程如下图示:

其中最复杂的部分就是从二进制文件中寻找某个符号的位置,在 fishhook 的 README 中,有这样一张图:

这张图初看很复杂,不过它演示的是寻找符号的过程,我们根据这张图来分析一下这个过程:

  1. __DATA 段中的 lazy 符号指针表中查找某个符号,获得这个符号的偏移量 1061,然后在每一个 section_64 中查找 reserved1,通过这两个值找到 Indirect Symbol Table 中符号对应的条目

  2. 在 Indirect Symbol Table 找到符号表指针以及对应的索引 16343 之后,就需要访问符号表

  3. 然后通过符号表中的偏移量,获取字符串表中相关函数的符号