通过lldb来说符号表绑定与fishhook

fishhook,facebook开源的一个可以动态绑定Mach-O符号表的库。在程序启动时与运行时会通过dyld来绑定符号表(这里有非懒加载与懒加载之分),而fishhook可以修改符号表的绑定。


验证

  • 非懒加载绑定
  • 懒加载绑定
非懒加载绑定

先来说说非懒加载绑定,我们最熟悉的objc_msgSend就是非懒加载绑定。新建一个空的命令行项目:

int main(int argc, const char * argv[]) {
    NSLog(@"Hello, World!");
    return 0;
}

NSLog(@"Hello, World!")处打上断点,并在Debug Workflow中选中Always Show Disassembly,运行程序后执行如下操作:

image.png

可以看到slide的值为0,似乎命令行项目与iOS不同,并没有使用ASLR技术。

通过lldb来说符号表绑定与fishhook_第1张图片
image.png

通过MachOView可以看到,objc_msgSend在MachO中的偏移量为0x2008,而由于slide为0,所以这里的虚拟地址0x100002008就是程序运行后的真实地址,在控制台中继续执行如下操作:

通过lldb来说符号表绑定与fishhook_第2张图片
image.png
  1. 通过image lookup -a可以看到0x100002008地址对应的确实是objc_msgSend,并且得到函数指针0x00007fff6b167040,它存储在Test.__DATA_CONST.__got
  2. 通过image lookup -n得到objc_msgSend所在动态库libobjc.A.dylib及偏移量0x0000000000006040
  3. 通过image list -o -f得到libobjc.A.dylib动态库首地址0x00007fff6b161000
  4. libobjc.A.dylib动态库首地址加上objc_msgSend偏移量得到objc_msgSend存储地址0x00007fff6b167040

这与用image lookup -a命令看到的地址完全吻合。

懒加载绑定
通过lldb来说符号表绑定与fishhook_第3张图片
image.png

NSLog的偏移量为0x3000,同理,由于slide为0,这里的虚拟地址0x100003000就是真实地址。

通过lldb来说符号表绑定与fishhook_第4张图片
image.png

可以看到NSLog符号存储在Test.__DATA.__la_symbol_ptr中,但是这里得到的地址0x0000000100001eac与真实的NSLog存储地址0x00007fff37702332并不相同,显然在0x0000000100001eac处必定进行了绑定操作。

0x0000000100001eac处打上断点,继续执行程序,得到如下所示汇编代码:

通过lldb来说符号表绑定与fishhook_第5张图片
image.png

这里会跳转到0x100001e9c处,继续加断点跟进:

通过lldb来说符号表绑定与fishhook_第6张图片
image.png

这里调用了dyld_stub_binderNSLog的符号绑定就是通过这个函数完成的,继续跟进并在dyld_stub_binder对应的汇编代码末尾处打上断点,继续运行程序后做如下操作:

通过lldb来说符号表绑定与fishhook_第7张图片
image.png
  1. 此时r11存储的值为0x00007fff37702332
  2. 通过image lookup -a得到这个值就是NSLog的存储地址
  3. 再次查看0x100003000处信息,此时已经变成NSLog(之前为(void *)0x0000000100001eac

以上就是符号的非懒加载绑定与懒加载绑定,顺便说一句,懒加载绑定后已经存在符号到函数的映射信息,再次调用相同函数时不存在这个绑定过程,而是直接调用。

fishhook

  • fishhook的简单使用
  • fishhook源码解析
fishhook的简单使用

接着上文的NSLog继续说:

void (*sys_log)(NSString *format, ...);

void my_log(NSString *format, ...) {
    sys_log([format stringByAppendingFormat:@"---hello fishhook---"]);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"before");
        
        struct rebinding context;
        context.name = "NSLog";
        context.replacement = &my_log;
        context.replaced = (void *)&sys_log;
        struct rebinding contexts[1] = {context};
        rebind_symbols(contexts, 1);
        
        NSLog(@"after");
    }
    return 0;
}

运行程序后输出如下:

image.png

显然已经成功的hook了NSLog函数,下面来看fishhook是怎么做到的。

rebind_symbols(contexts, 1)NSLog(@"after")处打上断点,运行程序并作如下操作:

通过lldb来说符号表绑定与fishhook_第8张图片
image.png

由于在此之前已经调用过NSLog,所以懒加载符号绑定已经完成,此时0x100003000对应的就是NSLog的函数指针。继续运行程序执行rebind_symbols,在此查看0x100003000,此时此时绑定的地址变成了自定义的my_log函数,所以当再次调用NSLog时,通过符号表找到的是my_log,从而实现对NSLog的hook

fishhook源码解析

先了解一下数据结构:

struct rebinding {
  const char *name;    
  void *replacement;
  void **replaced;
};

struct rebindings_entry {
  struct rebinding *rebindings;
  size_t rebindings_nel;
  struct rebindings_entry *next;
};

static struct rebindings_entry *_rebindings_head;

接着看Demo中调用的关键函数rebind_symbols

rebind_symbols
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
  int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
  if (retval < 0) {
    return retval;
  }
  // 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,这个函数是整个数据结构的关键:

prepend_rebindings
static int prepend_rebindings(struct rebindings_entry **rebindings_head,
                              struct rebinding rebindings[],
                              size_t nel) {
  struct rebindings_entry *new_entry = (struct rebindings_entry *) malloc(sizeof(struct rebindings_entry));
  if (!new_entry) {
    return -1;
  }
  new_entry->rebindings = (struct rebinding *) malloc(sizeof(struct rebinding) * nel);
  if (!new_entry->rebindings) {
    free(new_entry);
    return -1;
  }
  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;
}

这里生成一个新的结构体指针new_entry,将rebindingsnel扔到结构体对应的成员变量中,并将next指针指向上文提到的_rebindings_head处,而此时的_rebindings_head值为NULL,最后又将new_entry赋值给_rebindings_head

显然,如果rebind_symbols函数调用多次,最终_rebindings_head是个单链表,prepend_rebindings函数的作用就是将新的结构体指针添加到链表头部,而这个单链表的尾部是NULL

当然,这里涉及到一些内存申请操作,如果申请失败返回-1

继续看prepend_rebindings函数之后的代码:

  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));
    }
  }
 

如果_rebindings_head->nextNULL也就是说rebind_symbols函数是首次调用,此时注册image的回调函数_rebind_symbols_for_image

_dyld_register_func_for_add_image

这个函数是dyld提供给我们的镜像加载回调函数,对应注释如下:

/*
* The following functions allow you to install callbacks which will be called
* by dyld whenever an image is loaded or unloaded. During a call to _dyld_register_func_for_add_image()
* the callback func is called for every existing image. Later, it is called as each new image
* is loaded and bound (but initializers not yet run). The callback registered with
* _dyld_register_func_for_remove_image() is called after any terminators in an image are run
* and before the image is un-memory-mapped.
*/

显然,在调用_dyld_register_func_for_add_image及有新镜像加载时,这个注册的回调函数会被每个已加载的镜像回调。

_rebind_symbols_for_image
static void _rebind_symbols_for_image(const struct mach_header *header,
                                      intptr_t slide) {
    rebind_symbols_for_image(_rebindings_head, header, slide);
}

回调函数直接调用rebind_symbols_for_image

rebind_symbols_for_image

这是fishhook的核心函数:

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_t *cur_seg_cmd;
  segment_command_t *linkedit_segment = NULL;
  struct symtab_command* symtab_cmd = NULL;
  struct dysymtab_command* dysymtab_cmd = NULL;

  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;
    }
  }

  if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
      !dysymtab_cmd->nindirectsyms) {
    return;
  }

  ...
}

先通过dladdr函数查看是否可以加载到mach_header *对应的info,如果加载不到,直接return。

这里说一下Dl_info的数据结构:

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;

比如,Demo对应的info信息如下:

通过lldb来说符号表绑定与fishhook_第9张图片
image.png

接着定义了4个结构体指针,以64位架构为例:

  1. segment_command_t就是segment_command_64
  2. symtab_commandLoad CommandsLC_SYMTAB对应的结构体
  3. dysymtab_commandLoad CommandsLC_DYSYMTAB对应的结构体

然后跳过MachO Header,开始遍历Load Commands,将对应的LC分别放到linkedit_segmentsymtab_cmddysymtab_cmd这三个结构体指针中。

完成循环后对结构体指针判空,如果有满足为空条件的,直接return。

static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
                                     const struct mach_header *header,
                                     intptr_t slide) {
  ...

  // Find base symbol/string table addresses
  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);

  // Get indirect symbol table (array of uint32_t indices into symbol table)
  uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);

  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;
      }
      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);
        }
      }
    }
  }
}
  1. 获取当前镜像SEG_LINKEDITASLR偏移后的首地址
  2. 获取符号表地址
  3. 获取字符串表地址
  4. 获取动态符号表地址
  5. 再次遍历Load Commands找到__DATA__DATA_CONST中对应的__nl_symbol_ptr__la_symbol_ptr进行重新绑定

perform_rebinding_with_section的功能就是,遍历当前符号表是否有与链表中每个结构体指针对应的name字段匹配,如果有则重新绑定。至此。fishhook的rebind就完成了。

再回到rebind_symbols,来看对应的else部分代码:

int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
  int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
  if (retval < 0) {
    return retval;
  }
  // 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;
}

上文说过,_dyld_register_func_for_add_image只有在调用或者有新镜像加载时才会调用注册的回调函数。对于同一个镜像来说,当rebind_symbols被多次调用时,注册的回调函数_rebind_symbols_for_image已经不会再被调用,此时先获取当前已加载镜像,然后逐个手动调用_rebind_symbols_for_image即可。


Have fun!

你可能感兴趣的:(通过lldb来说符号表绑定与fishhook)