6

iOS程序员的自我修养-fishhook原理(五)

 3 years ago
source link: https://wukaikai.tech/2019/08/12/iOS%E7%A8%8B%E5%BA%8F%E5%91%98%E7%9A%84%E8%87%AA%E6%88%91%E4%BF%AE%E5%85%BB-fishhook%E5%8E%9F%E7%90%86%EF%BC%88%E4%BA%94%EF%BC%89/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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是可以替换变量跟函数的。

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

// 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文件动态链接里面讲到

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

#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. 第一步:传入需要替换的函数
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没有影响,可先不管)

//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数据段,依次遍历这个数据段的符号名(指针对应的符号名),跟传入的符号名进行匹配时候,进行替换即可。
    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– 若无特别说明,本站文章均为原创,转载请保留链接,谢谢


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK