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 (定义在<mach-o/loader.h>头文件中)中的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; i<count; i++) {
        // 获取image的mach_header
        const struct mach_header* machHeader = _dyld_get_image_header(i);
        if (machHeader->filetype == 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; i<machHeader->ncmds; 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; j<segmentCmd->nsects; j++) {
                    section_t mSection = sections[j];
                    if ((mSection.flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS ) {
                        // 懒加载section header
                        lazySection = &sections[j];
                        NSLog(@"section name %s",lazySection->sectname);
                    }
                    
                    if ((mSection.flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
                        // 非懒加载到section header
                        nonLazySection = &sections[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; i<gotSymbolNum; i++) {
        // 在间接符号表中的index
        int dysm_index = nonLazySection->reserved1+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; i<lazySymbolNum; i++) {
        // 在间接符号表中的index
        int dysm_index = lazySection->reserved1+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程序员的自我修养

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容

  • 上一篇分析了fishHook原理,本文在fishHook原理基础上进行fishHook源码分析。从fishHook...
    king_jensen阅读 840评论 0 0
  • 前言 虽然写 fishhook 原理的文章有很多,但是总觉得不够简单直观。大部分都是罗列大堆源码进行讲解,看得人云...
    微微笑的蜗牛阅读 1,906评论 6 7
  • 13.1 Objective-C消息传递(Messaging) 对于C/C++这类静态语言,调用一个方法其实就是跳...
    泰克2008阅读 2,009评论 1 6
  • fishhook fishhook是Facebook提供的用于hook系统c函数的库。它能动态重新绑定运行在iOS...
    lattr阅读 1,085评论 0 1
  • 上一篇说到源码经过预处理、编译、汇编之后生成目标文件,这一章介绍一下iOS、Mac OS中目标文件的格式Mach-...
    Tenloy阅读 2,028评论 2 9