fishHook源码分析

2,915 阅读12分钟

fishhookfacebook提供的一个在运行时动态修改外部c函数的的一个三方库,只有短短的200多行代码。使用的代码实例如下:

//----------------------------------简单更改系统NSLog的调用!----------------------------------
//函数指针,用来保存原始的函数地址
static void(*sys_nslog)(NSString *format, ...);

//定义一个新的函数
void myNSLog(NSString *format, ...){
    format = [NSString stringWithFormat:@"修改后的---%@", format];
    //由于需要使用NSLog内部实现,所以保留原始调用
    sys_nslog(format);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"123");
    
    //实现交换
    //rebinding结构体
    struct rebinding nslog;
    nslog.name = "NSLog";//需要替换的函数名称
    nslog.replacement = myNSLog;//新的函数地址
    nslog.replaced = (void *)&sys_nslog;//原始函数指针
    //定义数组!里面放rebinding结构体
    struct rebinding rebs[1] = {nslog};
    
    /** 用于重新绑定符号
     * arg1: 存放rebinding结构体的数组
     * arg2: 数组的长度
     */
    rebind_symbols(rebs, 1);
    NSLog(@"123");
    
}

执行效果如下:

fishhookDemo[12388:4000541] 123
fishhookDemo[12388:4000541] 修改后的---123

在对fishhook源码进行分析之前,我们需要了解一下Mach-O文件

Mach-O文件的简介

文件类型

对于OSXiOS来说,Mach-O是其可执行文件的格式,主要包括以下几种文件类型:

* Executable:	应用的二进制文件
* Dylib:	动态库
* Bundle: 	不能被链接的动态库,只能通过dlopen()加载
* image:	包括Executable、Dylib和Bundle
* Framework:	包含Dylib以及资源文件和头文件的文件夹

Mach-O格式

如上图所示,Mach-O文件主要包含了Header、LoadCommands以及Segment几个主要的部分。 其中有几个结构体需要我们了解一下:

segment_command_64

对应了load Commands中的每一个segment command

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

对应了segment command中的每一个section

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

nlist_64

表示的是符号表SymbolTable中的每一个实体结构体

struct nlist_64 {
    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 */
    uint16_t n_desc;       /* see <mach-o/stab.h> */
    uint64_t n_value;      /* value of this symbol (or stab offset) */
};

大致了解了上述结构体之后,我们在看一下fishhook的源码

源码解析

首先看两个结构体:

1. struct rebinding

struct rebinding {
  const char *name;	//需要HOOK的函数名称,C字符串
  void *replacement;	//新函数的地址
  void **replaced;	//原始函数地址的指针!
};

2. struct rebindings_entry

struct rebindings_entry {
    struct rebinding *rebindings; 	// 需要进行HOOK的函数数组
    size_t rebindings_nel;	  	// 数组的个数
    struct rebindings_entry *next;	// 类似链表结构,指向下一个entry
};

static struct rebindings_entry *_rebindings_head; // 静态的全局entry链表,用来保存需要进行HOOK的各个entry,每一次HOOK会生成一个entry结点

fishhook的入口函数有两个:

//对于rebingdings数组中的每一个rebinding结构体来说,为每一个进程中的以及未来将被进程加载的镜像文件image重新绑定有着特定名称的外部间接符号的指针,使他们指向replacement函数。
//如果rebind_functions调用了多次,用来重新绑定的符号被加到了rebindings链表中,如果一个符号被绑定了多次,后来的rebinding优先处理
FISHHOOK_VISIBILITY
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);

//只在特定的image中执行。
FISHHOOK_VISIBILITY
int rebind_symbols_image(void *header,
                         intptr_t slide,
                         struct rebinding rebindings[],
                         size_t rebindings_nel);

rebind_symbols_image函数只是对rebind_symbols函数的简化,我们只看rebind_symbols函数。

rebind_symbols

int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
    // 处理需要重新绑定的符号
    int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
    // retval为负数表示出现了异常,直接返回
    if (retval < 0) {
        return retval;
    }
    //判断是不是第一次调用。
    if (!_rebindings_head->next) {
       // 第一次调用需要注册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;
}

rebind_symbols首先处理了传入的rebindings数组

prepend_rebindings

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;
    }
    // 存储传入的rebindings指针
    new_entry->rebindings = (struct rebinding *) malloc(sizeof(struct rebinding) * nel);
    if (!new_entry->rebindings) {
        free(new_entry);
        return -1;
    }
    // 复制一份rebindings数组,新节点的rebindings指针赋值
    memcpy(new_entry->rebindings, rebindings, sizeof(struct rebinding) * nel);
    new_entry->rebindings_nel = nel;
    // 头插法插入链表中并且重置链表的头结点rebindings_head
    new_entry->next = *rebindings_head;
    *rebindings_head = new_entry;
    return 0;
}
  • prepend_rebindings的函数会将整个 rebindings 数组生成一个rebindings_entry的结构体,添加到 _rebindings_head 这个链表的头部
  • Fishhook采用链表的方式来存储每一次调用rebind_symbols传入的参数,每次调用,就会在链表的头部插入一个节点,链表的头部是:_rebindings_head
  • 正常情况返回0,异常情况返回-1

关于_dyld_register_func_for_add_image

// 注册dyld回调,当镜像被加载或者卸载的时候由dyld调起。
// 在_dyld_register_func_for_add_image过程中已经被dyld加载的image会立刻进入回调。之后的image在dyld加载时触发回调。
// _dyld_register_func_for_remove_image函数在镜像文件的terminators方法之后,从内存中释放之前调用
extern void _dyld_register_func_for_add_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide))    __OSX_AVAILABLE_STARTING(__MAC_10_1, __IPHONE_2_0);
extern void _dyld_register_func_for_remove_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide)) __OSX_AVAILABLE_STARTING(__MAC_10_1, __IPHONE_2_0);

由此可见上述rebind_symbols方法在第一次进行重新绑定时会注册_rebind_symbols_for_image来触发回调,确保已加载的image以及未来加载的image中的符号都能被重新绑定。

如果不是第一次重新绑定,即链表_rebindings_head->next不为空时,已加载的image的回调已经触发过了,未加载的不用担心,我们只需要对已经在的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

static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
                                     const struct mach_header *header,
                                     intptr_t slide) {
    //这个dladdr函数就是在程序里面找header
    Dl_info info;
    if (dladdr(header, &info) == 0) {
        return;
    }
    //下面就是定义好几个变量,准备从MachO里面去找!
    segment_command_t *cur_seg_cmd;
    segment_command_t *linkedit_segment = NULL;
    struct symtab_command* symtab_cmd = NULL;
    struct dysymtab_command* dysymtab_cmd = NULL;
    // head是一个指针,存放的地址= pageZero+ASLR的值,再加上mach_header_t的大小,可以得到load Commands的地址
    uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
    // 遍历commands,确定linkedit、symtab、dysymtab这几个command的位置
    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;
    }
    
    // Find base symbol/string table addresses
    // 链接时程序的基址 = __LINKEDIT.VM_Address -__LINKEDIT.File_Offset + silde的改变值
    uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
    //    printf("地址:%p\n",linkedit_base);
    //符号表的地址 = 基址 + 符号表偏移量
    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);
    // 重置回到loadCommand开始的位置
    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) {
            //寻找到data段
            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 header
                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);
                }
            }
        }
    }
}

rebind_symbols_for_image主要就是从Macho-O文件中找到对应的懒加载符号段和非懒加载符号端以及符号表、动态符号表和字符串表,因为Mach-O文件的格式问题,我们并不能直接得到需要的内容,只能沿着相应的路径一步步寻找,得到我们想要的值。

perform_rebinding_with_section

static void perform_rebinding_with_section(struct rebindings_entry *rebindings, // 重新绑定的数组
                                           section_t *section, 			// section DATA段中的懒加载符号or非懒加载符号
                                           intptr_t slide, 			// 当前image的ASLR
                                           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;
    //slide+section->addr 就是符号对应的存放函数实现的数组也就是我相应的__nl_symbol_ptr和__la_symbol_ptr相应的函数指针都在这里面了,所以可以去寻找到函数的地址
    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中的数据,得到符号在DATA段中懒加载or非懒加载section的位置
        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,因为此时有的image可能还没有加载,因此需要每次都遍历整个链表以及链表每一个结点内部的数组
        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:;
    }
}

当我们得到所需要的各个参数后,perform_rebinding_with_section做了一下几件事情:

  1. 找到懒加载符号/非懒加载符号在间接符号表indirect_symtab中的起始index
  2. indirect_symtab中获取对应的符号的Symbol Table index
  3. Symbol Table中获取对应符号的String Table Index
  4. 比较String Tabel中的对应index位置的值和我们传入的rebinding.name是否匹配
  5. 修改Data段中对应的符号指针指向的地址,指向我们自己的函数实现,传入的replaced指针指向原始的函数实现。
  6. 遍历链表每一个中的数组

反汇编代码验证

首先看一下mach-O文件中懒加载符号NSLog的偏移,记为0x8010

NSLog执行前

在代码中打下断点: 通过image list指令打印出镜像列表,找到我们的fishhookDemo的地址为0x0000000102104000,接下来读取0x0000000102104000+0x8010位置的值,并且进行反汇编 上面的代码看起来没什么头绪,原因是我们的断点打在NSLog上,因为此时代码还没有执行,因此NSLog并没有进行绑定。我们再回到mach-O中,发现其实在懒加载指针中NSLog是有值0x10006964的,我们到这个路径一探究竟。 对比一下上图的汇编代码和我们反汇编的结果,发现0x10000696c+0x2104000刚好等于0x10210a96c,其中0x2104000是我们的ASLR的值。这也解释了为什么此时NSLog指向了上文中的一堆汇编,因为mach-O文件里面就是这么写的,至于原因,其实是因为这里的汇编代码是系统利用非懒加载符号dyld_stub_binder辅助进行懒加载符号绑定的过程,经过绑定之后,会指向正确的NSLog位置。

NSLog执行后,此时还没有hook

接下来断点走一步,打印出字符串"123",我们在看一下此时NSLog对应的反汇编代码 此时NSLog对应的函数已经发生了变化,变成了Foundation中的正确实现。

NSLog被hook之后

断点接着往下走,执行到hook代码之后,再看一下反汇编。 此时,又发生了变化,变成了我们自己的实现myNSLog。也就验证了我们的hook NSLog成功了。

官方图例

扩展

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可以用来获取获取某个地址的符号信息 在dyld3中,实现如下:

// 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 。
int dladdr(const void* addr, Dl_info* info)
{
    ...
    if ( info == NULL )
        return 0; // failure

    addr = stripPointer(addr);

    __block int         result = 0;
    const MachOLoaded*  ml     = nullptr;
    const char*         path   = nullptr;
    // 寻找当前addr所对应的image信息
    if ( gAllImages.infoForImageMappedAt(addr, &ml, nullptr, &path) ) {
        info->dli_fname = path;
        info->dli_fbase = (void*)ml;

        uint64_t symbolAddr;
        // 地址等于image的基址
        if ( addr == info->dli_fbase ) {
            // special case lookup of header
            info->dli_sname = "__dso_handle";
            info->dli_saddr = info->dli_fbase;
        }
        // 找到最近的符号
        else if ( ml->findClosestSymbol((long)addr, &(info->dli_sname), &symbolAddr) ) {
            info->dli_saddr = (void*)(long)symbolAddr;
            // never return the mach_header symbol
            // 不返回mach_header符号
            if ( info->dli_saddr == info->dli_fbase ) {
                info->dli_sname = nullptr;
                info->dli_saddr = nullptr;
            }
            // strip off leading underscore
            else if ( (info->dli_sname != nullptr) && (info->dli_sname[0] == '_') ) {
                info->dli_sname = info->dli_sname + 1;
            }
        }
        else {
            info->dli_sname = nullptr;
            info->dli_saddr = nullptr;
        }
        result = 1;
    }
    ....
    return result;
}

NSLog hook成功了吗?

当我们在使用多参数的NSLog时,会出现崩溃,这应该是NSLog的可变参数导致的,和只使用fishhook不能直接hook objc_msgSend一样,后续再做研究。

fishhook可以hook什么函数?

通过源码分析我们可以发现,fishhook是通过修改外部符号对应的指针所指向的地址这种方式来进行hook的,也就是说如果在mach-O文件里面已经确定的符号,即不需要通过指针进行绑定的符号是无能为力的,例如我们在代码里面写的c函数就是不能被hook的,而NSLog函数是属于Foundation框架的,是一个外部符号,因此可以被hook到。