趣探 Mach-O:FishHook 解析

作者: Joy___ | 来源:发表于2017-02-10 23:53 被阅读1951次

    这是Mach-O系列的第三篇

    阅读 FishHook源码之前,你可能需要对以下知识有个简单的了解

    本文的阐述顺序按照函数调用过程来进行

    Fishhook 可以做什么

    在此借用阿里百川的一张分析图,可以比较清晰的了解FishHook发挥了哪些作用

    阿里百川

    FishHook在这里是对动态链接库起作用,修改对应的函数实现

    对于动态链接库里面的C函数,第一次调用的时候,我们会得到函数和实现地址的对应关系,函数的实现地址存放在一个叫la_symbol_ptr的地方,第二次调用的时候,直接通过la_symbol_ptr找到函数地址就可以,不再需要繁琐的获取函数地址的过程。(具体通过哪些过程,可以参考刚才的链接:Mach-O 的动态链接过程

    那么,上图的含义就很明了了

    在程序运行时,动态链接的 C 函数dynamic(...)地址记录在DATA segment下的la_symbol_ptr中;初始时,程序只知道dynamic函数的符号名而不知道函数的实现地址;首次调用时,程序通过TEXT segment中的stub_helper取得绑定信息,通过dyld_stub_binder来更新la_symbol_ptr中的符号实现地址;这样,再次调用时,就可以通过la_symbol_ptr直接找到dynamic函数的实现;如果我们需要替换dynamic函数的实现,只需要修改__la_symbol_ptr即可,也就是我们要谈的Fishhook

    Fishhook 的实现

    通过fishhook的官方文档可以知道,Fishhook的使用方法大致如下:

    static int (*original_open)(const char *, int, ...);
    
    int new_open(const char *path, int oflag, ...) {
        va_list ap = {0};
        mode_t mode = 0;
    
        if ((oflag & O_CREAT) != 0) {
            // mode only applies to O_CREAT
            va_start(ap, oflag);
            mode = va_arg(ap, int);
            va_end(ap);
            printf("Calling real open('%s', %d, %d)\n", path, oflag, mode);
            return original_open(path, oflag, mode);
        } else {
            printf("Calling real open('%s', %d)\n", path, oflag);
            return original_open(path, oflag, mode);
        }
    }
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            struct rebinding open_rebinding = { "open", new_open, (void *)&original_open };
            rebind_symbols((struct rebinding[1]){open_rebinding}, 1);
            __unused int fd = open(argv[0], O_RDONLY);
        }
        return 0;
    }
    

    先从函数的入口,rebind_symbols开始谈起吧,rebind_symbols主要是使用_dyld_register_func_for_add_image来注册回调函数,在加载动态库的时候执行一些操作

    int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
      // 调用 prepend_rebindings 的函数,将整个 rebindings 数组添加到 _rebindings_head 这个私有链表的头部
      int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
      if (retval < 0) {
        return retval;
      }
      
      // 判断 _rebindings_head->next 的值来判断是否为第一次调用
      // If this was the first call, register callback for image additions (which is also invoked for
      // existing images, otherwise, just run on existing images
      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));
        }
      }
      return retval;
    }
    

    对于prepend_rebindings的代码如下

    // 链表的数组结构
    struct rebindings_entry {
      struct rebinding *rebindings;
      size_t rebindings_nel;
      struct rebindings_entry *next;
    };
    
    static struct rebindings_entry *_rebindings_head;
    
    static int prepend_rebindings(struct rebindings_entry **rebindings_head,
                                  struct rebinding rebindings[],
                                  size_t nel) {
      struct rebindings_entry *new_entry = malloc(sizeof(struct rebindings_entry));
      if (!new_entry) {
        return -1;
      }
      new_entry->rebindings = malloc(sizeof(struct rebinding) * nel);
      if (!new_entry->rebindings) {
        free(new_entry);
        return -1;
      }
      // 将 rebindings 插入到链表头部
      memcpy(new_entry->rebindings, rebindings, sizeof(struct rebinding) * nel);
      new_entry->rebindings_nel = nel;
      new_entry->next = *rebindings_head;
      *rebindings_head = new_entry;
      return 0;
    }
    

    基础结构解释

    Dl_info

    /*
     * Structure filled in by dladdr().
     */
    typedef struct dl_info {
            const char      *dli_fname;     /* Pathname of shared object */
            void            *dli_fbase;     /* Base address of shared object */
            const char      *dli_sname;     /* Name of nearest symbol */
            void            *dli_saddr;     /* Address of nearest symbol */
    } Dl_info;
    
    

    我们一会经过 dladdr()处理后的有效信息都会放进这个结构体中

    • fname:路径名,例如
    /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
    
    
    • dli_fbase:镜像的的起始地址(Base address of shared object,比如上面的 CoreFoundation)
    • dli_saddr :符号的地址
    • dli_sname:符号的名字,即下面的第四列的函数信息
    Thread 0:
    0     libsystem_kernel.dylib          0x11135810a __semwait_signal + 94474
    1     libsystem_c.dylib               0x1110dab0b sleep + 518923
    2     QYPerformanceMonitor            0x10dda4f1b -[ViewController tableView:cellForRowAtIndexPath:] + 7963
    3     UIKit                           0x10ed4d4f4 -[UITableView _createPreparedCellForGlobalRow:withIndexPath:willDisplay:] + 1586420
    

    LC_SYMTAB

    struct symtab_command {
        uint32_t    cmd;        /* LC_SYMTAB */
        uint32_t    cmdsize;    /* sizeof(struct symtab_command) */
        uint32_t    symoff;     /* symbol table offset */
        uint32_t    nsyms;      /* number of symbol table entries */
        uint32_t    stroff;     /* string table offset */
        uint32_t    strsize;    /* string table size in bytes */
    };
    
    

    主要是提供符号表的偏移量,以及元素个数,还有字符串表的偏移和其长度。符号表在 Mach-O目标文件中的地址可以通过LC_SYMTAB加载命令指定的 symoff找到,对应的符号名称在stroff,总共有nsyms条符号信息

    LC_DYSYMTAB

    这个数组结构有些复杂,有兴趣的可以阅读loader.h文件,内部标示了动态符号表的偏移量和符号个数

    struct dysymtab_command {
        uint32_t cmd;   /* LC_DYSYMTAB */
        uint32_t cmdsize;   /* sizeof(struct dysymtab_command) */
        uint32_t indirectsymoff; /* file offset to the indirect symbol table */
        uint32_t nindirectsyms;  /* number of indirect symbol table entries */
        .......
    

    _rebind_symbols_for_image

    对于关键的代码 _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;
      if (dladdr(header, &info) == 0) {
        return;
      }
        
      // segment_command_64
      segment_command_t *cur_seg_cmd;
      segment_command_t *linkedit_segment = NULL;
      // LC_SYMTAB
      struct symtab_command* symtab_cmd = NULL;
     // LC_DYSYMTAB
      struct dysymtab_command* dysymtab_cmd = NULL;
    
        // 下面是要寻找load_command,所以越过mach_header_t
      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
            linkedit_segment = cur_seg_cmd;
          }
        } else if (cur_seg_cmd->cmd == LC_SYMTAB) {
          //遍历寻找lc_symtab
          symtab_cmd = (struct symtab_command*)cur_seg_cmd;
        } else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
          //遍历寻找lc_dysymtab
          dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
        }
      }
    

    为什么要寻找这个几个LoadCommand的信息呢?就如上面介绍的__LINKEDITLC_DYSYMTABLC_SYMTAB都提供了重要的信息。

    __LINKEDIT段 含有为动态链接库使用的原始数据,比如符号,字符串,重定位表条目等等

    阅读下面的代码之前,先来看一个计算公式

    链接时程序的基址 = __LINKEDIT.VM_Address -__LINKEDIT.File_Offset + silde的改变值

    这里出现了一个 slide,那么slide是啥呢?先看一下ASLR

    ASLR:Address space layout randomization,将可执行程序随机装载到内存中,这里的随机只是偏移,而不是打乱,具体做法就是通过内核将 Mach-O的段“平移”某个随机系数。slide 正是ASLR引入的偏移

    也就是说程序的基址等于__LINKEDIT的地址减去偏移量,然后再加上ASLR造成的偏移

      // 链接时程序的基址
      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);
    

    符号表中的元素都是nlist_t结构体,nlist_t中有很多学问,这里先看一下他的基础结构

    /*
     * This is the symbol table entry structure for 32-bit architectures.
     */
    struct nlist {
        union {
            uint32_t n_strx;    /* index into the string table */
        } n_un;
        uint8_t n_type;     /* type flag, see below */
        uint8_t n_sect;     /* section number or NO_SECT */
        int16_t n_desc;     /* see <mach-o/stab.h> */
        uint32_t n_value;   /* value of this symbol (or stab offset) */
    };
    

    然后再次遍历loadcommands,寻找__DATA__DATA_CONSTsection,并对对__nl_symbol_ptr以及__la_symbol_ptr进行rebind

      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和__DATA_CONST的section,对__nl_symbol_ptr以及__la_symbol_ptr进行rebind
          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) {
              // sect为Section,symtab为符号表,strtab字符串表,indirect_symtab动态符号表(indirect symbol table)
              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);
            }
          }
        }
    

    perform_rebinding_with_section

    nl_symbol_ptrla_symbol_ptrsection中的reserved1字段指明对应的indirect symbol table起始的index

    For the two relevant sections, the section headers (struct sections from <mach-o/loader.h>) provide an offset (in the reserved1 field) into what is known as the indirect symbol table. The indirect symbol table, which is located in the __LINKEDIT segment of the binary, is just an array of indexes into the symbol table (also in __LINKEDIT) whose order is identical to that of the pointers in the non-lazy and lazy symbol sections

    So, given struct section nl_symbol_ptr, the corresponding index in the symbol table of the first address in that section is indirect_symbol_table[nl_symbol_ptr->reserved1]. The symbol table itself is an array of struct nlists (see <mach-o/nlist.h>), and each nlist contains an index into the string table in __LINKEDIT which where the actual symbol names are stored. So, for each pointer __nl_symbol_ptr and __la_symbol_ptr, we are able to find the corresponding symbol and then the corresponding string to compare against the requested symbol names, and if there is a match, we replace the pointer in the section with the replacement.

    结合英文,看下面的代码就很容易理解

    // sect为Section,symtab为符号表,strtab字符串表,indirect_symtab动态符号表(indirect symbol table)
    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) {
      // `nl_symbol_ptr`和`la_symbol_ptr`section中的`reserved1`字段指明对应的`indirect symbol table`起始的index
        //动态符号表中第一个解析的符号的起始地址
      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++) {
        // 符号表的index
        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;
        }
        //获取每一个需要动态解析的符号在符号表中的偏移量
        uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
        
        //通过字符串表偏移量获取符号对应的字符串(符号的名字)
        char *symbol_name = strtab + strtab_offset;
    

    上面的代码其实就可以用官方的一个图片很直观的表示

    走到这里是找到了字符串表对应的符号(字符串)

    如何替换实现

    遍历 rebindings 数组,符号进行比较,相同的符号就进行实现替换,这里的代码比较清晰,直接贴出

        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没法hook内联函数吧
      • Enum:传说中的如何hook C函数面试题的答案么。:scream:
        Enum:@sindri的小巢 作者可是APM巨佬:relieved:
        sindri的小巢::smiley: 很棒的文章,最近在实现公司的监控方案,文章中的基础结构对我了解获取堆栈上下文帮助很大
        Joy___:@Enum 额

      本文标题:趣探 Mach-O:FishHook 解析

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