巧用符号表 - 探求 fishhook 原理(一)

这是 探求 fishhook 原理 系列的第一篇。主要讲述了 Facebook 开源库 fishhook 的源码实现细节。需要具备 Mach-O 相关知识。可以先阅读 Mach-O 文件格式探索 一文。

关于符号表的基本知识

Lazy Binding 过程

在之前的Mach-O 文件格式探索一文中提及到了 __DATA.__la_symbol_ptr__DATA.__nl_symbol_ptr 这两个指针表,分别为lazy binding指针表non lazy binding指针表。并且这两个指针表,保存着与字符串标对应的函数指针。

而 Mach-O 文件文件中通过 dyld 加载的 Lazy Binding 表并没有在加载过程中直接确定地址列表,而是在第一次调用该函数的时候,通过 PLT(Procedure Linkage Table) 来进行一次 Lazy Binding。来使用 printf 方法验证一下:

#include <stdio.h>
int main(int argc, const char * argv[]) {
    printf("%s\n", "hello world");
    printf("%s\n", "hello desgard");
    return 0;
}

拖到 Hopper 中查看汇编:

 _main:
push       rbp
mov        rbp, rsp
sub        rsp, 0x10
lea        rdi, qword [0x100000f8e] ; "hello world\\n", argument "format" for method imp___stubs__printf
mov        dword [rbp+var_4], 0x0
mov        al, 0x0
call       imp___stubs__printf
lea        rdi, qword [0x100000f9b] ; "hello desgard\\n", argument "format" for method imp___stubs__printf
mov        dword [rbp+var_8], eax
mov        al, 0x0
call       imp___stubs__printf
xor        ecx, ecx
mov        dword [rbp+var_C], eax
mov        eax, ecx
add        rsp, 0x10
pop        rbp
ret

发现在使用 printf 的时候会触发 call imp__stubs__printf 这条指令。点击进入 imp__stubs__printf 的存储位置查看:

 imp___stubs__printf:
jmp        qword [_printf_ptr] ; _printf, CODE XREF=_main+24, _main+41
; ================================
; 继续跟踪....

0x100001010         dq         _printf  ; DATA XREF=imp___stubs__printf

在两个 printf 方法前加上断点,使用 lldb 对其进行调试:

为什么这里要观测 0x10001010 这个地址?因为 imp___stubs__printf 这个指针指向了它。这说明在 __la_symbol_ptr 表中对其进行了记录。使用 MachOView 对其进行验证:

发现在 __DATA.__la_symbol_ptr 中的记录值与 lldbx 0x10001010 命令输出的记录值均为 01 00 00 0f 84。这个值对应的地址就是 imp___stubs__printf 这个桩位置。

0x100000f84         push       0x0
0x100000f89         jmp        0x100000f74
; Section __stub_helper
0x100000f74         lea        r11, qword [dyld_stub_binder_100001000+8]   ; CODE XREF=0x100000f89
0x100000f7b         push       r11
0x100000f7d         jmp        qword [dyld_stub_binder_100001000]          ; dyld_stub_binder
0x100000f83         db         0x90
0x100000f84         push       0x0
0x100000f89         jmp        0x100000f74

这个位置是不是似曾相识?是的,在Mach-O 文件格式探索一文中的验证试验,已经试验过了 Stub 机制。说道这里,我们在 MachOView 中的 __TEXT.__stubs 这个 Section 对其进行验证:

与预想中的结果是相同的。这个在 __TEXT.__stub_helper 中解析出的汇编代码和 Hopper 中完全一致。在这里通过 _stub_helper 来调用 dyld_stu_binder 方法计算 printf 函数的真实地址。其具体信息可以看出,jmpq 0x100000f74 就是在 pushq 参数 $0x0 (link 过程中的标记值)后跳转到这个 section 的头部,并调用 binder 方法。

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

在第二次输出 0x10001010 的值的时候,发现与第一次的值不相同了。变成了 7f ff 76 cc 98 c4。这个地址其实就是 printf 的真实地址。通过 x 命令及方法名的方式进行验证:

也就是说 __DATA.__la_symbol_ptr 中指向 printf 地址的值已经发生了改变,并真正的指向了 printf 指令。

Dynamic Symbol Table

Dynamic Symbol Table 动态符号表是用来加载动态库的时候导出的符号表,该表在 dyld 时期使用,并且在对象被加载的时候映射到进程的地址空间。所以我们可以说 DST 是符号表的子集。对于 ELF 来讲,DST 是有其定义的;但是对于 Mach-O 来说,我们可以理解为 DST 就是 Symbol Stubs。DST 和 Mach-O 中的 Indirect Addressing 有很大的关联,这里给出 Apple 对于 Indirect Addressing 的[官方文档] (https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/MachOTopics/1-Articles/indirect_addressing.html)。

这个文档的主要内容是介绍了 Non-lazy symbol referencesLazy symbol reference 的特点和区别。其中 Lazy symbol reference 是指在方法在第一次调用的时候,会根据 dyld 过程时加载的映射地址进行处理,完成绑定操作。

【通过 MachOView 我们可以查看 Dynamic Symbol Table 中的所有 Indirect Symbol

理解 fishhook

明确思路

根据上面的实战内容,我们理解了 Lazy Binding 这个过程。受此启发,我们是不是可以重新绑定 Mach-O 的 Symbol 从而 hook 一些 C 中的库函数呢?这个思路在 fishhook 中已经实现。

准备

为了简化分析过程,我们引入了一个例子,之后的分析都会围绕着这个例子来进行。实例代码很简单,就是修改库函数 strlen,让其始终返回 666

#include <stdio.h>
#include "fishhook.h"

static int (*original_strlen)(const char *_s);

int new_strlen(const char *_s) {
    return 666;
}

int main(int argc, const char * argv[]) {
    struct rebinding strlen_rebinding = { "strlen", new_strlen,
        (void *)&original_strlen };
    rebind_symbols((struct rebinding[1]){ strlen_rebinding }, 1);
    char *str = "hellolazy";

    printf("%d\n", strlen(str));
    return 0;
}

main 方法中,我们构造了一个 rebinding 结构体实例,使用原方法名新方法的指针以及一个二阶指针(后面我们可以了解到这里存储了 __bss 段中原方法的地址)。

struct rebinding {
    const char *name;        // 原方法名
    void *replacement;        // 新方法的代码段首地址
    void **replaced;        // 旧方法的指针
};

在跟踪代码之前,我先使用 nm 命令对生成的执行文件查看 File 中的符号信息( -n 参数是根据已知地址进行排序):

$ nm -n TestObjcProj
                 U ___memcpy_chk
                 U __dyld_get_image_header
                 U __dyld_get_image_vmaddr_slide
                 U __dyld_image_count
                 U __dyld_register_func_for_add_image
                 U _dladdr
                 U _free
                 U _malloc
                 U _printf
                 U _strcmp
                 U _strlen
                 U _strnlen
                 U dyld_stub_binder
0000000100000000 T __mh_execute_header
0000000100000690 t _rebind_symbols_image
00000001000006f0 t _prepend_rebindings
00000001000007d0 t _rebind_symbols_for_image
0000000100000b00 t _rebind_symbols
0000000100000bc0 t __rebind_symbols_for_image
0000000100000bf0 t _perform_rebinding_with_section
0000000100000e10 T _new_strlen
0000000100000e20 T _main
0000000100001090 b __rebindings_head
0000000100001098 b _original_strlen

这里每一行代表一个符号,三列值分别代表地址符号说明符号名。其中符号说明为以下规则:

  • 小写代表作用域为 Local,大写代表符号是 Global(external) 的。
  • A - 符号值是绝对的,在链接过程中不允许对其改变,这个符号常常出现在中断向量表中。
  • B - 符号值出现在内存 BSS 段。例如在某一个文件中定义全局的 static 方法 static void test,则符号 test 的类型为 b,切存储于 BSS 中。其值为该符号在 BSS 中的偏移。一般来说,BSS 分配在 RAM 中。
  • C - 称为Common Symbol 一般符号,是为初始化的数据段。该符号不包含于普通的 Section 中。只有在链接过程才会进行分配。符号的值为所需要的字节数。
  • D - 称之为 Data Symbol。位于初始化数据段中。一般分配到 Data Section 中。例如全局的 int table[5] = {233, 123, 321, 132, 231};,会分配到初始化数据段中。
  • T - 该符号位于代码区 TS 中。
  • U - 说明当前文件中该符号是未定义的,该符号的定义在别的文件中

我们找到这几个与 strlen 先关的符号,先记录下来:

                 U _strlen    # 系统库方法,dyld 动态加载,所以未定义
0000000100000e10 T _new_strlen  # 自定义方法,位于 Text Section
0000000100001098 b _original_strlen  # 

另外我们还需要了解的一些基地址信息。例如:Lazy Symbol Pointer Table(__DATA.__la_symbol_ptr)、Indirect Symbol TableSymbol Table。这些均能从 MachOView 中获得到。

获取到 __DATA.__la_symbol_ptr 基地址为 0x100001010

这里获取到在 Indirect Symbols 中,_strlen 符号的地址为 0x1000025f0

这里可以获取到在 Symbol Table 中对应的符号位置 0x100002560,而且其中还能检索到在 String Table 中对应的符号 _strlen 的符号名称。

调试代码

在进行 hook 的代码逻辑中,在声明一个 strlen_rebinding 实例之后,需要手动调用方法 rebind_symbols ,其中需要传入一个 rebinding 数组的头地址,以及其 size。下面来看一下 rebind_symbols 方法的实现:

#define __SIZE_TYPE__ long unsigned int
typedef __SIZE_TYPE__ size_t;
/**
 * rebind_symbols
 * struct rebinding rebindings[] - rebinding 结构体数组
 * size_t rebindings_nel - 数组长度
 */
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 会维护这个结构:

```c
/**

  • 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 成员中
top Created with Sketch.