离久的小站

TrampolineHook 源码阅读

2020/06/07 Share

TrampolineHook 是一个中心重定向框架。可以通过一个函数替换/拦截所有你想要函数的框架。

由于 TrampolineHook 进行过更新,本文先从最初的源码开始阅读分析。

预备知识

汇编语言

之前有文章简单总结了汇编语言。

虚拟内存

关于虚拟内存,特地做了一篇简述

初版

提交记录为 :commit => ‘eb2cb31243a86cd50760e8bb8f8e2145e18e4467’

代码例子

TrampolineHook 中提供了一个 demo ,展示了如何使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/// 一个自定义函数,它会在拦截的目标方法前执行
static void MTMainThreadChecker(id obj, SEL selector) //
{
if (![NSThread isMainThread]) {
NSLog(@"[MTMainThreadChecker]::Found issue on %@ with selector %@", obj, NSStringFromSelector(selector));
}
}

/// 使用 TrampolineHook
static bool MTAddSwizzler(Class cls, SEL selector)
{
Method origMethod = class_getInstanceMethod(cls, selector);

IMP originIMP = method_getImplementation(origMethod);

static THInterceptor *interceptor = nil;
if (!interceptor) {
/// THInterceptor 通过一个拦截方法初始化
interceptor = [THInterceptor sharedInterceptorWithFunction:(IMP)MTMainThreadChecker];
}

/// 构造一个新的 IMP
THInterceptorResult *result = [interceptor interceptFunction:originIMP];
/// 替换掉目标方法的 IMP
method_setImplementation(origMethod, result.replacedAddress);

return true;
}

直接替换掉目标方法的IMP,那么它是如何做到拦截方法的 ?

源码解析

新的 IMP 是在 THDynamicPageAllocator 中构建的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (IMP)allocateDynamicPageForFunction:(IMP)functionAdress
{
if (!functionAdress) return NULL;

/// THDynamicPage 是一个结构体
/// fetchCandidiateDynamicPage 方法动态构造页,下面会有说
THDynamicPage *dynamicPage = [self fetchCandidiateDynamicPage];

int slot = dynamicPage->dataPage.nextAvailableIndex;
dynamicPage->dataPage.dynamicData[slot].originIMP = (IMP)functionAdress;
dynamicPage->dataPage.nextAvailableIndex++;

/// 这里返回了结构体中的一个索引
return (IMP)&dynamicPage->codePage.jumpInstructions[slot];
}

fetchCandidiateDynamicPage 会调用 THCreateDynamicePage() 返回一个结构体。
THCreateDynamicePage() 方法中申请内存和映射的代码,将汇编映射到虚拟内存中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static THDynamicPage *THCreateDynamicePage()
{
/// _th_dynamic_page 是汇编中的方法
vm_address_t fixedPage = (vm_address_t)&th_dynamic_page;

vm_address_t newDynamicPage = 0;
kern_return_t kernResult = KERN_SUCCESS;

/// 申请2页内存,低位一页对应dataPage,高位一页对应codePage
kernResult = vm_allocate(current_task(), &newDynamicPage, PAGE_SIZE * 2, VM_FLAGS_ANYWHERE);

/// 清空代码段的内存
vm_address_t newCodePageAddress = newDynamicPage + PAGE_SIZE;
kernResult = vm_deallocate(current_task(), newCodePageAddress, PAGE_SIZE);

/// remap虚拟内存映射 newCodePageAddress 的内容被重新映射到 th_dynamic_page 代码段所在的内存页里面。
/// 然后 newCodePageAddress 具有了执行权限,但没了读写权限
vm_prot_t currentProtection, maxProtection;
kernResult = vm_remap(current_task(), &newCodePageAddress, PAGE_SIZE, 0, 0, current_task(), fixedPage, FALSE, &currentProtection, &maxProtection, VM_INHERIT_SHARE);

return (void *)newDynamicPage;
}

我们来看看汇编和结构体的结构

汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
.text
.align 14
.globl _th_dynamic_page ;用global让一个标签对链接器可见,可以供其他链接对象模块使用

interceptor: 定义拦截器的地址存放处
.quad 0

.align 14
_th_dynamic_page: ;定义动态代码的开始,在代码段可以搜索到 th_dynamic_page

_th_entry: ;声明一个方法,执行以下指令。通过偏移地址执行原函数方法。

nop ;用来32位对齐,因为下面有用的代码有27条,加上这5条空指令,刚好32条,每条指令在arm64上是4字节,一共是128字节
nop
nop
nop
nop

sub x12, lr, #0x8 ;这里减去0x8之后,得到是bl上面一条指令的地址
sub x12, x12, #0x4000 ;减去一页的地址,可以拿到存放原函数的地址,存到x12
mov lr, x13 ;将之前的lr存回去

ldr x10, [x12] ;取出原函数实际地址 到x10

stp q0, q1, [sp, #-32]! ;保存原函数的调用寄存器
stp q2, q3, [sp, #-32]!
stp q4, q5, [sp, #-32]!
stp q6, q7, [sp, #-32]!

stp lr, x10, [sp, #-16]!
stp x0, x1, [sp, #-16]!
stp x2, x3, [sp, #-16]!
stp x4, x5, [sp, #-16]!
stp x6, x7, [sp, #-16]!
str x8, [sp, #-16]!

ldr x8, interceptor ;加载拦截方法
blr x8 ;调用拦截方法,且会返回

ldr x8, [sp], #16 ;加载原函数的调用寄存器
ldp x6, x7, [sp], #16
ldp x4, x5, [sp], #16
ldp x2, x3, [sp], #16
ldp x0, x1, [sp], #16
ldp lr, x10, [sp], #16

ldp q6, q7, [sp], #32
ldp q4, q5, [sp], #32
ldp q2, q3, [sp], #32
ldp q0, q1, [sp], #32

br x10 ;调用原函数,并且不用返回

.rept 2032 ;构造一堆命令,用来将原来的Method指向这里
mov x13, lr
bl _th_entry;
.endr

声明的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

// 0x4000是一页的大小,共16K

//其实一组就是2条指令,对应上面重复的那一坨代码的一组 sizeof(THDynamicPageEntryGroup) 是8字节
typedef int32_t THDynamicPageEntryGroup[2];

static const int32_t THDynamicPageInstructionCount = 32; //ARM64指令一般是4字节
static const int32_t THPageSize = 0x4000;//算出来的值是2032
static const size_t THNumberOfDataPerPage = (0x4000 - THDynamicPageInstructionCount * sizeof(int32_t)) / sizeof(THDynamicPageEntryGroup);
typedef struct {
union {
struct {
IMP redirectFunction; //8字节,存放的是拦截器函数
int32_t nextAvailableIndex; //存放的是下一个空余的引索
}; // 16字节大小,因为要考虑8字节对齐

int32_t placeholder[THDynamicPageInstructionCount]; // 128字节,占位,没有实际意义
}; //128字节

THDynamicData dynamicData[THNumberOfDataPerPage]; //剩余的2032字节,用来存放原始的IMP
} THDataPage; //第一页内存

typedef struct {
int32_t fixedInstructions[THDynamicPageInstructionCount]; //存放前面128字节的代码

// 存放后面那堆的重复代码 mov x13, lr; bl _th_entry; 每组是8字节。 // 替换后的IMP执行这里的某一组开头
THDynamicPageEntryGroup jumpInstructions[THNumberOfDataPerPage];
} THCodePage; //第2页内存

typedef struct {
THDataPage dataPage;
THCodePage codePage;
} THDynamicPage; //精心设计的2页虚拟内存

我们用图对比这两个结构

可以看出,allocateDynamicPageForFunction 返回的索引,其实是汇编中的两条指令

1
2
mov x13, lr
bl _th_entry;

通过 _th_entry 执行偏移地址执行原函数方法。

具体流程如下图

再版

针对可变参数函数方法,解决了栈污染问题,将寄存器的值存放在堆上。

申请堆空间保存寄存器数值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

__attribute__((__naked__))
void THPageVariadicContextPre(void)
{

// 先保存,避免调用 malloc 破坏寄存器
saveRegs();

// 分配堆上内存 extra 16 byte + sizeof(THPageVariadicContext)
__asm volatile ("mov x0, #0xF0");
__asm volatile ("bl _malloc");

// 返回的分配内存地址保存起来 callee-saved
__asm volatile ("str x19, [x0]");
__asm volatile ("mov x19, x0");

// 恢复堆栈,避免影响变参所处在的堆栈
restoreRegs();

// 用堆上空间保存数据
__asm volatile ("stp x0, x1, [x19, #(16 + 0 * 16)]");
__asm volatile ("stp x2, x3, [x19, #(16 + 1 * 16)]");
__asm volatile ("stp x4, x5, [x19, #(16 + 2 * 16)]");
__asm volatile ("stp x6, x7, [x19, #(16 + 3 * 16)]");
__asm volatile ("stp x8, x13, [x19, #(16 + 4 * 16)]");

__asm volatile ("stp q0, q1, [x19, #(16 + 5 * 16 + 0 * 32)]");
__asm volatile ("stp q2, q3, [x19, #(16 + 5 * 16 + 1 * 32)]");
__asm volatile ("stp q4, q5, [x19, #(16 + 5 * 16 + 2 * 32)]");
__asm volatile ("stp q6, q7, [x19, #(16 + 5 * 16 + 3 * 32)]");

__asm volatile ("stp lr, x10, [x19, #(16 + 5 * 16 + 4 * 32)]");

__asm volatile ("ret");

}

释放寄存器数值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

__attribute__((__naked__))
void THPageVariadicContextPost(void)
{
// x19 肯定是正确的地址,使用x19恢复对应的数据
__asm volatile ("ldp lr, x10, [x19, #(16 + 5 * 16 + 4 * 32)]");
__asm volatile ("ldp q6, q7, [x19, #(16 + 5 * 16 + 3 * 32)]");
__asm volatile ("ldp q4, q5, [x19, #(16 + 5 * 16 + 2 * 32)]");
__asm volatile ("ldp q2, q3, [x19, #(16 + 5 * 16 + 1 * 32)]");
__asm volatile ("ldp q0, q1, [x19, #(16 + 5 * 16 + 0 * 32)]");

__asm volatile ("ldp x8, x13, [x19, #(16 + 4 * 16)]");
__asm volatile ("ldp x6, x7, [x19, #(16 + 3 * 16)]");
__asm volatile ("ldp x4, x5, [x19, #(16 + 2 * 16)]");
__asm volatile ("ldp x2, x3, [x19, #(16 + 1 * 16)]");
__asm volatile ("ldp x0, x1, [x19, #(16 + 0 * 16)]");

// 保存一下,避免 free 的影响。
saveRegs();

// 恢复原先的 x19, 调用free
__asm volatile ("mov x0, x19");
__asm volatile ("ldr x19, [x19]");
__asm volatile ("bl _free");

// 恢复堆栈
restoreRegs();

__asm volatile ("mov lr, x13");
__asm volatile ("br x10");
}

这里用了

1
__attribute__((__naked__))
,这个作用是为了让我们的函数不会额外的生成函数 prologue/epilogue 中的压栈消栈操作。

CATALOG
  1. 1. 预备知识
    1. 1.1. 汇编语言
    2. 1.2. 虚拟内存
  2. 2. 初版
    1. 2.1. 代码例子
    2. 2.2. 源码解析
  3. 3. 再版