最近也学习了下这个fishhook库,感觉200多行代码就能实现方法的交换确实挺厉害的,下面就进行简单的分析下
首先我们先来查看下fishhook.h文件
#ifndef fishhook_h
#define fishhook_h
#include <stddef.h>
#include <stdint.h>
//defined用来检测常量有没有被定义,若常量存在,则返回true,否则返回 false
#if !defined(FISHHOOK_EXPORT)
//如果FISHHOOK_EXPORT没有被定义,在动态库中隐藏该符号用于下面的两个函数
#define FISHHOOK_VISIBILITY __attribute__((visibility("hidden")))
#else
#define FISHHOOK_VISIBILITY __attribute__((visibility("default")))
#endif
//extern "C"的真实目的是实现类C和C++的混合编程。extern “C”是由C++提供的一个连接交换指定符号,用于告诉C++这段代码是C函数。extern “C”后面的函数不使用的C++的名字修饰,而是用C。这是因为C++编译后库中函数名会变得很长,与C生成的不一致,造成C++不能直接调用C函数
#ifdef __cplusplus
extern "C" {
#endif //__cplusplus
/*
* A structure representing a particular intended rebinding from a symbol
* name to its replacement
*/
struct rebinding {
const char *name;//字符串方法名称
void *replacement; //替换后的方法
void **replaced; //代表函数的指针,保存原始的函数地址,之所以要保存是因为我们之后要使用原函数的功能还可以使用
};
/*
* For each rebinding in rebindings, rebinds references to external, indirect
* symbols with the specified name to instead point at replacement for each
* image in the calling process as well as for all future images that are loaded
* by the process. If rebind_functions is called more than once, the symbols to
* rebind are added to the existing list of rebindings, and if a given symbol
* is rebound more than once, the later rebinding will take precedence.
*/
FISHHOOK_VISIBILITY
//两个参数分别是rebinding结构体数组,rebindings_nel,也就是 rebindings 的个数也就是数组的长度
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);
/*
* Rebinds as above, but only in the specified image. The header should point
* to the mach-o header, the slide should be the slide offset. Others as above.
*/
FISHHOOK_VISIBILITY
//只在指定的镜像中重新绑定。header应该指向mach-o的header,slide应该是slide的偏移量。如上所述。
int rebind_symbols_image(void *header,
intptr_t slide,
struct rebinding rebindings[],
size_t rebindings_nel);
#ifdef __cplusplus
}
#endif //__cplusplus
#endif //fishhook_h
接下来我们来查看fishhook.m文件,先看下头文件宏定义部分
#include "fishhook.h"
#include <dlfcn.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <mach-o/dyld.h>
#include <mach-o/loader.h>
#include <mach-o/nlist.h>
//判断是否在64位架构下
#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;
//loda command类型表示将文件的64位的段映射到进程地址空间
#define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT_64
//下面就是针对32位架构进行的设计
#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
//如果SEG_DATA_CONST未被定义就去定义SEG_DATA_CONST
#ifndef SEG_DATA_CONST
#define SEG_DATA_CONST "__DATA_CONST"
#endif
然后再看看我们在外界会经常调用的rebind_symbols方法的实现
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
//prepend_rebindings的函数会将整个 rebindings 数组添加到 _rebindings_head 这个链表的头部
int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
//根据上面的prepend_rebinding来做判断,如果小于0的话,直接返回一个错误码回去
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
//Fishhook采用链表的方式来存储每一次调用rebind_symbols传入的参数,每次调用,就会在链表的头部插入一个节点,链表的头部是:_rebindings_head
//根据_rebindings_head->next是否为空来判断是否是第一次调用rebind_symbols方法
if (!_rebindings_head->next) {
//fishhook利用了_dyld_register_func_for_add_image在动态库加载完成之后,做一次符号地址的替换
//调用_dyld_register_func_for_add_image注册监听方法后,当前已经装载的image(动态库等)会立刻触发回调,之后的image会在装载的时候触发回调。dyld在装载的时候,会对符号进行bind,而fishhook则会在回调函数中进行rebind。
//启动的时候做函数替换,注册image装载的监听方法,当dyld链接符号时,调用此回调函数
_dyld_register_func_for_add_image(_rebind_symbols_for_image);
} else {
//遍历已经加载的image,进行的hook
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;
}
接着看链表结构体,保存的就是我们要交换的函数所带信息的结构体数据
//存放重新绑定的信息的链表,这个链表是rebindings_entry类型的
static struct rebindings_entry *_rebindings_head;
struct rebindings_entry {
//要重新绑定的rebinding的结构体
struct rebinding *rebindings;
//rebinding结构体的数量也就是需要重新绑定的函数的数量
size_t rebindings_nel;
//next指针
struct rebindings_entry *next;
};
紧接着我们再来看下prepend_rebindings函数
static int prepend_rebindings(struct rebindings_entry **rebindings_head,
struct rebinding rebindings[],
size_t nel) {
//调用malloc函数开辟链表空间
struct rebindings_entry *new_entry = (struct rebindings_entry *) malloc(sizeof(struct rebindings_entry));
//如果链表创建失败就返回-1
if (!new_entry) {
return -1;
}
//申请rebinding这个结构体大小乘nel大小的空间
new_entry->rebindings = (struct rebinding *) malloc(sizeof(struct rebinding) * nel);
//判断rebindings结构体有没有申请空间成功,如果失败了就释放,返回-1
if (!new_entry->rebindings) {
free(new_entry);
return -1;
}
//void *memcpy(void *dest, const void *src, size_t n);
//memcpy指的是c和c++使用的内存拷贝函数,memcpy函数的功能是从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中。
memcpy(new_entry->rebindings, rebindings, sizeof(struct rebinding) * nel);
//设置所含的rebinding结构体的数量
new_entry->rebindings_nel = nel;
//由于是往链表的头节点插入,所以这里设置new_entry->next为rebindings_head
new_entry->next = *rebindings_head;
*rebindings_head = new_entry;
return 0;
}
其实也就是这样的过程
接下来我们再来看下_rebind_symbols_for_image方法
//此函数内部调用了rebind_symbols_for_image
//参数1:mach_header的地址,参数2:slide 随机偏移量
//由于ASLR的缘故,导致程序实际虚拟内存地址与对应的MachO结构中的地址不一致,有一个偏移量 slide,slide是程序装在时随机生成的随机数。
static void _rebind_symbols_for_image(const struct mach_header *header,
intptr_t slide) {
//这里就是调用rebind_symbols_for_image方法
rebind_symbols_for_image(_rebindings_head, header, slide);
}
我们再来看下rebind_symbols_for_image函数,先看一部分
//完成动态库的binding之后,会回调这个函数。其中slide跟ALSR(Address space layout randomization)有关系,是一个随机的加载地址。
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
const struct mach_header *header,
intptr_t slide) {
Dl_info info;
/*dladdr() 可确定指定的address 是否位于构成进程的进址空间的其中一个加载模块(可执行库或共享库)内,如果某个地址位于在其上面映射加载模块的基址和为该加载模块映射的最高虚拟地址之间(包括两端),则认为该地址在加载模块的范围内。如果某个加载模块符合这个条件,则会搜索其动态符号表,以查找与指定的address 最接近的符号。最接近的符号是指其值等于,或最为接近但小于指定的address 的符号。
*/
/*
如果指定的address 不在其中一个加载模块的范围内,则返回0 ;且不修改Dl_info 结构的内容。否则,将返回一个非零值,同时设置Dl_info 结构的字段。
如果在包含address 的加载模块内,找不到其值小于或等于address 的符号,则dli_sname 、dli_saddr 和dli_size字段将设置为0 ; dli_bind 字段设置为STB_LOCAL , dli_type 字段设置为STT_NOTYPE 。
*/
//获取某个地址的符号信息
if (dladdr(header, &info) == 0) {
return;
}
//下面就是要在加载命令中去寻找linkedit_segment和symtab_cmd以及dysymtab_cmd
//segment_command_64
segment_command_t *cur_seg_cmd;
segment_command_t *linkedit_segment = NULL;
//LC_SYMTAB
struct symtab_command* symtab_cmd = NULL;
struct dysymtab_command* dysymtab_cmd = NULL;
接下来就是去寻找__LINKEDIT和LC_SYMTAB以及LC_DYSYMTAB
//要去寻找load command,所以这里跳过sizeof(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;
//找到__LINKEDIT段,LC_SEGMENT_64是个宏定义代表的是,LC_SEGMENT_64
//__LINKEDIT段 含有为动态链接库使用的原始数据,比如符号,字符串,重定位表条目等等
/*
LC_SYMTAB这个LoadCommand主要提供了两个信息
Symbol Table的偏移量与Symbol Table中元素的个数
String Table的偏移量与String Table的长度
LC_DYSYMTAB
提供了动态符号表的位移和元素个数,还有一些其他的表格索引
LC_SEGMENT.__LINKEDIT
含有为动态链接库使用的原始数据
*/
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) {
//遍历寻找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;
}
}
//如果下面有一项为空就直接返回
if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
!dysymtab_cmd->nindirectsyms) {
return;
}
// Find base symbol/string table addresses
//链接时程序的基址 = __LINKEDIT.VM_Address -__LINKEDIT.File_Offset + silde的改变值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);
// Get indirect symbol table (array of uint32_t indices into symbol table)
//动态符号表地址 = 基址 + 动态符号表偏移量
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
//重新回到跳过header头大小的位置
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;
//先判断command的描述
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
//寻找__DATA和__DATA_CONST的section
if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
continue;
}
//标示了Segment中有多少secetion
for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
//_DATA 加上结构体偏移
//
// 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_t *sect =
(section_t *)(cur + sizeof(segment_command_t)) + j;
//寻找__la_symbol_ptr区
if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
//寻找__nl_symbol_ptr
if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
}
}
}
寻找到section之后就要去调用perform_rebinding_with_section函数了,下面我们就看看这个函数,上面之所以说__nl_symbol_ptr和__la_symbol_ptr的概念是因为__DATA区有两个section和动态符号链接相关:__nl_symbol_ptr 、__la_symbol_ptr。__nl_symbol_ptr为一个指针数组,直接对应non-lazy绑定数据。__la_symbol_ptr也是一个指针数组,通过dyld_stub_binder进行链接实现。
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) {
//typedef struct section_64 section_t;
/*
struct section_64
{
char sectname[16];
char segname[16];
uint64_t addr;
uint64_t size;
uint32_t offset;
uint32_t align;
uint32_t reloff;
uint32_t nreloc;
uint32_t flags;
uint32_t reserved1;
uint32_t reserved2;
};
*/
//对于 symbol pointer sections 和 stubs sections 来说,reserved1 表示 indirect table 数组的 index。用来索引 section's entries. stubs sections在__TEXT段的section
//nl_symbol_ptr和la_symbol_ptrsection中的reserved1字段指明对应的indirect symbol table起始的index
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
//slide+section->addr 就是符号对应的存放函数实现的数组也就是我相应的__nl_symbol_ptr和__la_symbol_ptr相应的函数指针都在这里面了,所以可以去寻找到函数的地址
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
到了这里其实void indirect_symbol_bindings = (void )((uintptr_t)slide + section->addr);找到的就是
所以下面再进行遍历交换
//遍历section里面的每一个符号
for (uint i = 0; i < section->size / sizeof(void *); i++)
{
//找到符号在Indrect Symbol Table表中的值
//读取indirect table中的数据
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作为下标,访问symbol table
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
//获取到symbol_name
char *symbol_name = strtab + strtab_offset;
//判断是否函数的名称是否有两个字符,为啥是两个,因为函数前面有个_,所以方法的名称最少要1个
bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1];
//遍历最初的链表,来进行hook
struct rebindings_entry *cur = rebindings;
while (cur) {
for (uint j = 0; j < cur->rebindings_nel; j++) {
//这里if的条件就是判断从symbol_name[1]两个函数的名字是否都是一致的,以及判断两个
if (symbol_name_longer_than_1 &&
strcmp(&symbol_name[1], cur->rebindings[j].name) == 0)
{
//判断replaced的地址不为NULL以及我方法的实现和rebindings[j].replacement的方法不一致
if (cur->rebindings[j].replaced != NULL &&
indirect_symbol_bindings[i] != cur->rebindings[j].replacement)
{
//让rebindings[j].replaced保存indirect_symbol_bindings[i]的函数地址
*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
}
//将替换后的方法给原先的方法,也就是替换内容为自定义函数地址
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
goto symbol_loop;
}
}
cur = cur->next;
}
symbol_loop:;
}
}
最后看看MachO文件中的一些数据结构
__LINKEDIT段
LC_SYMTAB
- LC_DYSYMTAB
根据这里的IndSym Table Offset的value,我们就可以找到Dynamic Symbols Table
下面的Data值000000A2其实就是一个下标,对应的是符号表的下标,对应着Symbol Table
需要注意的是上面的000000F7其对应的是在String Table里面的偏移,所以其实上面的
//以symtab_index作为下标,访问symbol table
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
//获取到symbol_name
char *symbol_name = strtab + strtab_offset;
这段程序就是去寻找String Table当中的函数的名称的
这里需要注意的是在TEXT.stubs 里面本质上是说的是一小段会直接跳入la_symbol_ptr的表对应项指针指向的地址的代码,因为动态库加载的地址我们在编译的时候肯定是无法确定的
而TEXT.stub_helper 这里面就是辅助函数。上述提到的la_symbol_ptr的表中对应项的指针在没有找到真正的符号地址的时候,都指向这里
也就是说__la_symbol_ptr 中的数据被第一次调用时会通过 dyld_stub_binder 进行相关绑定,也就是说 __la_symbol_ptr刚开始的时候数据在初始状态都被 bind 成 stub_helper,接着 当加载相应的动态链接库,执行具体的函数实现的时候,dyld_stub_binder会进行绑定函数地址,也就是说此时 __la_symbol_ptr 也获取到了函数的真实地址,下面可以清楚的看到初始状态的时候,确实都bind到了stud_helper,而Non-Lazy Symbol 里面的是non-lazy符号在程序启动所依赖的动态库被链接的时候立即link到可执行文件
再看stud_helper的内容
其实可以理解成,在程序运行的时候时,动态链接的一个函数func( )地址记录是保存在DATA segment下的la_symbol_ptr的里面的中;但是在初始化的时候,程序只知道func函数的符号名而不知道函数的实现地址;在第一次调用的时候,程序会通过TEXT segment中的stub_helper取得绑定信息,然后通过dyld_stub_binder来更新la_symbol_ptr中的符号实现地址;这样下次再次调用的时候,就可以通过la_symbol_ptr直接找到func函数的实现;如果我们需要用黑魔法去替换func函数的实现,只需要去修改__la_symbol_ptr就可以了
__nl_symbol_ptr 中的数据就是在动态库绑定时进行加载
附上官方的图,通过lazy Symbol Pointer Table表去寻找Indirect Symbol Table表,然后再通过Indirect Symbol Table表去寻找Symbol Table表,然后再根据Symbol Table表当中的内容去寻找String Table