美文网首页
fishhook扫盲

fishhook扫盲

作者: 李永开 | 来源:发表于2022-05-17 17:20 被阅读0次

一.啥是fishhook

fishhook是一个运行在IOS的模拟器和真机环境,在MACH-O文件中能够动态重新绑定符号的简单的库。

二.咋用呢

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    NSLog(@"");//先执行一次系统函数,共享函数库里才会加载进这个方法 -> 待确定
//    struct rebinding {
//      const char *name;   //需要hook的函数名称,c字符串
//      void *replacement;  //新函数的地址
//      void **replaced;    //原函数的地址
//    };
    struct rebinding logRebinding;
    logRebinding.name = "NSLog";
    logRebinding.replacement = myLog;
    logRebinding.replaced = (void *)&sys_nslog;
    
    struct rebinding rebs[1] = {logRebinding};
    
    //rebs: 存放rebinding结构体的数组
    //1: 数组的长度
    rebind_symbols(rebs, 1);
    
    NSLog(@"originLog");
    return YES;
}

static void (*sys_nslog)(NSString *format, ...);
void myLog(NSString *format, ...) {
    format = @"newLog";
    sys_nslog(format);
}

//2022-05-17 14:31:10.058392+0800 fishhookdemo[2753:1073295] newLog

当我们打印NSLog(@"originLog");时,输出了newLog,很神奇

三.为啥这么神奇?

道理很简单,就是找到NSlog的位置,将其替换成我们自己的函数地址就行,只是说这个找寻过程有点麻烦而已。
这就需要我们具备一定的铺垫知识。

3.1 动态库共享缓存(dyld shared cache)

在iOS系统中,每个程序依赖的动态库都需要通过dyld(位于/usr/lib/dyld)一个一个加载到内存,然而如果在每个程序运行的时候都重复的去加载一次,势必造成运行缓慢,为了优化启动速度和提高程序性能,共享缓存机制就应运而生。所有默认的动态链接库被合并成一个大的缓存文件,放到/System/Library/Caches/com.apple.dyld/目录下,按不同的架构保存着.

NSlog就在共享动态库里面

3.2 懒加载符号表

  • 我们把项目里的NSLog函数都注释掉
图片.png
  • 打开NSLog函数,使用machoview看下,发现NSLog函数是存放在Lazy Symbol Pointers即懒加载段里面, 偏移量为00008010
图片.png

NSLog之前打一个断点,然后使用image list拿到基地址0x0000000100814000

图片.png
  • 基地址+偏移量按理说应该可以拿到NSLog函数的内存地址
    16进制打印
图片.png
  • 查看NSLog函数的指针,反汇编一下,发现并不是NSLog函数
图片.png
图片.png
  • 跳过NSLog这个断点,重新看下0x000000010081c018地址存放的数据,发现地址变了
图片.png
  • 反编译下
    震惊,发现该地址是NSLog函数
图片.png
  • 结论: 懒加载表中的函数,是在函数运行一次后才会确定函数的地址
    所以使用fishhook需要先调用下要hook的函数

3.3 符号绑定& 重绑定符号

NSLog函数属于共享缓存库的函数,所以在编译期间,是不能确定它的地址的。只有在运行时,dyld才会从共享缓存库拿到NSLog函数并且确定它的地址,写入到懒加载符号表里

四.咋实现的呢

4.1 记录下简单的步骤

  1. 找到字符串表symtab_cmd->stroffsymtab_segment
  2. 找到符号表LC_SYMTABsegment
  3. 找到动态符号表LC_DYSYMTABsegment
  4. indirect_symtab = LC_DYSYMTAB->indirectsymoff拿到间接符号表indirect_symtab,间接符号表其实就是动态库的符号表
  5. 拿到懒加载section__la_symbol_ptr,这里面就有NSLog函数的地址
  6. indirect_symbol_indices = indirect_symtab + __la_symbol_ptr.reserved1得到__la_symbol_ptr模块在indirect_symtab表的起始位置 7 8 9
  7. for循环取出LC_SYMTAB.7.n_un.n_strx拿到其在字符串表symtab_segment中的位置, 取出字符串NSLog
  8. 判断要替换的字符串和取出的字符串是否一样,一样的话将__la_symbol_ptr替换成我们的函数

4.2 搞个超简版demo,来替换NSLog

调用下面的函数,你会发现不管你NSLog什么,打印出来都是"替换NSLog成功啦"

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    NSLog(@"");//先执行一次系统函数,共享函数库里才会加载进这个方法 -> 因为是懒
    
    //验证
    replaceNSLog(youNSLog);
    NSLog(@"");
    return YES;
}

static void youNSLogFuc() {
    printf("替换NSLog成功啦");
}
void (*youNSLog)(void) = youNSLogFuc;

fishcook.h

#ifndef fishcook_h
#define fishcook_h

#include <stdio.h>


#ifdef __cplusplus
extern "C" {
#endif //__cplusplus

//替换成你自己的函数
int replaceNSLog(void *youNSLog);


#ifdef __cplusplus
}
#endif //__cplusplus


#endif /* fishcook_h */

fishcook.m

//
//  fishcook.c
//  fishhookdemo
//
//  Created by liyongkai on 2022/7/21.
//

#include "fishcook.h"
#include <mach-o/dyld.h>
#include <mach/mach.h>
#include <mach-o/loader.h>
#include <mach-o/nlist.h>


void *_youNSLog;

/// 替换函数
/// @param symtab 符号表
/// @param strtab 字符串表
/// @param section __la_symbol_ptr
/// @param indirect_symtab 间接符号表
static void replaceImp(intptr_t vmaddr_slide,
                       struct nlist_64 *symtab,
                       char *strtab,
                       struct section_64 *section,
                       uint32_t *indirect_symtab) {
    
    //拿到__la_symbol_ptr section里面的内容在间接表中的起始位置
    uint32_t *indirect_symtab_indices = indirect_symtab + section->reserved1;
    
    for (uint i = 0; i < section->size / sizeof(void *); i ++) {
        uint32_t tabIndex = indirect_symtab_indices[i];
        
        //通过符号表拿到nslog在字符串表中的位置
        uint32_t strtab_offset = symtab[tabIndex].n_un.n_strx;
        
        //拿到字符串
        char *symbol_name = strtab + strtab_offset;
        
        //如果一样就替换
        if (strcmp(&symbol_name[1], _youNSLog)) {
            
            void **la_symbol_prt_address = (void **)(vmaddr_slide + section->addr);
            la_symbol_prt_address[i] = _youNSLog;
            
            return;
        }
    }
}




static void allImages(const struct mach_header* header, intptr_t vmaddr_slide) {
    
    //1.能到符号表segment和动态符号表segment
    //记录当前的segmnet
    struct segment_command_64 *cur_seg_cmd;
    
    //符号表LC_SYMTAB
    struct symtab_command* symtab_cmd = NULL;
    //动态符号表LC_DYSYMTAB
    struct dysymtab_command* dysymtab_cmd = NULL;

    //第一个segment的地址
    uintptr_t cur = (uintptr_t )header + sizeof(struct mach_header_64);
    
    //循环遍历,找到  符号表LC_SYMTAB, 动态符号表LC_DYSYMTAB
    for (uint i = 0; i < header->ncmds; i ++, cur += cur_seg_cmd->cmdsize) {
        cur_seg_cmd = (struct segment_command_64 *)cur;
        
        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) {
        // 符号表或者动态符号表的segment为空,说明没有动态库,return
        return;
    }
    
    
    
    
    //2.通过segment拿到符号表section, string table, 间接符号表
    uintptr_t mh_addr = (uintptr_t)_dyld_get_image_header(0);

    //拿到符号表section
    struct nlist_64 *symtab = (struct nlist_64 *) (mh_addr +symtab_cmd->symoff);
    
    //拿到string table
    char *strtab = (char *)(mh_addr + symtab_cmd->stroff);
    
    //拿到间接符号表, 也就是动态库的符号表
    uint32_t *indirect_symtab = (uint32_t *)(mh_addr + dysymtab_cmd->indirectsymoff);
    
    //找到__got section 和 la_symbol_ptr section, 他们都在__DATA segment里面
    cur = (uintptr_t)header + sizeof(struct mach_header_64);
    for (uint i = 0; i < header->ncmds; i ++, cur += cur_seg_cmd->cmdsize) {
        cur_seg_cmd = (struct segment_command_64 *)cur;
        if (cur_seg_cmd->cmd == LC_SEGMENT_64) {
            if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
                strcmp(cur_seg_cmd->segname, "__DATA_CONST") != 0) {
              continue;
            }
            
            for (uint j = 0; j < cur_seg_cmd->nsects; j ++) {
                struct section_64 *sect = (struct section_64 *)(cur + sizeof(struct segment_command_64)) + j;
                if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
                    // 懒加载符号表
                    replaceImp(vmaddr_slide, symtab, strtab, sect, indirect_symtab);
                    return;
                }
            }
        }
    }
}


int replaceNSLog(void *youNSLog) {
    _youNSLog = youNSLog;
    
    //监听各个image
    _dyld_register_func_for_add_image(allImages);
    
    return 0;
}

五.总结

说白了,fishhook代码看似复杂,主要mach-O内部结构体复杂,找来找去很是麻烦。但如果有一天你对mach-O了如执掌,那么fishhook也就不攻自破了。

  • fishhook只能hook动态库里面的函数,包含__got和la_symbol_ptr段
  • 使用fishhook需要先调用下要hook的函数
  • 将hook代码尽量提前

六.Q

总结哪些函数属于OC函数,哪些属于共享缓存库

相关文章

网友评论

      本文标题:fishhook扫盲

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