美文网首页iOS逆向随笔
fishhook x MachOView源码阅读

fishhook x MachOView源码阅读

作者: 收纳箱 | 来源:发表于2020-03-16 11:44 被阅读0次

    1. fishhook源码阅读

    1.1 fishhok原理

    dyld通过更新Mach-O二进制文件中特定__DATA段的指针来绑定惰性和非惰性符号。fishhook通过传递给rebind_symbols的符号名来确定需要更新的位置,然后用相应的替换项重新绑定这些符号。

    对于给定的镜像,__DATA段可以包含与动态符号绑定相关的两个部分:__nl_symbol_ptr__la_symbol_ptr

    • __nl_symbol_ptr是指向非延迟绑定数据的指针数组(这些指针在加载库时绑定)。

    • __la_symbol_ptr是指向导入函数的指针数组,通常在第一次调用该符号时由名为dyld_stub_binder的例程填充(也可以在启动时告诉dyld绑定这些指针)。

    为了找到对应于这些部分中某个特定位置的符号的名称,我们需要通过几个间接层来进行查看。

    • 对于两个相关部分,section header<mach-o/loader.h>中声明的struct section)提供一个偏移量(在reserved1字段中)到所谓的间接符号表中。

    • 间接符号表位于二进制文件的__LINKEDIT段中,它只是符号表(也在__LINKEDIT中)中的索引数组,其顺序与非惰性和惰性符号部分中的指针顺序相同。因此,struct section nl_symbol_ptr,该部分中第一个地址的符号表中的对应索引是indirect_symbol_table[nl_symbol_ptr->reserved1]

    • 符号表本身是一个struct nlist数组(请参见<mach-o/nlist.h>),每个nlist都包含一个指向__LINKEDIT中字符串表的索引,其中存储了实际的符号名。因此,对于每个指针__nl_symbol_ptr__la_symbol_ptr,我们都可以找到相应的符号,然后找到相应的字符串与请求的符号名进行比较,如果有匹配项,我们用替换项替换节中的指针。

    fishhook官方原理示意图

    1.2 测试代码

    //---------------------------------更改NSLog-----------
    //函数指针
    static void(*sys_nslog)(NSString * format,...);
    
    //定义一个新的函数
    void my_nslog(NSString * format,...){
        format = [format stringByAppendingString:@"你咋又来了 \n"];
        //调用原始的
        sys_nslog(format);
    }
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        NSLog(@"log来了,老弟");
        
        struct rebinding nslog;
        nslog.name = "NSLog";
        nslog.replacement = my_nslog;
        nslog.replaced = (void *)&sys_nslog;
        struct rebinding rebs[1] = {nslog};
        rebind_symbols(rebs, 1);
        
        NSLog(@"log来了,老弟");
    }
    
    @end
    

    运行结果:

    2020-03-16 09:47:38.526862+0800 Demo[28657:5210895] log来了,老弟
    2020-03-16 09:47:38.536892+0800 Demo[28657:5210895] log来了,老弟你咋又来了
    

    1.3 Mach-O附着

    MachOView Attach

    MachOView会弹出输入框让你输入PID

    PID

    这个PID在Xcode的Show the Debug navigator菜单下,可以用⌘ + 7快速切过来。这里我们可以看到进程的PID,输入到上面的框中。

    Xcode PID获取

    1.4 MachOView与源码阅读验证

    顶部数据定义与初始化

    struct rebindings_entry {
        struct rebinding *rebindings;
        size_t rebindings_nel;
        struct rebindings_entry *next;
    };
    
    static struct rebindings_entry *_rebindings_head;
    
    // 给需要rebinding的方法结构体开辟出对应的空间
    // 生成对应的链表结构(rebindings_entry)
    static int prepend_rebindings(struct rebindings_entry **rebindings_head,
                                  struct rebinding rebindings[],
                                  size_t nel) {
        // 开辟一个rebindings_entry大小的空间
        struct rebindings_entry *new_entry = (struct rebindings_entry *) malloc(sizeof(struct rebindings_entry));
        if (!new_entry) {
            return -1;
        }
        // 一共有nel个rebinding
        new_entry->rebindings = (struct rebinding *) malloc(sizeof(struct rebinding) * nel);
        if (!new_entry->rebindings) {
            free(new_entry);
            return -1;
        }
        // 将rebinding赋值给new_entry->rebindings
        memcpy(new_entry->rebindings, rebindings, sizeof(struct rebinding) * nel);
        // 继续赋值nel
        new_entry->rebindings_nel = nel;
        // 每次都将new_entry插入头部
        new_entry->next = *rebindings_head;
        // rebindings_head重新指向头部
        *rebindings_head = new_entry;
        return 0;
    }
    

    这里定义了rebindings_entry链表。每次进行绑定的时候,会传入struct rebinding rebindings[]数组,创建一个新的rebindings_entry结构,然后把这个结构插入链表头部。

    两个公开方法

    static void _rebind_symbols_for_image(const struct mach_header *header, intptr_t slide) {
        // 找到对应的符号,进行重绑定
        rebind_symbols_for_image(_rebindings_head, header, slide);
    }
    
    // 在知道确定的MachO,可以使用该方法
    int rebind_symbols_image(void *header,
                             intptr_t slide,
                             struct rebinding rebindings[],
                             size_t rebindings_nel) {
        struct rebindings_entry *rebindings_head = NULL;
        int retval = prepend_rebindings(&rebindings_head, rebindings, rebindings_nel);
        rebind_symbols_for_image(rebindings_head, (const struct mach_header *) header, slide);
        if (rebindings_head) {
            free(rebindings_head->rebindings);
        }
        free(rebindings_head);
        return retval;
    }
    
    int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
        int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
        if (retval < 0) {
            return retval;
        }
        // 如果这是第一次调用,请为image添加注册回调(这也会为现有image调用,否则,只在现有image上运行
        if (!_rebindings_head->next) {
            // 向每个image注册_rebind_symbols_for_image函数,并且立即触发一次
            _dyld_register_func_for_add_image(_rebind_symbols_for_image);
        } else {
            // _dyld_image_count() 获取image数量
            uint32_t c = _dyld_image_count();
            for (uint32_t i = 0; i < c; i++) {
                // _dyld_get_image_header(i) 获取第i个image的header指针
                // _dyld_get_image_vmaddr_slide(i) 获取第i个image的基址
                _rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
            }
        }
        return retval;
    }
    

    rebind_symbols_imagerebind_symbols是两个公开的方法,用于重新绑定符号。rebind_symbols_image用于指定镜像的符号绑定,rebind_symbols对所有镜像进行处理。

    不管是哪个方法,最后都是调用rebind_symbols_for_image去获取相关部分的地址。

    相关部分的地址

    static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
                                         const struct mach_header *header,
                                         intptr_t slide) {
        Dl_info info;
        // 判断当前macho是否在进程里,如果不在则直接返回
        if (dladdr(header, &info) == 0) {
            return;
        }
        
        // 定义好几个变量,后面去遍历查找
        segment_command_t *cur_seg_cmd;
        // MachO中Load Commons中的linkedit
        segment_command_t *linkedit_segment = NULL;
        // MachO中LC_SYMTAB
        struct symtab_command* symtab_cmd = NULL;
        // MachO中LC_DYSYMTAB
        struct dysymtab_command* dysymtab_cmd = NULL;
        
        // header的首地址+mach_header的内存大小
        // 得到跳过mach_header的地址,也就是直接到Load Commons的地址
        uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
        // 遍历Load Commons 找到上面三个遍历
        for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
            cur_seg_cmd = (segment_command_t *)cur;
            // 如果是LC_SEGMENT_64
            if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
                // 找到linkedit
                if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
                    linkedit_segment = cur_seg_cmd;
                }
            }
            // 如果是LC_SYMTAB,就找到了symtab_cmd
            else if (cur_seg_cmd->cmd == LC_SYMTAB) {
                symtab_cmd = (struct symtab_command*)cur_seg_cmd;
            }
            // 如果是LC_DYSYMTAB,就找到了dysymtab_cmd
            else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
                dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
            }
        }
        // 下面其中任何一个值没有都直接return
        // 因为image不是需要找的image
        if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
            !dysymtab_cmd->nindirectsyms) {
            return;
        }
        
        // Find base symbol/string table addresses
        // 找到linkedit的头地址
        uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
        // 获取symbol_table的真实地址
        nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
        // 获取string_table的真实地址
        char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
        
        // Get indirect symbol table (array of uint32_t indices into symbol table)
        // 获取indirect_symtab的真实地址
        uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
        // 同样的,得到跳过mach_header的地址,得到Load Commons的地址
        cur = (uintptr_t)header + sizeof(mach_header_t);
        // 遍历Load Commons,找到对应符号进行重新绑定
        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) {
                // 如果不是__DATA段,也不是__DATA_CONST段,直接跳过
                if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
                    strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
                    continue;
                }
                // 遍历所有的section
                for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
                    section_t *sect = (section_t *)(cur + sizeof(segment_command_t)) + j;
                    // 找懒加载表S_LAZY_SYMBOL_POINTERS
                    if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
                        // 重绑定的真正函数
                        perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
                    }
                    // 找非懒加载表S_NON_LAZY_SYMBOL_POINTERS
                    if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
                        // 重绑定的真正函数
                        perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
                    }
                }
            }
        }
    }
    

    最上面,通过header指针和header大小获取到加载指令的基址。然后遍历获取3个数据结构:

    // MachO中Load Commons中的linkedit
    segment_command_t *linkedit_segment = NULL;
    // MachO中LC_SYMTAB
    struct symtab_command* symtab_cmd = NULL;
    // MachO中LC_DYSYMTAB
    struct dysymtab_command* dysymtab_cmd = NULL;
    

    下面是比较核心的代码:

    // 找到linkedit的头地址
    uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
    
    __LINKEDIT段

    我们来看看linkedit_segment->vmaddr对应4294995968linkedit_segment->fileoff对应28672。这样可能看不太出来这是基地址,我们格式化一下:

    (lldb) p/x 4294995968
    (long) $0 = 0x0000000100007000
    (lldb) p/x 28672
    (int) $1 = 0x00007000
    (lldb) p/x 4294995968 - 28672
    (long) $2 = 0x0000000100000000
    

    我们可以看出这个部分就是拿到了image对应的内存基址。

    // 获取symbol_table的真实地址
    nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
    // 获取string_table的真实地址
    char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
    
    LC_SYMTAB

    struct symtab_command结构中获取到符号表的字符表的偏移量,然后加载基址就是内存中两个表的地址了。

    (lldb) p/x 0x0000000100000000 + 30200
    (long) $3 = 0x00000001000075f8
    (lldb) p/x 0x0000000100000000 + 33408
    (long) $4 = 0x0000000100008280
    
    符号表验证 字符表验证

    通过MachOView我们也验证了这两个地址是正确的。

    // 获取indirect_symtab的真实地址
    uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
    

    通过struct dysymtab_command获取间接符号表。

    间接符号表偏移量
    (lldb) p/x 0x0000000100000000 + 33224
    (long) $5 = 0x00000001000081c8
    
    间接符号表验证

    间接符号表的地址我们也获得了。

    与动态符号绑定相关的两个部分

    // 同样的,得到跳过mach_header的地址,得到Load Commons的地址
    cur = (uintptr_t)header + sizeof(mach_header_t);
    // 遍历Load Commons,找到对应符号进行重新绑定
    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) {
            // 如果不是__DATA段,也不是__DATA_CONST段,直接跳过
            if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
                strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
                continue;
            }
            // 遍历所有的section
            for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
                section_t *sect = (section_t *)(cur + sizeof(segment_command_t)) + j;
                // 找懒加载表S_LAZY_SYMBOL_POINTERS
                if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
                    // 重绑定的真正函数
                    perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
                }
                // 找非懒加载表S_NON_LAZY_SYMBOL_POINTERS
                if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
                    // 重绑定的真正函数
                    perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
                }
            }
        }
    }
    

    对于给定的image__DATA段包含与动态符号绑定相关的两个部分:__nl_symbol_ptr__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) {
        // reserved1对应的的是indirect_symbol中的offset,也就是indirect_symbol的真实地址
        // indirect_symtab+offset就是indirect_symbol_indices(indirect_symbol的数组)
        uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
        // 函数地址,addr就是section的偏移地址
        void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
        // 遍历section中的每个符号
        for (uint i = 0; i < section->size / sizeof(void *); i++) {
            // 访问indirect_symbol,symtab_index就是indirect_symbol中data的值
            uint32_t symtab_index = indirect_symbol_indices[i];
            if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
                symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) {
                continue;
            }
            // 访问symbol_table,根据symtab_index获取到symbol_table中的偏移offset
            uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
            // 访问string_table,根据strtab_offset获取symbol_name
            char *symbol_name = strtab + strtab_offset;
            // string_table中的所有函数名都是以"."开始的,所以一个函数一定有两个字符
            bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1];
            struct rebindings_entry *cur = rebindings;
            // 已经存入的rebindings_entry
            while (cur) {
                // 循环每个entry中需要重绑定的函数
                for (uint j = 0; j < cur->rebindings_nel; j++) {
                    // 判断symbol_name是否是一个正确的函数名
                    // 需要被重绑定的函数名是否与当前symbol_name相等
                    if (symbol_name_longer_than_1 &&
                        strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
                        // 判断replaced是否存在
                        // 判断replaced和老的函数是否是一样的
                        if (cur->rebindings[j].replaced != NULL &&
                            indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
                            // 将原函数的地址给新函数replaced
                            *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
                        }
                        // 将replacement赋值给刚刚找到的
                        indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
                        goto symbol_loop;
                    }
                }
                // 继续下一个需要绑定的函数
                cur = cur->next;
            }
        symbol_loop:;
        }
    }
    

    这个部分就像fishhook原理里面提到的:

    1. indirect_symbol_indices[nl_symbol_ptr->reserved1]拿到间接符号表的函数起始地址。
    2. indirect_symbol_bindingsnl_symbol_ptr中对应的函数指针数组。
    3. 依次遍历间接符号表拿到符号表索引值,并取出符号表中对应索引值的结构,拿到字符表中的偏移量
    4. 通过字符表和偏移量获取到函数名的字符数组首地址。
    5. 字符表中的函数名都是.开头的,所以至少有2个字符。symbol_name[1]是去掉开头.的字符串。
    6. 循环遍历我们要绑定的链表,对比函数名和symbol_name[1]是否相等,将原来的函数地址给replaced中的函数指针,再将原来函数的地址替换为我们要绑定的replacement函数地址。

    相关文章

      网友评论

        本文标题:fishhook x MachOView源码阅读

        本文链接:https://www.haomeiwen.com/subject/vkmwehtx.html