C94c1df9c2098ffc25e080f0eecd4db6
核心知识篇:Fishhook 解读

1.引言:符号绑定

在前面的 Mach-O 的 4.4 小结中,提到了 __DATA.__la_symbol_ptr 和 __DATA.__nl_symbol_ptr 这两个指针表,分别为 lazy binding 指针表和 non lazy binding 指针表。在这两个指针表中,保存着与字符串标对应的函数指针。

但是,Mach-O 文件通过 dyld 加载的 Lazy Binding 表并没有在加载过程中直接确定地址列表,而是在第一次调用该函数的时候,通过 PLT(Procedure Linkage Table) 来进行一次 Lazy Binding。这里我们通过一个示例来看一下。

小示例

int main(int argc, char * argv[]) {
    NSLog(@"Hello Gof");
    NSLog(@"Hello Lee");
    return 0;
}

在两个 NSLog 处都打断点,运行程序。用 MachOView 查看生成的可执行文件,可以看到 NSLog 的函数偏移地址是 0xC000。

代码执行到第一个【NSLog】断点时,我们可以通过指令 “image list”,查看可执行文件和依赖库的 image:

// 指令
image list
/* 结果
[  0] D8BB5F17-6DD2-3AAE-8042-68875FC5503B 0x000000010023c000 /Users/GofLee/Library/Developer/Xcode/DerivedData/GofFishhook-eamgtrmlkfpmsigdlmhkuygtoplh/Build/Products/Debug-iphoneos/GofFishhook.app/GofFishhook 
[  1] E008B938-7593-3F57-B94A-747BC6C3BEB5 0x00000001004b4000 /Users/GofLee/Library/Developer/Xcode/iOS DeviceSupport/13.3.1 (17D50) arm64e/Symbols/usr/lib/dyld 
[  2] 7A7A96AF-79E4-3DB1-8904-42E61CAE8999 0x0000000192463000 /Users/GofLee/Library/Developer/Xcode/iOS DeviceSupport/13.3.1 (17D50) arm64e/Symbols/System/Library/Frameworks/Foundation.framework/Foundation
*/

可以看到可执行文件的【ASLR】是【0x23c000】。然后通过指令【memory read】或者 【x】,查看「NSLog」函数地址和对应地址处的汇编:

(lldb) x 0x000000010023c000+0xc000
0x100248000: 2c 25 24 00 01 00 00 00 8c 25 24 00 01 00 00 00  ,%$......%$.....
0x100248010: 98 25 24 00 01 00 00 00 a4 25 24 00 01 00 00 00  .%$......%$.....
(lldb) dis -s 0x10024252c
    0x10024252c: ldr    w16, 0x100242534
    0x100242530: b      0x100242514
    0x100242534: udf    #0x0
    0x100242538: ldr    w16, 0x100242540
    0x10024253c: b      0x100242514
    0x100242540: udf    #0xf5
    0x100242544: ldr    w16, 0x10024254c
    0x100242548: b      0x100242514

可以看到这时候,该地址处,并不是「NSLog」函数。使用指令【c】继续执行,到第二个「NSLog」函数调用处,我们再来查看一下:

(lldb) x 0x000000010023c000+0xc000
0x100248000: 7c 8f 58 92 01 00 00 00 8c 25 24 00 01 00 00 00  |.X......%$.....
0x100248010: 98 25 24 00 01 00 00 00 a4 25 24 00 01 00 00 00  .%$......%$.....
(lldb) dis -s 0x192588f7c
Foundation`NSLog:
    0x192588f7c <+0>:  pacibsp 
    0x192588f80 <+4>:  sub    sp, sp, #0x20             ; =0x20 
    0x192588f84 <+8>:  stp    x29, x30, [sp, #0x10]
    0x192588f88 <+12>: add    x29, sp, #0x10            ; =0x10 
    0x192588f8c <+16>: adrp   x8, 254391
    0x192588f90 <+20>: ldr    x8, [x8, #0xc00]
    0x192588f94 <+24>: ldr    x8, [x8]
    0x192588f98 <+28>: str    x8, [sp, #0x8]

从结果可以看到, 这时「NSLog」已经绑定正确的地址。由此可见「NSLog」是在调用时,才进行符号绑定的。

Dynamic Symbol Table

Dynamic Symbol Table 动态符号表是用来加载动态库的时候导出的符号表,该表在 dyld 时期使用,并且在对象被加载的时候映射到进程的地址空间。DST 和 Mach-O 中的 Indirect Addressing 有很大的关联,在 Indirect Addressing 中介绍了 Non-lazy symbol references、Lazy symbol reference 的特点和区别。其中 Lazy symbol reference 是指在方法在第一次调用的时候,会根据 dyld 过程时加载的映射地址进行处理,完成绑定操作。

2.Fishhook

2.1 Fishhook 是什么?

Fishhook 是 Facebook 提供的一个动态修改链接 Mach-O 符号表的开源工具。原理是通过修改懒加载和非懒加载两个表的指针达到 C 函数 HOOK 的目的,所以不能修改自己定义的函数,只能修改系统库函数。

2.2理解 Fishhook

自定义函数 && 系统函数

这里我们先对自定义函数 和 系统函数做一个了解:

  • 自定义函数在我们自己的 Mach-O 文件中。在编译的时候,是要确定它的位置的,如果只定义没有实现的话,会产生一个链接错误;
  • 系统函数在系统的库里面。只有在运行的时候,才能确定它的位置。

在编译时期,只能确定自定义函数的位置。系统函数在编译时期,是不能确定位置的。当需要调用一个外部函数(非 Mach-O 文件里的),会生成一个列表,在 __DATA 段中保存一个指针列表(符号表),指向外部函数(这时候指针全是 0),当启动 App 进入 内存时,DYLD 会给指针列表中的指针(符号)进行赋值,将 Mach-O 的 __DATA 段中的指针,指向外部函数,赋值的过程叫做【符号绑定】。

小示例

从上面的示例中,可以看到 Lazy Binding 这个过程,那么我们是不是可以重新绑定 Mach-O 的 Symbol 从而 hook 一些 C 中的库函数呢?下面我们通过一个例子来看一下。该示例功能比较简单: hook 了系统的 NSLog 函数,在打印前加了一个标识 Gof。

- (void)viewDidLoad {
    [super viewDidLoad];

    struct rebinding gofLog;
    gofLog.name = "NSLog";
    gofLog.replacement = GofNSLog;
    gofLog.replaced = (void *)&sysNSLog;

    struct rebinding rebs[1] = {gofLog};
    rebind_symbols(rebs, 1);  //断点2
}

/// 自定义的 Log 函数
/// @param format 参数
void GofNSLog(NSString *format, ...) {
    sysNSLog(@"Gof %@", format);
}

/// 定义一个变量,保存系统的函数指针
static void (*sysNSLog)(NSString *format, ...);

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"屏幕点击");
}

先来看一下 rebinding 结构体的定义:

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

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

/// 重绑定符号表
/// @param rebindings rebinding 结构体数组
/// @param 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 {
    /// rebinding 数组实例
    struct rebinding *rebindings;
    /// 元素数量
    size_t rebindings_nel;
    /// 链表索引
    struct rebindings_entry *next;
};

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

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

/// 用于 rebindings_entry 结构的维护
/// @param rebindings_head 对应的是 static 的 _rebindings_head
/// @param rebindings 传入的方法符号数组
/// @param 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 -> next 赋值,维护链表结构
    new_entry->next = *rebindings_head;
    // 移动 head 指针,指向表头
    *rebindings_head = new_entry;
    return 0;
}

之后这一部分的结构将会持续被维护,用图示来说明一下:

在执行完链表的初始化及结构维护的 prepend_rebindings 方法,继续执行。由于我们的 NSLog 是 dyld 加载的系统库方法,所以 _rebindings_head -> next 在第一次调用的时候为空,因为没有做过替换符号,所以会调用 _dyld_register_func_for_add_image 来注册 _rebind_symbols_for_image 方法,之后程序每次加载动态库的时候,都会去调用该方法。如果不是第一次替换符号,则遍历已经加载的动态库。

这一块的流程也是 fishhook 代码的精髓部分,逐一来分析这些地方。

_dyld_register_func_for_add_image 是什么?

在 Apple 官方的 dyld.h 头文件中,对于 _dyld_register_func_for_add_image 有较为详细的说明(dyld.h):

The following functions allow you to install callbacks which will be called by dyld whenever an image is loaded or unloaded. During a call to _dyld_register_func_for_add_image() the callback func is called for every existing image. Later, it is called as each new image is loaded and bound (but initializers not yet run). The callback registered with _dyld_register_func_for_remove_image() is called after any terminators in an image are run and before the image is un-memory-mapped.

_dyld_register_func_for_add_image 这个方法当镜像 Image 被 load 或是 unload 的时候都会由 dyld 主动调用。当该方法被触发时,会为每个镜像触发其回调方法。之后则将其镜像与其回电函数进行绑定(但是未进行初始化)。使用 _dyld_register_func_for_add_image 注册的回调将在镜像中的 terminators 启动后被调用。

为什么传入 _rebind_symbols_for_image 作为回调函数呢?

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);

extern 关键字来告知编译器当调用方法的时候,请在其他模块中寻找该方法定义。并且在这两个方法的声明中,所注册方法的参数列表一定是 (const struct mach_header* mh, intptr_t vmaddr_slide)。

/// _rebind_symbols_for_image 是 rebind_symbols_for_image 的一个入口方法。这个入口方法存在的意义是满足 _dyld_register_func_for_add_image 传入回调方法的格式
/// @param header Mach-O 头
/// @param 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 是个入口,添加这个入口方法其实是为了适应回调函数的参数格式,但是真正调用的 rebind_symbols_for_image 所需要的参数不满足其方法的描述。

intptr_t 是什么类型?

Understanding and using C pointers 有相关介绍:
```
/// /usr/include/stdint.h

/* Types for `void *' pointers. */

if __WORDSIZE == 64

ifndef __intptr_t_defined

typedef long int intptr_t;

define __intptr_t_defined

endif

typedef unsigned long int uintptr_t;

else

ifndef __intptr_t_defined

top Created with Sketch.