fishhook实现原理分析

fishhook 是FaceBook开源的可以用来重绑定Mach-O格式的外部动态库中符号的一个库,这里一定要理解为什么hook的是动态库,想要真正搞清楚这个库的原理可以阅读《程序员的自我修养》这本书,首先要理解什么是静态库,什么是动态库。这篇文章比较偏重对整个库实现过程的分析,实现代码的理解

使用

static void (*sys_NSLog)(NSString *format,...);
static void hook_nslog(NSString *format, ...){
    // 修改打印的内容
    format = [format stringByAppendingFormat:@" haha"];
    // 调用hook的函数
    sys_NSLog(format);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSLog(@"Hook before !");
        // hook Foundation框架中的NSLog函数
        struct rebinding rebindSymbol;
        rebindSymbol.name = "NSLog";
        rebindSymbol.replacement = (void *)hook_nslog;
        // 将原来函数的地址保持在sys_NSLog中
        rebindSymbol.replaced = (void **)&sys_NSLog;
        
        struct rebinding rebs[] = {rebindSymbol};
        rebind_symbols(rebs,1);
        
        NSLog(@"Hook after !");
        
    }
    return 0;
}

打印结果

Hook before !
Hook after ! haha

通过前后两个打印信息,可以看到我们hook了NSLog函数,并且打印了自定义的信息,同时要注意在我们替换的函数中调用保存的函数,这样才会调用到原来函数中

实现原理分析

dyld通过更新Mach-O二进制文件的__Data段的特定部分的指针来绑定所谓的 lazy 和 non-lazy 符号。fishhook通过rebind_symbols函数传入的需要替换的符号名称来定位它的位置,然后执行替换来实现重绑定符号的过程

在一个Mach-O文件中,__Data段可能会包含动态绑定符号相关的section:__nl_symbol_ptr 和 __la_symbol_ptr ,__nl_symbol_ptr是非懒加载的一组指针数组(可以理解为函数地址,这些地址在程序载入的时候绑定),__la_symbol_ptr也是指向导入函数的指针数组,通常在第一次调用该符号时由dyld_stub_binder函数填充,为了能在相应的sections中找到特定位置的符号的名称,需要跳过几个间接层。对于这两个相关的sections,对应的section header (定义在头文件中)中的reserved1字段提供了他们相关的符号在间接符号表中的起始位置,间接符号表可以通过__LINKEDIT段来定位,它是在符号表中的一组index数组,其顺序与懒加载和非懒加载部分中指针的顺序相同,所以对于struct section nl_symbol_ptr,它在符号表中第一个符号的index可以通过这样来获取indirect_symbol_table[nl_symbol_ptr->reserved1],符号表是为struct nlist的数组,每一个nlist中对应的在字符表中的index,字符表也可以通过__LINKEDIT段来定位,字符表存储的就是符号名称的字符数组。所以最后我们就可以通过字符表和需要hook的符号名称比较来找到符号的位置,然后可以将函数指针替换。

上面是对官方说明文档的一些翻译理解,总结一下整个过程就是首先要明确我们要替换的数据是在数据区,当然代码区的数据我们也无法修改。动态库的符号又分为所谓的:懒加载符号和非懒加载符号,非懒加载符号在程序加载阶段就必须要完成绑定,绑定就是dyld去查找对应的符号对应的函数地址,然后将地址写入到非懒加载的数据区。懒加载符号会在第一次调用这个函数时,程序会通过懒加载符号的数据区找对应的函数地址,而此时这个函数地址指向的是__stud_helper代码段的一段固定代码,这段 代码又会跳转到dyld_stub_binder这个函数处,然后通过dyld_stub_binder去查找外部符号地址,找到后将地址写入到相应的数据区。

实现过程

整个实现过程就像是一个文件的解析,如果有解析过mp4,flv这种类似的文件,可能会更好理解整个过程。

第一步:找到当前可执行文件的image文件

    // 获取加载的image文件个数
    int count = _dyld_image_count();
    int executeIndex = -1;
    for (int i = 0; ifiletype == MH_EXECUTE) { // 查找主程序的image的index
            executeIndex = i;
            break;
        }
    }

先通过dyld提供的函数_dyld_image_count获取当前程序加载的image文件个数,然后遍历查找主程序image所在的index。

第二步:查找符号命令,动态符号命令,链接命令

#ifdef __LP64__
typedef struct mach_header_64 mach_header_t;
typedef struct segment_command_64 segment_command_t;
typedef struct section_64 section_t;
typedef struct nlist_64 nlist_t;
#define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT_64
#else
typedef struct mach_header mach_header_t;
typedef struct segment_command segment_command_t;
typedef struct section section_t;
typedef struct nlist nlist_t;
#define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT
#endif

    const struct mach_header* machHeader = _dyld_get_image_header(executeIndex);
    
    uintptr_t cur = (uintptr_t)machHeader + sizeof(mach_header_t);
    
    // 符号表命令
    struct symtab_command *symCommand = NULL;
    // 动态符号表命令
    struct dysymtab_command *dysymCommand = NULL;
    // 链接命令
    segment_command_t *linked_cmd = NULL;
    
        for (int i = 0; incmds; i++) {
        
        struct load_command *command = (struct load_command *)cur;
        
        // 链接命令属于segment_command类型
        if (command->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
            segment_command_t *segmentCmd = (segment_command_t *)command;
            
            // 链接命令
            if (strcmp(segmentCmd->segname, SEG_LINKEDIT) == 0) {
                linked_cmd = segmentCmd;
            }
        }
        
        // 符号表
        if (command->cmd == LC_SYMTAB) {
            symCommand = (struct symtab_command *)command;
        }
        
        // 动态符号表
        if (command->cmd == LC_DYSYMTAB) {
            dysymCommand = (struct dysymtab_command *)command;
        }
        
        cur += command->cmdsize;
    }
    

通过上面的代码就可以找到符号命令,动态符号命令,链接符号命令。

第三步:获取懒加载符号函数地址和非懒加载符号函数地址的section Hearder

非懒加载符号对应的函数指针数组在数据区的__got节,懒加载符号对应的函数指针数组在数据区的__la_symbol_ptr节,首先需要查找到对应到Section header,这两个section header在segment_command为SEG_DATA和SEG_DATA_CONST的command中,,__got section header在SEG_DATA_CONST的segment_command中,__la_symbol_ptr section header在SEG_DATA的segment_command中

if (strcmp(segmentCmd->segname, SEG_DATA) == 0 || strcmp(segmentCmd->segname, SEG_DATA_CONST) == 0) {
                section_t *sections = (section_t *)((uintptr_t)segmentCmd + sizeof(segment_command_t));
                for (int j = 0; jnsects; j++) {
                    section_t mSection = sections[j];
                    if ((mSection.flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS ) {
                        // 懒加载section header
                        lazySection = §ions[j];
                        NSLog(@"section name %s",lazySection->sectname);
                    }
                    
                    if ((mSection.flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
                        // 非懒加载到section header
                        nonLazySection = §ions[j];
                        int index = nonLazySection->reserved1;
                        NSLog(@"section name %s",nonLazySection->sectname);
                    }
                }
            }

第四步:理解ASLR,计算符号信息,间接符号信息,字符信息在内存中实际地址

  • ASLR:通俗的说就是在app每次启动的时候会随机给一个地址偏移量,由于现代计算机都使用的是虚拟内存,会导致程序加载到内存中可能每次都是固定的一个地址,这样会有安全问题,通过每次程序启动时给程序加载的地址添加一个随机的偏移值,就是所谓的ASLR
  • 计算程序加载的基地址:通过链接段的vmaddr和fileoff字段计算出没有ASLR的情况下程序加载的基地址,然后将这个地址加上ASLR的值就可以得到程序实际的基地址
  • 计算符号信息地址:通过 base(上面计算得到的基地址) + symtab 的偏移量 计算 symtab 表的首地址,并获取 nlist_t 结构体实例
  • 计算间接符号信息地址:通过 base + indirectsymoff 偏移量来计算动态符号表的首地址
  • 计算字符信息地址:/通过 base + stroff 字符表偏移量计算字符表中的首地址,获取字符串表
    // 得到当前程序ASLR的值
    intptr_t slide = _dyld_get_image_vmaddr_slide(executeIndex);
    
    // 计算实际加载的基地址
    uintptr_t linked_base_address = linked_cmd->vmaddr-linked_cmd->fileoff+slide;
    
    // 计算符号表所在地址
    nlist_t *symbolList = (nlist_t *)(linked_base_address+symCommand->symoff);
    // 计算间接符号表所在位置
    uint32_t *dysmList = (uint32_t *)(linked_base_address+dysymCommand->indirectsymoff);
    // 计算字符表所在位置
    char *strList = (char *)(linked_base_address+symCommand->stroff);

上面就是计算的方法,其中symCommand和dysymCommand通过上面步骤二获取

第五步:遍历__got段和__la_symbol_ptr段

最后一步就是遍历数据区的__got段(非懒加载符号的函数地址数组)和__la_symbol_ptr (懒加载符号的函数地址数组)比对要查找的符号名称,找到要替换的符号位置

    // 遍历__got段
    int gotSymbolNum = nonLazySection->size/(sizeof(void*));
    void **gotSymbolValue = (void **)((uintptr_t)slide + nonLazySection->addr);
    for (int i = 0; ireserved1+i;
        // 在符号表中的index
        uint32_t symtab_index = dysmList[dysm_index];
        if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
            symtab_index == (INDIRECT_SYMBOL_LOCAL   | INDIRECT_SYMBOL_ABS)) {
          continue;
        }
        // 找到对应的符号
        nlist_t findSymbol = symbolList[symtab_index];
        char *mSymbolName = strList+findSymbol.n_un.n_strx;
        bool symbol_name_longer_than_1 = mSymbolName[0] && mSymbolName[1];
        if (symbol_name_longer_than_1) {
            // 由于c语言在编译时将符号名前面加上_,所以这里需要从index为1处开始比较
            if (strcmp(&mSymbolName[1],symbolName) == 0) {
                NSLog(@"Find symbolName : %s",symbolName);
                //break;
                // 替换函数实现
                gotSymbolValue[i] = replaceFunc;
            }
        }
    }
    // 遍历__la_symbol_ptr段
    int lazySymbolNum = lazySection->size/sizeof(void*);
    void **laSymbolValue = (void **)((uintptr_t)slide + lazySection->addr);
    for (int i = 0; ireserved1+i;
        // 在符号表中的index
        uint32_t symtab_index = dysmList[dysm_index];
        if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
            symtab_index == (INDIRECT_SYMBOL_LOCAL   | INDIRECT_SYMBOL_ABS)) {
          continue;
        }
        // 在字符表中的index
        int str_offset = symbolList[symtab_index].n_un.n_strx;
        char *symbolStr = strList+str_offset;
        bool symbol_name_loger_than_1 = symbolStr[0] && symbolStr[1];
        if (symbol_name_loger_than_1) {
            if (strcmp(&symbolStr[1], symbolName) == 0) {
                NSLog(@"Find symbol : %s",symbolName);
                // 替换函数实现
                laSymbolValue[i] = replaceFunc;
            }
        }
    }

总结

以上主要分析了查找符号的整个流程,具体实现可根据fishhook源码比较分析.

参考资料

《程序员的自我修养》

《深入理解Mac OSX & iOS操作系统》

探究Mach-O文件

iOS程序员的自我修养

你可能感兴趣的:(fishhook实现原理分析)