Hook汇编函数(objc_msgSend)

一、概览

Hook汇编函数,首先可以了解一下汇编的本质和编译的本质。

汇编本质:操作寄存器和内存。如将数据暂存寄存器,将寄存器值存入内存,读内存值到寄存器,管理内存。

编译本质:编译器按照一定规则将高级语言翻译成汇编语言。如记录上下文,运算指令转换,查表等。

灵魂三问:

1、为什么会有汇编函数呢?
2、为什么汇编效率高不用汇编编码呢?
3、为什么汇编编码执行效率会比高级语言编码效率高呢?

灵魂三答:

1、因为编函数效率高;
2、因为汇编难以阅读和理解,不便于维护,敲多了四肢会变的僵硬;
3、因为编译器并不是很聪明,它只会按照流程手册办事,总喜欢把买来的萝卜放框里又拿出来(但它会变的聪明起来)。

不要问我为什么知道,我猜的……

二、高级语言到汇编

应用程序的诞生:高级语言-预编译-编译-汇编-链接-可执行文件

因为主要看下如何hook汇编函数,这里只看编译过程,也就是高级语言到汇编。先了解一下高级代码和汇编代码的对应关系(开发工具Xcode)。

1、用C实现求和功能

1
2
3
4
5
6
7
8
9
10
int sumFunc(int a, int b) {
return a + b;
}

- (void)viewDidLoad {
[super viewDidLoad];

int res = sumFunc(1, 2);
printf("res:%d\n", res);
}

在函数调用出下断点,并切换为汇编查看,观察编译后的汇编代码(Debug-Debug Workflow-disassembly):

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
ASMDemo`-[ViewController viewDidLoad]:
0x104b41f20 <+0>: sub sp, sp, #0x40 ; =0x40
0x104b41f24 <+4>: stp x29, x30, [sp, #0x30]
0x104b41f28 <+8>: add x29, sp, #0x30 ; =0x30
0x104b41f2c <+12>: stur x0, [x29, #-0x8]
0x104b41f30 <+16>: stur x1, [x29, #-0x10]
0x104b41f34 <+20>: ldur x8, [x29, #-0x8]
0x104b41f38 <+24>: add x0, sp, #0x10 ; =0x10
0x104b41f3c <+28>: str x8, [sp, #0x10]
0x104b41f40 <+32>: adrp x8, 8
0x104b41f44 <+36>: ldr x8, [x8, #0x2a0]
0x104b41f48 <+40>: str x8, [sp, #0x18]
0x104b41f4c <+44>: adrp x8, 8
0x104b41f50 <+48>: ldr x1, [x8, #0x278]
0x104b41f54 <+52>: bl 0x104b42500 ; symbol stub for: objc_msgSendSuper2
0x104b41f58 <+56>: mov w0, #0x1
0x104b41f5c <+60>: mov w1, #0x2
-> 0x104b41f60 <+64>: bl 0x104b41f00 ; sumFunc at ViewController.m:14
0x104b41f64 <+68>: str w0, [sp, #0xc]
0x104b41f68 <+72>: ldr w9, [sp, #0xc]
0x104b41f6c <+76>: mov x8, x9
0x104b41f70 <+80>: adrp x0, 2
0x104b41f74 <+84>: add x0, x0, #0x3f0 ; =0x3f0
0x104b41f78 <+88>: mov x9, sp
0x104b41f7c <+92>: str x8, [x9]
0x104b41f80 <+96>: bl 0x104b4253c ; symbol stub for: printf
0x104b41f84 <+100>: ldp x29, x30, [sp, #0x30]
0x104b41f88 <+104>: add sp, sp, #0x40 ; =0x40
0x104b41f8c <+108>: ret

让我解释下:

  • x30:也叫链接寄存器(Link Register),简称lr寄存器,存放返回的地址
  • w0/w1:通用寄存器x0/x1的低32位,用来存放函数的参数,也用作存放返回值
  • bl:是跳转指令,有子函数调用,就会保存x30寄存器值到内存,以免x30值被子函数修改
  • ret:返回指令,主要任务读取x30寄存器存放的地址,并跳转到该地址处

知道上面的寄存器及指令,就可以理解一个函数是如何调用和返回的了(自己理一下)。

再看看求和函数的汇编指令:

1
2
3
4
5
6
7
8
9
ASMDemo`sumFunc:
-> 0x104b41f00 <+0>: sub sp, sp, #0x10 ; =0x10
0x104b41f04 <+4>: str w0, [sp, #0xc]
0x104b41f08 <+8>: str w1, [sp, #0x8]
0x104b41f0c <+12>: ldr w8, [sp, #0xc]
0x104b41f10 <+16>: ldr w9, [sp, #0x8]
0x104b41f14 <+20>: add w0, w8, w9
0x104b41f18 <+24>: add sp, sp, #0x10 ; =0x10
0x104b41f1c <+28>: ret
  • 第一步:开辟栈空间,栈区开辟方向由高低向低地址,因此使用sub减运算指令重新指定栈顶位置
  • 第二步:存储参数,汇编中参数是通过寄存器传递,存储目的是保留上下文,因为寄存器会被重复使用
  • 第三步:读取内存值到新寄存器
  • 第四步:求和运算,并将结果存到w0寄存器中,返回值一般使用w0~w7寄存器(w0x0的低32位
  • 第五步:执行ret指令,读取x30存放的地址,跳转到该地址,完成子函数返回

这里截图看下x30寄存器的地址:

这样对比可以看出blret在子函数调用和返回的作用。以上也就是编译规则的一部分,任何高级代码都会按照改则生成指定的汇编指令,CPU会逐条执行每一句汇编指令,每一条汇编指令耗时是固定的,指令越多耗时越多。

为什么说高级语言效率低呢,下面用汇编实现一个相同的功能,对比一下。

2、用汇编实现求和功能

1
2
3
4
5
.text
.global _sumFuncAsm
_sumFuncAsm:
add w0, w0, w1
ret

运行结果如下:

对比高级语言生成的汇编指令,实现同样的功能,直接使用汇编编码,效率会高n倍。这就是为什么有些使用频次较高的函数,需要使用汇编实现。汇编能够提升效率、降低功耗。

知道高级语言和汇编的关系后,那汇编函数可以hook了吗?

首先声明hook是针对系统函数的,一般会使用fishhook来实现,如果用来hook系统汇编函数,在调用系统原始汇编函数时,则存在传参问题,因为汇编函数是通过寄存器传参的,而高级语言中无法直接设置寄存器,这时候就需要借助内嵌汇编来解决传参问题。

内嵌汇编实现前,先熟悉下裸函数

三、裸函数

裸函数,就是在编译时不会生成保存上下文退出子函数的指令,内部只能编写汇编指令。借用裸函数可以构建一个纯净的汇编环境。

编写函数和裸函数,观察生成的汇编指令的区别

普通函数代码:

1
2
3
void normalFunc(int a, int b) {

}

裸函数代码:

1
2
3
4
__attribute__((__naked__))
void nakedFunc(int a, int b) {

}

普通函数编译后的汇编:

1
2
3
4
5
6
ASMDemo`normalFunc:
-> 0x102ae9f50 <+0>: sub sp, sp, #0x10 ; =0x10
0x102ae9f54 <+4>: str w0, [sp, #0xc]
0x102ae9f58 <+8>: str w1, [sp, #0x8]
0x102ae9f5c <+12>: add sp, sp, #0x10 ; =0x10
0x102ae9f60 <+16>: ret

裸函数编译后的汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ASMDemo`nakedFunc:
-> 0x1043cdf64 <+0>: nop
````

**普通函数也能内嵌汇编,为什么不用普通函数来hook呢?**

>普通函数也可以使用内嵌汇编协助`hook`,但普通函数在编译时会生成无用的栈空间申请指令,会有更多消耗,因此建议使用裸函数。


# 四、线程私有数据

这里为什么介绍线程私有数据呢,因为`hook`汇编函数存在多线程安全问题,`hook`时会用到该技术点。

**线程私有数据(`TSD:thread specific data`)**:每个线程内的私有数据是独立的,不可被其他线程访问。而`TSD`则可以通过一个`key`,获取不同线程的私有数据。

**1、创建一个线程特定的key,该key对进程中所有线程均可见**
```c
int pthread_key_create(pthread_key_t *, void (* _Nullable)(void *));
  • 参数一:关联线程私有数据的key
  • 参数二:一个析构函数,当线程结束时会调用该函数

2、取线程中的私有数据(无需指定线程id)

1
void* _Nullable pthread_getspecific(pthread_key_t);

3、设置线程中的私有数据

1
int pthread_setspecific(pthread_key_t , const void * _Nullable);
  • 参数一:指定key
  • 参数二:指定数据,如结构体、数组

4、删除特定的key,并清除与key关联的线程特定的数据,无需用户释放

1
int pthread_key_delete(pthread_key_t);

使用示例:

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
#include <pthread.h>

/// 与线程关联的key
static pthread_key_t pthread_key_test;

/// 私有数据结构体
struct test_data {
int count;
};

/// 线程结束释放私有数据内存
void release_pthread_key_data(void *ptr) {
if (ptr != NULL) {
free(ptr);
}
}

/// 线程私有数据测试
void phtread_test(void) {
//创建线程私有数据
struct test_data *data = (struct test_data *)malloc(sizeof(struct test_data));
data->count = 1;
//创建关联线程的key
pthread_key_create(&pthread_key_test, release_pthread_key_data);
//在当前线程创建私有数据,和key关联
pthread_setspecific(pthread_key_test, &data);

//重新创建一个子线程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//创建子线程的私有数据,和key关联
struct test_data *newData = (struct test_data *)malloc(sizeof(struct test_data));
newData->count = 100;
pthread_setspecific(pthread_key_test, &newData);
NSLog(@"① key=%ld data_count=%d thread=%@", pthread_key_test, newData->count, NSThread.currentThread);
});
sleep(0.5);
NSLog(@"② key=%ld data_count=%d thread=%@", pthread_key_test, data->count, NSThread.currentThread);
}

输出:

1
2
① key=267 data_count=100 thread=<NSThread: 0x280c5e900>{number = 2, name = (null)}
② key=267 data_count=1 thread=<NSThread: 0x280c5ae80>{number = 1, name = main}

因此可以使用线程私有数据绑定,来解决多线程中数据安全问题。

五、Hook汇编函数objc_msgSend

Hook思路

1、保存上下文,即将寄存器值保存到内存,避免寄存器值被子函数修改;
2、汇编调用c函数,将参数传递到c语言环境,便于对记录hook数据(封装一个汇编函数),此处还需要保存lr链接寄存器的值,即返回的地址,解决多线程对lr值的修改(使用TSD技术点解决);
3、恢复上下文,由于调用了c函数,寄存器值会被修改,返回后不能直接使用,需要将内存值重新赋值给寄存器;
4、调用原始汇编函数;
5、保存上下文,保存寄存器值到内存,准备调用恢复lr值;
6、恢复lr链接寄存器的值,保证执行完成后正确返回;
7、恢复上下文,将内存的值重新赋值给寄存器;
8、完成hook任务后返回到上一级函数中。

具体实现如下:

创建一个c函数文件ASMHook.c

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#include "ASMHook.h"
#include <objc/message.h>
#include <pthread/pthread.h>
#include <dispatch/dispatch.h>
#include <stdarg.h>
#include "fishhook.h"

#pragma mark - 定义线程私有数据key
static pthread_key_t pthread_key;

#pragma mark - 定义线程私有数据结构体
struct private_data {
uintptr_t lr_list[10000];
int lr_index;
} private_data;

#pragma mark - 是否和线程关联的数据
static void release_pthread_key_data(void *ptr) {
struct private_data *data = (struct private_data *)ptr;
if (data) free(data);
}

#pragma mark - 1、汇编调用c函数
#define asm_call_c(funcP)\
__asm volatile("stp x8, x9, [sp, #-16]! \n");\
__asm volatile("mov x12, %x0 \n"\
:\
: "r"(funcP));\
__asm volatile("ldp x8, x9, [sp], #16 \n");\
__asm volatile("blr x12 \n");\

#pragma mark - 2、hook前,保存寄存器值
#define save_context()\
__asm volatile( \
"stp x8, x9, [sp, #-16]!\n" \
"stp x6, x7, [sp, #-16]!\n" \
"stp x4, x5, [sp, #-16]!\n" \
"stp x2, x3, [sp, #-16]!\n" \
"stp x0, x1, [sp, #-16]!\n" \
"stp q6, q7, [sp, #-32]!\n" \
"stp q4, q5, [sp, #-32]!\n" \
"stp q2, q3, [sp, #-32]!\n" \
"stp q0, q1, [sp, #-32]!\n" );

#pragma mark - 3、hook后,恢复寄存器值
#define resume_context()\
__asm volatile( \
"ldp q0, q1, [sp], #32\n" \
"ldp q2, q3, [sp], #32\n" \
"ldp q4, q5, [sp], #32\n" \
"ldp q6, q7, [sp], #32\n" \
"ldp x0, x1, [sp], #16\n" \
"ldp x2, x3, [sp], #16\n" \
"ldp x4, x5, [sp], #16\n" \
"ldp x6, x7, [sp], #16\n" \
"ldp x8, x9, [sp], #16\n" );

#pragma mark - 用来对外传递参数
void (*my_func)(const char *clsName, const char *selector);

#pragma mark - 保存子程序返回地址(TSD解决线程安全问题)
static void save_lr(id self, SEL _cmd, id param1, id param2, uintptr_t lr) {
const char *clsName = class_getName(object_getClass(self));
const char * selector = sel_getName(_cmd);
if (strcmp( selector, "isEqualToString:" ) == 0) {
printf("===>class:%s methodname:%s param1:%p\n", clsName, selector, param1);
} else {
printf("===>class:%s methodname:%s\n", clsName, selector);
}
if (strcmp( selector, "setName:" ) == 0) {
my_func(clsName, selector);
printf("===>class:%s methodname:%s param1:%p\n", clsName, selector, param1);
} else {
my_func(clsName, selector);
}
struct private_data *data = (struct private_data *)pthread_getspecific(pthread_key);
if (data == NULL) {
data = (struct private_data *)malloc(sizeof(struct private_data));
}
data->lr_list[data->lr_index++] = lr;
pthread_setspecific(pthread_key, data);
}

#pragma mark - 恢复LR寄存器值(TSD解决线程安全问题)
static uintptr_t resume_lr(SEL _cmd, uintptr_t lr) {
struct private_data *data = (struct private_data *)pthread_getspecific(pthread_key);
if (data == NULL) return 0;
int index = data->lr_index;
data->lr_index = index > 0 ? index-1 : 0;
return data->lr_list[data->lr_index];
}

#pragma mark - hook汇编函数
/// 原msgSend函数指针
__unused static id (*orig_objc_msgSend)(id, SEL, ...);
/// hook函数 声明裸函数,不生成入口代码和返回代码
__attribute__((__naked__))
static void hook_objc_msgSend() {
//1、记录上下文,上文是汇编函数
save_context();
//2、设置参数,并记录lr寄存器值,保存子函数返回的地址
__asm volatile("mov x4, lr \n");
asm_call_c(&save_lr);
//3、恢复上下文
resume_context();

//4、调用原始汇编函数
asm_call_c(orig_objc_msgSend);

//5、记录上下文
save_context();
//6、恢复lr寄存器值,找到返回的地址,并重新设置lr寄存器(函数返回值存放在x0寄存器)
asm_call_c(&resume_lr);
__asm volatile("mov lr, x0 \n");
//7、恢复上下文
resume_context();
//8、返回到上一级函数继续执行
__asm volatile("ret \n");
}

#pragma mark - 开始hook
void hookStart(void *func) {
my_func = func;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//创建线程私有数据,相同的key,在不同线程关联不同的数据,第二个参数为线程结束时回调的函数,用来释放数据
pthread_key_create(&pthread_key, &release_pthread_key_data);
//使用fishhook替换函数指针
struct rebinding reb = {"objc_msgSend", (void *)hook_objc_msgSend, (void **)&orig_objc_msgSend};
struct rebinding rebs[] = {reb};
rebind_symbols(rebs, sizeof(rebs)/sizeof(reb));
});
}

调用:

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
#import "ViewController.h"
#import "ASMHook.h"

@interface ViewController ()

@property (nonatomic, strong) NSString *myName;

@end

@implementation ViewController

#pragma mark - 获取hook到的参数
static void view_func(const char *clsName, const char *selector) {
printf("===hibo== clsName:%s sel:%s\n", clsName, selector);
}

- (void)viewDidLoad {
[super viewDidLoad];
hookStart(&view_func);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self setName:[ViewController new]];
}

@end

六、应用

  • 打印方法调用
  • 监控方法执行耗时

demo:https://github.com/yahibo/iOSReverse/tree/master/Hook汇编函数