文章目录
  1. 1. 目录
  2. 2. fishhook原理
    1. 2.1. 举个🌰 (动态替换变量跟函数)
  3. 3. fishhook实现分析
    1. 3.1. LINKEDIT
    2. 3.2. 替换函数/变量地址过程
      1. 3.2.1. 指针对应的符号名
      2. 3.2.2. 找到nl_symbol_ptr(got)/la_symbol_ptr
    3. 3.3. 源码分析
  4. 4. 最后

目录

fishhook原理

MachO文件动态链接里面讲到,模块间的数据访问和函数调用,都是用间接寻址。主模块将要访问动态库里的数据符号地址放在got(也称Non-Lazy Symbol Pointers)数据段,调用动态库的函数的地址放在la_symbol_ptr数据段。而数据段是可读写的,所以程序运行期间我们可以通过修改got(non_la_symbol_ptr)和la_symbol_ptr数据段,来替换函数跟全局变量的地址。这个就是fishhook的原理。模块内部的数据跟函数地址,静态链接时候已经确定好了,而且在代码段(可读、可执行、不可写),所以fishhook是不能rebinding模块内部的symbols。

facebook是这样介绍fishhook的:

A library that enables dynamically rebinding symbols in Mach-O binaries running on iOS.

这里的symbols,就是指动态库里暴露出来的变量跟函数。所以fishhook是可以替换变量跟函数的。

举个🌰 (动态替换变量跟函数)

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// b.m文件
char *global_var = "world";

=========================

//main.m文件
#import <Foundation/Foundation.h>
#import "fishhook.h"

static void (*orgi_NSLog)(NSString *format, ...);
char *orgi_var = "wukaikai";
extern char *global_var;

void my_NSLog(NSString *format, ...)
{
printf("hello %s\n", global_var);
}

int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
printf("hello %s\n", global_var);
struct rebinding rebind[2] = {{"NSLog", my_NSLog, (void *)&orgi_NSLog}, {"global_var", &orgi_var, NULL} };
rebind_symbols(rebind, 2);
NSLog(@"%s",global_var);
}
return 0;
}

=========================

//依次执行这两个命令,生成可执行文件main (不懂为啥是这两个命令,回顾前面博客)
clang -fpic -shared b.m -o libStr.dylib
clang -framework Foundation main.m fishhook.c -o main -L . -l str

=========================

//输出
hello world
hello wukaikai
//可以看到,global_var和NSLog都被替换了

fishhook实现分析

fishhook用到LINKEDIT去计算基址,这里我先讲这个加载命令LC_SEGMENT_64(_LINKEDIT)

LINKEDIT

LINKEDIT segment是link editor在链接时候创建生成的segment,这个段包含了符号表(symtab)、间接符号表(dysymtab)、字符串表(string table)等。

这个我在MachO文件结构分析最后讲到:从链接的角度来看,Mach-O文件是按照section来存储文件的,segment只不过是把多个section打包放在一起而已;但是从Mach-O文件装载到内存的角度来看,Mach-O文件是按照segment(编译时候,编译器把相同权限的数据放在一起,成为segment)来存储的,即使一个segment里的内容小于1页空间的内存,但是还是会占用一页空间的内存,所以segment里不仅有filesize,也有vmsize,而section不需要有vmsize。不信你看符号表和间接符号表这两个加载命令里都没有vmsize,所以我是不是也可以把符号表和间接符号表理解成两个section。

我个人觉得segment、section、加载命令这些概念都是从不同角度去看待的,不用严格区分。

替换函数/变量地址过程


从上面原理中,我们知道替换过程非常简单,如下:

  1. 传入需要替换的函数/变量。(这个函数跟变量是其它模块(dylib)中的)
  2. 找到nl_symbol_ptr(got)/la_symbol_ptr数据段,依次遍历这个数据段,找到符号名跟第一步传入的符号名匹配时候,进行替换即可。

第二步又有两个问题需要解决,nl_symbol_ptr(got)/la_symbol_ptr这两个数据段存放的是符号地址(指针),1. 如何知道这个指针对应的符号名?2. 如何找到nl_symbol_ptr(got)/la_symbol_ptr数据段?

指针对应的符号名

MachO文件动态链接里面讲到

1
2
3
4
5
6
7
8
9
10
value = IndirectSymbolTable[got.section_64.reserved1];
symbolTable[value] 就是got数据段的第一个符号。
symbolTable[value+1] 就是got数据段的第二个符号。
...依次类推

//1. 从got的section_64可以找到got数据段里面元素对应的符号
//2. 符号(nlist_64)里的n_strx,去字符串表获取符号名
//la_symbol_ptr也是同样的方法找到符号名
==============
如果看不懂通过reserved1,一步一步获取到符号名。那说明这系列课程前面部分,你需要再回顾一遍。

所以我们找到符号表、字符串表、间接符号表,就可以得到指针对应的符号名了。通过加载命令,很容易得到这些。

找到nl_symbol_ptr(got)/la_symbol_ptr

由于这两个section都是在DATA segment里,我们先根据加载命令得到DATA;然后根据section_64的flag,可以找到nl_symbol_ptr(got)/la_symbol_ptr

1
2
#define	S_NON_LAZY_SYMBOL_POINTERS	0x6	/* section with only non-lazy symbol pointers */
#define S_LAZY_SYMBOL_POINTERS 0x7 /* section with only lazy symbol pointers */

源码分析

注意,为了让读者注意力都放在主要逻辑线上,下面的源码,我会省略许多非核心的逻辑,比如边界判断等。完整源码请见fishhook

  1. 第一步:传入需要替换的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
//将每次传入的rebindings当做一个结点,构建成链表
int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
// 第一次调用,进入if里面;_dyld_register_func_for_add_image做了2件事,第一件事是跟else里面一样,为每个image(镜像)调用_rebind_symbols_for_image,第二件事是当dyld后面加载镜像时候,也为这个新镜像调用_rebind_symbols_for_image。
if (!_rebindings_head->next) {
_dyld_register_func_for_add_image(_rebind_symbols_for_image);
} else {
uint32_t c = _dyld_image_count();
for (uint32_t i = 0; i < c; i++) {
_rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
}
}
}

  1. 第二步:做了三件事
    1. 计算基址(为第2步服务)
    2. 找到符号表、字符串表、间接符号表
    3. 找到nl_symbol_ptr(got)/la_symbol_ptr

步骤2、3上面已经讲了。那为啥要计算基址呢,因为ASLR技术,简单理解就是Windows所有程序虚拟内存起始地址是一样的,但是iOS中,为了预防黑客攻击,起始地址有一个随机偏移值。(不理解ASLR,对理解fishhook没有影响,可先不管)

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
//rebindings上面链表的表头;slide ASLR随机偏移值
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
const struct mach_header *header,
intptr_t slide) {
segment_command_t *cur_seg_cmd;
segment_command_t *linkedit_segment = NULL; //LINKEDIT
struct symtab_command* symtab_cmd = NULL; //符号表
struct dysymtab_command* dysymtab_cmd = NULL; //间接符号表
//1. 遍历加载命令,获得MachO中符号表、间接符号表、LINKEDIT三个加载命令
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
linkedit_segment = cur_seg_cmd;
}
} else if (cur_seg_cmd->cmd == LC_SYMTAB) {
symtab_cmd = (struct symtab_command*)cur_seg_cmd;
} else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
}
}
// 本来是:基址=linkedit内存地址 - linkedit的fileoff
//由于ASLR:真实基址 = 基址 + slide
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
//根据真实基址,得到符号表、间接符号表、字符串表的虚拟内存地址
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
===========================
//2. 遍历加载命令,得到DATA,然后遍历DATA里面的section,
//找到nl_symbol_ptr(got)/la_symbol_ptr
cur = (uintptr_t)header + sizeof(mach_header_t);
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
continue;
}
//遍历DATA里面的section,找到nl_symbol_ptr(got)/la_symbol_ptr
for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
section_t *sect =
(section_t *)(cur + sizeof(segment_command_t)) + j;
if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
}
}
}
}

大家想过没有,为啥计算基址,要用LINKEDIT。其实用TEXT、DATA哪个加载命令,都可以得到基址(很容易得到结论)。我觉得是因为我们寻找的符号表、间接符号表、字符串表都在LINKEDIT里面,假如这三个表没有了,后面操作就不用进行了。所以要是没有LINKEDIT,肯定没有这三个表,但是其它TEXT/DATA等就没有这个保证了(比如有这三个表,但是没有TEXT/DATA),facebook是为了严谨性吧。(这个也是我的推测,有不同意见的,欢迎评论区说下你的想法)

  1. 第三步:根据nl_symbol_ptr(got)/la_symbol_ptr数据段,依次遍历这个数据段的符号名(指针对应的符号名),跟传入的符号名进行匹配时候,进行替换即可。
    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
    28
    29
    30
    static void perform_rebinding_with_section(struct rebindings_entry *rebindings,
    section_t *section,
    intptr_t slide,
    nlist_t *symtab,
    char *strtab,
    uint32_t *indirect_symtab) {
    uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
    void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
    for (uint i = 0; i < section->size / sizeof(void *); i++) {
    uint32_t symtab_index = indirect_symbol_indices[i];
    uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
    char *symbol_name = strtab + strtab_offset;
    struct rebindings_entry *cur = rebindings;
    while (cur) {
    for (uint j = 0; j < cur->rebindings_nel; j++) {
    if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
    //第一次,保存原函数
    if (cur->rebindings[j].replaced != NULL &&
    indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
    *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
    }
    indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
    goto symbol_loop;
    }
    }
    cur = cur->next;
    }
    symbol_loop:;
    }
    }

最后

fishhook是一个很好的例子,可以用来检验自己是否理解了MachO文件。如果你看fishhook源代码没有障碍,那恭喜你已经对MachO有不错的理解了;反之你觉得代码还有不理解地方,那就要看下前几篇相应的地方了。

–EOF– 若无特别说明,本站文章均为原创,转载请保留链接,谢谢

文章目录
  1. 1. 目录
  2. 2. fishhook原理
    1. 2.1. 举个🌰 (动态替换变量跟函数)
  3. 3. fishhook实现分析
    1. 3.1. LINKEDIT
    2. 3.2. 替换函数/变量地址过程
      1. 3.2.1. 指针对应的符号名
      2. 3.2.2. 找到nl_symbol_ptr(got)/la_symbol_ptr
    3. 3.3. 源码分析
  4. 4. 最后