fishhook代码剖析个人记录

655 阅读10分钟

年底了,个人方面啥都没发过,把之前在公司发布文章剩下的边角料整理了下,总结了这篇文章。主要是对之前读fishhook源码的一些记录。

fishhook原理简单说明

  • 动态库中函数和参数的调用地址被保存在全局地址偏移表里,对动态库中函数的调用其实就是一系列machO数据表的查找过程
  • 其查找过程为 字符串表->符号表->间接符号表->全局地址偏移表

详细的原理大家可以搜 动态链接原理,这类文章写的好的有好几篇,我就不献丑了,这里主要是读懂fishhook源码。同时要读懂源码还需要了解一下上面几张表的加载命令。通过加载命令我们就可以拿到表的中数据在machO文件中的具体地址。需要了解的加载命令如下:

load commands

machO 的头
struct mach_header_64 {
	uint32_t	magic;		/* mach magic number identifier */
	cpu_type_t	cputype;	/* cpu specifier */
	cpu_subtype_t	cpusubtype;	/* machine specifier */
	uint32_t	filetype;	/* type of file */
	uint32_t	ncmds;		/* number of load commands */
	uint32_t	sizeofcmds;	/* the size of all the load commands */
	uint32_t	flags;		/* flags */
	uint32_t	reserved;	/* reserved */
};

间接符号表中存放的是需要动态绑定的符号在符号表中的index,这个命令的参数太多了,不全放了
struct dysymtab_command {
  ... 
    uint32_t indirectsymoff; /* file offset to the indirect symbol table */  间接符号表的位置
    uint32_t nindirectsyms;  /* number of indirect symbol table entries */ 间接符号表中符号的个数
};	
符号表加载命令
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 */
};

段加载命令
struct segment_command_64 { /* for 64-bit architectures */
	uint32_t	cmd;		/* LC_SEGMENT_64 */
	uint32_t	cmdsize;	/* includes sizeof section_64 structs */
	char		segname[16];	/* segment name */
	uint64_t	vmaddr;		/* memory address of this segment */
	uint64_t	vmsize;		/* memory size of this segment */
	uint64_t	fileoff;	/* file offset of this segment */
	uint64_t	filesize;	/* amount to map from the file */
	vm_prot_t	maxprot;	/* maximum VM protection */
	vm_prot_t	initprot;	/* initial VM protection */
	uint32_t	nsects;		/* number of sections in segment */
	uint32_t	flags;		/* flags */
};
段里面的section_64 的加载命令
struct section_64 { /* for 64-bit architectures */
	char		sectname[16];	/* name of this section */
	char		segname[16];	/* segment this section goes in */
	uint64_t	addr;		/* memory address of this section */
	uint64_t	size;		/* size in bytes of this section */
	uint32_t	offset;		/* file offset of this section */
	uint32_t	align;		/* section alignment (power of 2) */
	uint32_t	reloff;		/* file offset of relocation entries */
	uint32_t	nreloc;		/* number of relocation entries */
	uint32_t	flags;		/* flags (section type and attributes)*/
	uint32_t	reserved1;	/* reserved (for offset or index) */
	uint32_t	reserved2;	/* reserved (for count or sizeof) */
	uint32_t	reserved3;	/* reserved */
};

源码逐句分析

对于太久不接触C语言的同学来说,乍看C的代码其实还是有些头疼的,但是随着年龄的增加,职业的道路愈发的接近瓶颈,想要突破就必须在专业性上有所建树,而很多底层的东西都是C语言实现的,不接触或是总是不自觉的忽略这门底层语言终究是和自己的职业生涯过不去。

选择fishhook来啃其实主要还是因为它代码量不多,在了解大概原理的情况下吃透代码难度不大。这里为了以后方便自己查阅和记录,我这边对通篇代码进行了翻译,尽量不漏掉任何一个细节。

(建议开着xcode配合fishhook源码观看fishhook源码

源码中自定义的两个结构体

struct rebinding {
  const char *name; ///需要hook的方法的名字
  void *replacement; /// 自己的方法的函数指针
  void **replaced;  /// 用来存放原函数指针的一个地址   void*
 };
ListNode
struct rebindings_entry {
  struct rebinding *rebindings; // 传过来的待绑定符号的结构体指针
  size_t rebindings_nel;  // 待绑定的符号个数
  struct rebindings_entry *next; /// next指针 , 为什么要用链表结构
};

入口

static struct rebindings_entry *_rebindings_head;

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

构建rebinding链表

可以看到rebind_symbols中遇到的第一个调用是prepend_rebindings,这里逐句翻译

static int prepend_rebindings(struct rebindings_entry **rebindings_head,
                              struct rebinding rebindings[],
                              size_t nel) {
  //1. 分配空间给new_entry 和  new_entry.rebindings                            
  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;
  }
  //2.用 memcpy , 把 rebind structure 拷贝到 new_entry->rebindings 所指向的位置里去
  
  memcpy(new_entry->rebindings, rebindings, sizeof(struct rebinding) * nel);
  new_entry->rebindings_nel = nel;
  
  //3.  new_entry->next = *rebindings_head  上面说了rebindings_head是地址,所以* rebindings_head 对这个地址取值,这是个空指针,所以这里就是将new_entry->next 置为空,如果之前绑定过,那么实际上这里就是进行了一个头插法
  
  new_entry->next = *rebindings_head;
  
  //4 *rebindings_head = new_entry 则是让 rebindings_head指向的值变成new_entry, rebindings_head是外部变量_rebindings_head的地址,那么现在外部的_rebindings_head等于new_entry这个指针
  *rebindings_head = new_entry;
  return 0;
}

注意,这里的局部变量rebindings_head 其实 是外部定义的结构体指针_rebindings_head 的地址

入口中prepend_rebindings后面的代码

 if (!_rebindings_head->next) {
 	//1. 使用_dyld_register_func_for_add_image,为镜像们注册回掉函数,注册之后每个镜像加载完之后都会调用_rebind_symbols_for_image函数
    _dyld_register_func_for_add_image(_rebind_symbols_for_image);
  } else {
   // 2. 如果有新的镜像被加载进来,也回为新的镜像调用这个回掉函数
    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));
    }
  }

看了上面的prepend_rebindings (上面注释默认按头指针为空的思路来写的),很明显如果你调用过多次绑定的话,那么你每次传进去的结构体都会被prepend_rebindings函数添加进去_rebindings_head这个链表。

所以上面这段代码的意思就是: 第一次调用,进入if里:

  1. 使用_dyld_register_func_for_add_image,为每个镜像注册回掉函数,注册之后每个镜像加载完之后都会调用_rebind_symbols_for_image函数
  2. 如果有新的镜像被加载进来,也回为新的镜像调用这个毁掉函数

然后如果不是第一次调用:也就是else里面 则直接为所有的镜像调用_rebind_symbols_for_image函数

renbinding过程一:_rebind_symbols_for_image

由于64位和非64位的加载命令不太一样,所以fishhook在.c文件顶上做了点类型重定义工作

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

  /**
  ### 找到相应的加载命令
然后我们开始我们上面提到的三张表这个漫长的过程

1. _LINKEDIT 用来计算machO的基地址
2. LC_SYMTAB  对应这里的 symtab_cmd 也就是符号表加载命令
3. LC_DYSYMTAB 对应这里的  dysymtab_cmd  也就是间接符号表加载命令
  */	
  segment_command_t *cur_seg_cmd;
  segment_command_t *linkedit_segment = NULL;
  struct symtab_command* symtab_cmd = NULL;
  struct dysymtab_command* dysymtab_cmd = NULL;

/**
cur_seg_cmd是一个游标,用来进行偏移遍历machO文件,因为loadcommand是从header之后开始的,所以游标的初始位置在header + header的大小 处,
每进行一次for循环 cur_seg_cmd 往下移动当前指向的这个加载命令的cmdsize大小,
for 循环的循环体里就是判断cur_seg_cmd是不是上面列出的三个加载命令的名字
*/

  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;
    /// 判断循环到的段是不是LC_SEGMENT段
    if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
    	///判断这个LC_SEGMENT是不是linkedit
      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;
    }
  }

  ///容错处理,没找到直接return
  if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
      !dysymtab_cmd->nindirectsyms) {
    return;
  }

  /**
  1. 由于machO每次加载会有一个偏移值,所以我们macho真正的基地址每次都是不一样的 需要计算出来,
  2. fishhook中是用linkedit加载命令段段作来计算基地址的,公式其实很简单,就是 _LINKEDIT的vmaddr -  fileoffset, 得到的结果加上macho偏移值slide,得到linkedit_base 就是真实基地址了
  vmaddr参考上面的段加载命令注释,就是这个段的实际内容的内存地址
  最终算出来的linkedit_base就是真是基地
  */
  uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
  
  //通过上面符号表和间接符号表的命令,我们就可以用基地址加上偏移来获得真真正的符号表地址了,symoff代表的是符号表的偏移
  nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
  char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
  
  //这个是拿间接符号表地址的,indirectsymoff代表的是间接符号表的偏移
  // Get indirect symbol table (array of uint32_t indices into symbol table)
  uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);

  //3.  找到data段中的got表和lazy_symbol表的位置,
  cur = (uintptr_t)header + sizeof(mach_header_t);
  for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
 	//找到用来加载data段的命令,我们通过machO可以看到got相关的两张表分别在是由DATA段加载命令 和DATA_CONST段加载命令加载的,所以我们就找到这两个段
    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;
      }
      
      //找到后在这两个段后分别里面遍历每一个区(section)
      for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
      
      /**
      1.sizeof(segment_command_t)知识代表段加载命令结构体的偏移,所以偏过去之后就是后面紧接着的section的开始了
      2.要注意这个偏移和cmdsize不同,cmdsize是连里面的section一起的偏移,所以在遍历段的时候用的是cmdsize
      */
      ///分别找到懒加载和非懒加载的符号表位置,进入到最后的绑定过程
        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);
        }
      }
    }
  }
}

renbinding过程二:perform_rebinding_with_section

前面我们提过了secion64里的reserved1字段存储了符号在间接符号表里的起始ndex,下面的代码就回用到这个

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) {
    
  //第一步indirect_symtab + section->reserved1 定位到间接符号表对应got表的的初始位置
  uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
    
  //这个是got表里面的指针数组的头指针,等下要进行重新绑定的时候就是把这个指针的值给到 我们提供的函数指针,然后把这个指针指向我们结构体里的新定义的函数
    
  void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
    
  //section->size / sizeof(void *) 就是got或lazy里符号的个数 (其实是里面都是地址指针嘛,一个地址8字节,也就是sizeof(void *)嘛,动态链接的时候就是为这里面的指针找库里的实现地址)
  for (uint i = 0; i < section->size / sizeof(void *); i++) {
      
    //开始遍历间接符号表,间接符号表里存的是符号在 真正符号表里的index,这个index现在用symtab_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;
    }
      
    //通过symtab_index 找到 符号在字符串表的的偏移,这个偏移在符号表中, 所以代码是这样的 n_un是一个结构体中存储着在字符串中偏移的信息
    uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
      
    // 通过符号表和上面刚刚拿到的 strtab_offset(符号在字符表中的偏移) 获取的符号的名字symbol_name,这是个字符串
    char *symbol_name = strtab + strtab_offset;
      
    //  symbol_name_longer_than_1 用来判断符号的长度
    bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1];
    struct rebindings_entry *cur = rebindings;
      
    //while循环 遍历我们之前构建的链表 rebindings,实际上就是一开始那个全局的 _rebindings_head
    while (cur) {
       
      // 又来了一层循环,根据我们传入的符号数量循环
      for (uint j = 0; j < cur->rebindings_nel; j++) {
          
        // 因为符号第一个字是下划线_,所以这里做判断的时候是从index1开始的,index0是_, 这也是为什么,我们传进来的字符串不用带下划线的原因
        if (symbol_name_longer_than_1 &&
            strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
            
          //当我们自定义的用来保存原来函数的指针地址存在,且got里的地址和我们自定的指针不一样的时候,表明是第一次,这个时候把原函数保存下来,就是把结构体里的指针替换成got或者lazy表里的指针
          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表示的是got表里的指针数组,上面讲过了
          // 然后把got变或者lazy表里的指针改成我们自己的指针
          indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
          goto symbol_loop;
        }
      }
      cur = cur->next;
    }
  symbol_loop:;
  }
}

symbol_loop 结束符号是个啥,这个我一开始看的时候被绕的有点晕

比如你有10个等待绑定的符号,你在遍历懒加载表的时候找到了一个符号a,然后你在这里去遍历你的rebinding列表里的10个符号,发现我在这10个里面到第5个的时候就是我要进行重新绑定的符号a,那这个时候剩下的5个就不用在继续比较了,我们需要跳到懒加载的下一个符号去进行下一层循环, 所以这里用goto直接跳到下一层循环里去了

总结

没啥好总结的,就是记录了下自己读代码的流程,年底了,希望明年可以多发点水平高点的,有点深度的文章。