使用Linux模块hook系统调用

前言

这原本是HDU操作系统课程设计的第二个作业,内核模块编程,但是寻思着仅仅重复当初编写系统调用的功能代码也太无趣了,况且还不能被重复调用。正好之前在系统调用的时候遇见过使用模块编程hook系统调用的教程,我为什么不正好来实践一下呢?说干就干!

选题

其实选题并不重要,但是做事情总要有目标对吧?况且最终提交方式也是作业。我选择了一个比较好在hook中有意义的功能:

选题

我们hook的目标函数是fork(),当使用syscall(__NR_FORK)时,我们将会输出当前进程与fork子进程的上述相关信息。

思路

  1. 获取syscall_table
  2. 保存原始函数:
    1
    2
    asmlinkage long (*origin)(void) = NULL;
    origin = (long (*)(void))(sys_call_table[__NR_fork]);
  3. 替换为我们的hook函数:
    1
    sys_call_table[__NR_fork] = (unsigned long)&hacked_func;
  4. 在hook函数中加入我们自己的功能。

过程

获取系统调用表

很遗憾,在Linux2.6版本后,syscall_table不再被EXPORT,我们只能采取曲线救国的方式。方法很多,由于不是我们实验的重点,我们采取简单的获取+硬编码的方式。

1
$ sudo grep sys_call_table  /boot/System.map-`uname -r`

在此,我们选择sys_call_table即可,后两者是为了兼容性准备的。

1
unsigned long *sys_call_table = (unsigned long *) 0xffffffff82000300;

关闭写保护

在改写系统调用表前,我们需要关闭寄存器cr0的写保护位(WP)也就是第17位:

cr0 register

Q:在这里,可能会有疑问为什么不需要改变内存的读写权限呢?(系统调用表的内存一般是只读的)
A:

页面错误

正当我们开开心心完成我们的基础设施后(隐藏了一些次要代码):

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
unsigned long *sys_call_table;
asmlinkage long (*origin)(void) = NULL;
asmlinkage long hacked_func(void);
int origin_cr0;

static int __init init_mymodule(void) {
printk("start init\n");
sys_call_table = (unsigned long *) 0xffffffff82000300;
printk("the syscall table's address is %lx\n", (unsigned long)sys_call_table);

origin = (long (*)(void))(sys_call_table[__NR_fork]);
printk("got origin func\n");

printk("try to change mode\n");
origin_cr0 = clear_cr0();
sys_call_table[__NR_fork] = (unsigned long)&hacked_func;
printk("changed successfully\n");
setback_cr0(origin_cr0);
return 0;
}

static void __exit exit_mymodule(void) {
origin_cr0 = clear_cr0();
sys_call_table[__NR_fork] = (unsigned long)origin;
setback_cr0(origin_cr0);
printk(KERN_DEBUG "goodbye~\n");
}

/*
我们的函数
*/
asmlinkage long hacked_func() {
printk("hack successfully!\n");
return origin();
}

执行装载:

1
2
$ sudo insmod mymodule.ko
KILLED

查看dmesg,我们看到了上篇文章出现的熟悉字样PAGE FAULT,这个问题困扰了很久,因为我们的代码是没错的。

原因在于kaslr(Kernel Address Space Layout Randomization),盲猜是为了防止栈溢出攻击(滑稽。我们可以很轻松地在grub中添加启动选项-nokaslr来关闭它。(这样的行为是不安全的,完全是为了实验目的,真实情况还需采用其他方式!)

1
2
3
$ sudo vim /etc/default/grub
# ...
$ sudo reboot

No Kernel Address Space Layout Randomization

期间还碰到一件很有意思的事情,在我调试尝试用printk("%p")去打印sys_call_table的地址时,发现我的地址打印错误!刚开始有点怀疑人生,后来查阅资料才知道,printk直接打印的%p地址会经过哈希加密防止内核地址泄漏。How to get printk format specifiers right

实现我们的功能

这个模块的实际功能并不复杂,主要有一点需要注意的是关于进程父子关系链表的实现逻辑。

task_struct中,我们可以看到有两个list_head字段:childrensibling

我们可以这样认为,因为在linux的链表实现中,采用的是空头节点的双向循环链表,而children可以理解为儿子节点的头节点,扩展到container的角度讲,当前节点就是儿子节点的空头节点,而sibling我们可以理解为是当前节点的条目(entry)。

按照上述逻辑,我们可以通过以下方式来达到遍历儿子节点的目的:

1
2
3
4
struct task_struct *pos = NULL;
list_for_each_entry(pos, p_children, sibling) {
// do something
}

源代码

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
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/kallsyms.h>

static int clear_cr0(void) {
unsigned int cr0 = 0;
unsigned int ret;
asm volatile("mov %%cr0,%%rax"
: "=a"(cr0));
ret = cr0;
cr0 &= 0xfffeffff; //将cr0变量值中的第17位清0
asm volatile("mov %%rax,%%cr0" ::"a"(cr0));
return ret;
}

static void setback_cr0(int val) {
asm volatile("mov %%rax,%%cr0" ::"a"(val));
}

unsigned long *sys_call_table;
asmlinkage long (*origin)(void) = NULL;

asmlinkage long hacked_func(void);
int origin_cr0;

static int __init init_mymodule(void) {
printk("start init\n");
sys_call_table = (unsigned long *) 0xffffffff82000300;
printk("the syscall table's address is %lx\n", (unsigned long)sys_call_table);

origin = (long (*)(void))(sys_call_table[__NR_fork]);
printk("got origin func\n");

printk("try to change mode\n");
origin_cr0 = clear_cr0();
// make_rw((unsigned long) sys_call_table);
sys_call_table[__NR_fork] = (unsigned long)&hacked_func;
printk("changed successfully\n");
// make_ro((unsigned long) sys_call_table);
setback_cr0(origin_cr0);
return 0;
}

static void __exit exit_mymodule(void) {
origin_cr0 = clear_cr0();
sys_call_table[__NR_fork] = (unsigned long)origin;
setback_cr0(origin_cr0);

printk(KERN_DEBUG "goodbye~\n");
}

void print_children(struct task_struct *p_task) {
struct task_struct *pos = NULL;
struct list_head *p_children;
p_children = &p_task->children;
if(list_empty(p_children)) {
printk("Children: (null)\n");
} else {
printk("Children: \n");
list_for_each_entry(pos, p_children, sibling) {
printk("\t%d\n", pos->pid);
}
}
}

void print_task(struct task_struct *p_task) {
printk("pid is %d\n", p_task->pid);
printk("Parent's pid is %d\n", p_task->real_parent->pid);
print_children(p_task);
}

asmlinkage long hacked_func() {
pid_t new_pid;
struct pid *p_pid;
struct task_struct *p_new;

printk("hack successfully!\n");
new_pid = origin();

printk("[Current]\n");
print_task(current);
if(new_pid != -1) {
p_pid = find_get_pid(new_pid);
p_new = get_pid_task(p_pid, PIDTYPE_PID);
printk("[Fork]\n");
print_task(p_new);
} else {
printk("fork fail\n");
return new_pid;
}

return new_pid;
}

module_init(init_mymodule);
module_exit(exit_mymodule);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("yztz");
MODULE_VERSION("v1");
MODULE_DESCRIPTION("This is my module");

参考文献

https://blog.csdn.net/qq_37414405/article/details/84487591
https://stackoverflow.com/questions/58523370/kernel-module-crash-when-reading-system-call-table-function-address
https://elixir.bootlin.com/linux/v5.15-rc6/source/include/linux/list.h#L280
https://wohin.me/linux-rootkit-shi-yan-0004-ling-wai-ji-chong-xi-tong-diao-yong-gua-gou-ji-zhu/
https://www.kernel.org/doc/html/latest/core-api/printk-formats.html
https://www.cnblogs.com/jiayy/p/3562055.html