ios逆向 - fishhook的源码分析

一 知识回顾

在上一节,我们分析了fishhook的原理, 知道fishhook 通过动态修改懒加载或非懒加载指针表来达到hook 的目的, 这篇文章就是讲一下fishhook 的具体是实现

二 fishhook的源码分析

预备知识
1:外部函数调用前的样式以及加载链接的过程

在Mach-o 文件中,包含__DATA.__la_symbol_ptr 和 __DATA.__nl_symbol_ptr 这两个指针表,分别为lazy binding指针表和non lazy binding指针表。并且这两个指针表,保存着与字符串标对应的函数指针。


image.png

调用外部函数时,比如调用print函数,会采用下面的方式调用

image.png

进入imp __stub_printf查看

image.png

我们发现imp___stubs__printf 这个指针指向了0x100001010,而这个地址是在__la_symbol_ptr 表中创建的指针变量(上一章中提到)

image.png

结合我们上一篇文章讲的__DATA.__la_symbol_ptr 创建的指针变量的内容一开始是0, 我们发现实际上这个指针变量的内容不是0,而是一个地址01 00 00 0f 84,继续追踪这个地址,发现01 00 00 0f 84指向在 __TEXT.__stubs 这个 Section中的一串代码


image.png

我们称这串代码是imp___stubs__printf 的桩,也就是说__DATA.__la_symbol_ptr 创建的指针变量一开始指向的是桩代码

继续追踪0x100000f74这个地址,然后会跳到 __TEXT.__stub_helper

image.png

在这里通过 _stub_helper 来调用 dyld_stu_binder 方法计算 printf 函数的真实地址,

binder 方法的作用简单来讲就是计算对应的函数地址进行绑定

在第二次输出 0x10001010 的值的时候,__DATA.__la_symbol_ptr 中指向 printf 地址的值已经发生了改变,即指向桩代码的地址替换成了真实函数的地址

2:认识Dynamic Symbol Table 动态符号表

用来加载动态库的时候导出的符号表,lazy指针表与之一一对应的符号表(Indirect Symbols)。


image.png

我们可以看到,dyld 通过动态符号表去跳转到__DATA.__la_symbol_ptr 的0x100001010处,并最终执行到dyld_stu_binder函数,内部拿到第三方库的GOT表,然后在拿到库PLT表开始解析地址

源码分析

fishhook 的使用

  struct rebinding nslogBind;
    //函数的名称
    nslogBind.name = "NSLog";
    //新的函数地址
    nslogBind.replacement = myMethod;
    //保存原始函数地址变量的指针
    nslogBind.replaced = (void *)&old_nslog;
    
    //定义数组
    struct rebinding rebs[] = {nslogBind};
    
    /**
     arg1: 存放rebinding结构体的数组
     arg2: 数组的长度
     */
    rebind_symbols(rebs, 1);

这里一看主要是分析rebind_symbols(rebs, 1)函数


int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
  // 维护一个 rebindings_entry 的结构
  // 将 rebinding 的多个实例组织成一个链表
  int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
  // 判断是否 malloc 失败,失败会返回 -1
  if (retval < 0) {
    return retval;
  }
  // _rebindings_head -> next 是第一次调用的标志符,NULL 则代表第一次调用
  if (!_rebindings_head->next) {
     // 第一次调用,将 _rebind_symbols_for_image 注册为回调
    _dyld_register_func_for_add_image(_rebind_symbols_for_image);
  } else {
    // 先获取 dyld 镜像数量
    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;
}

为了将多次绑定时的多个符号组织成一个链式结构,fishhook 自定义了一个链表结构来组织这个逻辑,其中每个节点的数据结构如下

struct rebindings_entry {
    struct rebinding *rebindings; // rebinding 数组实例
    size_t rebindings_nel; // 元素数量
    struct rebindings_entry *next; // 链表索引
};

// 全局量,直接拿出表头
static struct rebindings_entry *_rebindings_head;

在 prepend_rebindings 方法中,fishhook 会维护这个结构:

/**
 * prepend_rebindings 用于 rebindings_entry 结构的维护
 * struct rebindings_entry **rebindings_head - 对应的是 static 的 _rebindings_head
 * struct rebinding rebindings[] - 传入的方法符号数组
 * size_t nel - 数组对应的元素数量
 */
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 数组中 copy 到 new_entry -> rebingdings 成员中
    memcpy(new_entry->rebindings, rebindings, sizeof(struct rebinding) * nel);
    // 为 new_entry -> rebindings_nel 赋值
    new_entry->rebindings_nel = nel;
    // 为 new_entry -> newx 赋值,维护链表结构
    new_entry->next = *rebindings_head;
    // 移动 head 指针,指向表头
    *rebindings_head = new_entry;
    return 0;
}

用图说明一下上述的结构:


链表模型

由于我们的 nslog 是 dyld 加载的系统库方法,所以 _rebindings_head -> next 在第一次调用的时候为空,因为这个时候只创建了第一个节点,因为没有做过替换符号,所以会调用 _dyld_register_func_for_add_image 来注册 _rebind_symbols_for_image 回调方法,之后程序每次加载动态库的时候,都会去调用该方法。如果不是第一次替换符号,则遍历已经加载的动态库。

接续分析:
_dyld_register_func_for_add_image 作用是啥?
首先它是dyld 内部的方法, _dyld_register_func_for_add_image 这个方法当镜像[动态库] 被 load 或是 unload 的时候都会由 dyld 主动调用。当该方法被触发时,会为每个镜像触发其回调方法。之后则将其镜像与其回电函数进行绑定(但是未进行初始化)。使用 _dyld_register_func_for_add_image 注册的回调将在镜像中的 terminators 启动后被调用。

/**
 * _rebind_symbols_for_image 是 rebind_symbols_for_image 的一个入口方法
 * 这个入口方法存在的意义是满足 _dyld_register_func_for_add_image 传入回调方法的格式
 * header - Mach-O 头
 * slide - intptr_t 持有指针
 */
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 方法描述的也就是整个 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;
    }

    // 声明几个查找量:
    // linkedit_segment, symtab_command, dysymtab_command
    segment_command_t *cur_seg_cmd;
    segment_command_t *linkedit_segment = NULL;
    struct symtab_command* symtab_cmd = NULL;
    struct dysymtab_command* dysymtab_cmd = NULL;

    // 初始化游标
    // header = 0x100000000 - 二进制文件基址默认偏移
    // sizeof(mach_header_t) = 0x20 - Mach-O Header 部分
    // 首先需要跳过 Mach-O Header
    uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
    // 遍历每一个 Load Command,游标每一次偏移每个命令的 Command Size 大小
    // header -> ncmds: Load Command 加载命令数量
    // cur_seg_cmd -> cmdsize: Load 大小
    for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
        // 取出当前的 Load Command
        cur_seg_cmd = (segment_command_t *)cur;
        // Load Command 的类型是 LC_SEGMENT
        if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
            // 比对一下 Load Command 的 name 是否为 __LINKEDIT
            if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
                // 检索到 __LINKEDIT
                linkedit_segment = cur_seg_cmd;
            }
        }
        // 判断当前 Load Command 是否是 LC_SYMTAB 类型
        // LC_SEGMENT - 代表当前区域链接器信息
        else if (cur_seg_cmd->cmd == LC_SYMTAB) {
            // 检索到 LC_SYMTAB
            symtab_cmd = (struct symtab_command*)cur_seg_cmd;
        }
        // 判断当前 Load Command 是否是 LC_DYSYMTAB 类型
        // LC_DYSYMTAB - 代表动态链接器信息区域
        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;
    }

    // slide: ASLR 偏移量
    // vmaddr: SEG_LINKEDIT 的虚拟地址
    // fileoff: SEG_LINKEDIT 地址偏移
    // 式①:base = SEG_LINKEDIT真实地址 - SEG_LINKEDIT地址偏移
    // 式②:SEG_LINKEDIT真实地址 = SEG_LINKEDIT虚拟地址 + ASLR偏移量
    // 将②代入①:Base = SEG_LINKEDIT虚拟地址 + ASLR偏移量 - SEG_LINKEDIT地址偏移
    uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
    // 通过 base + symtab 的偏移量 计算 symtab 表的首地址,并获取 nlist_t 结构体实例
    nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
    // 通过 base + stroff 字符表偏移量计算字符表中的首地址,获取字符串表
    char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
    // 通过 base + indirectsymoff 偏移量来计算动态符号表的首地址
    uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);

    // 归零游标,复用
    cur = (uintptr_t)header + sizeof(mach_header_t);
    // 再次遍历 Load Commands
    for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
        cur_seg_cmd = (segment_command_t *)cur;
        // Load Command 的类型是 LC_SEGMENT
        if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
            // 查询 Segment Name 过滤出 __DATA 或者 __DATA_CONST
            if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
                strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
                continue;
            }
            // 遍历 Segment 中的 Section
            for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
                // 取出 Section
                section_t *sect = (section_t *)(cur + sizeof(segment_command_t)) + j;
                // flags & SECTION_TYPE 通过 SECTION_TYPE 掩码获取 flags 记录类型的 8 bit
                // 如果 section 的类型为 S_LAZY_SYMBOL_POINTERS
                // 这个类型代表 lazy symbol 指针 Section
                if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
                    // 进行 rebinding 重写操作
                    perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
                }
                // 这个类型代表 non-lazy symbol 指针 Section
                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 方法展示了冲绑定过程中,所有计算地址的流程。浏览过代码之后思考一个问题:为了完成重绑定的操作,我们需要获取哪些地址信息呢?

1. 获取 Linkedit Base Addr [ 很重要!!!]

fishhook 绝大多数重要的地址计算都要使用到或是间接使用到 Linkedit Base Addr[链接段的首地址或者Linkedit Segment 在文件中的首地址]。链接阶段: 无论是编译完成后的静态链接还是动态链接,只要调用其他文件的函数都需要链接,所以链接段的首地址很重要

为什么 Linkedit Segment 首地址信息十分重要,因为在 Load Command 中,LC_SYMTAB 和 LC_DYSYMTAB 的中所记录的 Offset 都是基于 __LINKEDIT 段的。而 LC_SYMTAB 中通过偏移量可以拿到symtab 符号表首地址、strtab 符号名称字符表首地址以及indirect_symtab 跳转表首地址。
我们拿到 Indirect Symbols 的首地址 indirect_symtab 再加上 LC_SEGMENT.__DATA 中任何一个 Section 信息的 reverved1 字段就可以获取到对应的 Indirect Address 信息。在这之后我们可以遍历每一个 Indirect Symbols,并以索引方式获取到每一个 nlist 结构的符号,从符号中获取到符号名字符串在字符表中的偏移量,进而继续获取符号名。

image.png

2. 二次遍历 Load Command 的目的

第一次遍历的目的在第一个疑问中已经解释的很清楚了,遍历目的有三个,为了找出 LC_SEGMENT(__LINKEDIT)、LC_SYMTAB 和 LC_DYSYMTAB 三个 Load Command,从而我们可以计算出 Base Address、Symbol Table、Dynamic Symbol 和 String Table 的位置。

而在第二次遍历中,我们需要获取的是 LC_SEGMENT(__DATA) 中 __nl_symbol_ptr 和 __la_symbol_ptr 这两个 Section。其目的是为了确定 lazy binding指针表 和 non lazy binding指针表 在 Dynamic Symbol 中对应的位置,方法就是获取到 reserved1 字段。这个在后面我们会做一个验证实现。

重绑定

在一切基址准备好之后,开始进行重绑定 perform_rebinding_with_section 方法:

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 Symbol 表中检索到对应位置
    uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
    // 获取 _DATA.__nl_symbol_ptr(或__la_symbol_ptr) Section
    // 已知其 value 是一个指针类型,整段区域用二阶指针来获取
    void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
    // 用 size / 一阶指针来计算个数,遍历整个 Section
    for (uint i = 0; i < section->size / sizeof(void *); i++) {
        // 通过下标来获取每一个 Indirect Address 的 Value
        // 这个 Value 也是外层寻址时需要的下标
        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;
        }
        // 获取符号名在字符表中的偏移地址
        uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
        // 获取符号名
        char *symbol_name = strtab + strtab_offset;
        // 过滤掉符号名小于 4 位的符号
        if (strnlen(symbol_name, 2) < 2) {
            continue;
        }
        // 取出 rebindings 结构体实例数组,开始遍历链表
        struct rebindings_entry *cur = rebindings;
        while (cur) {
            // 对于链表中每一个 rebindings 数组的每一个 rebinding 实例
            // 依次在 String Table 匹配符号名
            for (uint j = 0; j < cur->rebindings_nel; j++) {
                // 符号名与方法名匹配
                if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
                    // 如果是第一次对跳转地址进行重写
                    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[i] = cur->rebindings[j].replacement;
                    // 完成后不再对当前 Indirect Symbol 处理
                    // 继续迭代到下一个 Indirect Symbol
                    goto symbol_loop;
                }
            }
            // 链表遍历
            cur = cur->next;
        }
    symbol_loop:;
    }
}

这段方法主要描述了替换 __DATA.__la_symbol_ptr 和 __DATA.__la_symbol_ptr 的 Indirect Pointer 主要过程。从 reserved1 字段获取到 Indirect Symbols 对应的位置。从中我们可以获取到指定符号的偏移量,这个偏移量主要用来在 String Table 中检索出符号名称字符串。之后我们找到 __DATA.__la_symbol_ptr 和 __DATA.__la_symbol_ptr 这两个 Section。这两个表中,都是由 Indirect Pointer 构成的指针数组,但是其中的元素决定了我们调用的方法应该以哪个代码段的方法来执行。我们遍历这个指针数组中每一个指针,在每一层遍历中取出其符号名称,与我们的 rebindings 链表中每一个元素进行比对,当名称匹配的时候,重写其指向地址。


image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容