Kernel Module实战指南(四):系统调用劫持

Introduction

Kernel Module还可以做一些比较cool的事情,比如劫持系统调用,增加我们自己的逻辑,在系统调用监听、过滤和审计的场景使用。

系统调用概述

劫持系统调用是一件比较危险的事情,例如劫持open()系统调用,并且阻止一切open()的操作,那么计算机将不能够打开任何文件,甚至无法关闭计算机,唯一能做的事情只有冷重启计算机。
通常来讲,用户进程不允许直接访问内核,不能访问内核内存,也不能使用内核函数,这由CPU架构来保证,无法改变。
为什么是通常来讲?因为有一个例外,那就是系统调用。发生系统调用时,用户进程将所需的值(系统调用号、系统调用参数)压入寄存器中,然后调用一条特殊的CPU指令使程序从用户态切换到内核态继续执行。这个特殊的CPU指令,在Intel X86架构下,就是所谓的interrupt 0x80,即80中断。当系统调用结束后,通过同样的方式,从内核态切换到用户态,继续执行程序。
程序切入到系统态后,会根据寄存器中的系统调用号,在系统调用表(sys_call_table)中,执行相应的系统调用处理函数。

系统调用劫持代码

劫持系统调用是一个高危操作,在2.6.24内核之前,可以简单的替换sys_call_table中的系统调用函数地址。
下面给出2.6.24内核之前的劫持系统调用open()/__NR_open的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <linux/kernel.h>
#include <linux/module.h>
...
extern void *sys_call_table[];
asmlinkage int (*original_open)(const char *, int, int);
asmlinkage int hijack_open(const char *filename, int flags, int mode) {
// do hijack logic, just print the parameter
printk(KERN_INFO "hijack: open(%s, %d, %d)\n", filename, flags, mode);
return original_open(filename, flags, mode);
}
int init_module() {
original_open = sys_call_table[__NR_open];
sys_call_table[__NR_open] = hijack_open;
return 0;
}
void cleanup_module() {
sys_call_table[__NR_open] = original_open;
}

不幸的是,由于劫持系统调用产生的安全性问题(如监听、窃取),Linux Kernel在2.6.24将sys_call_table的内存地址变为只读,用上述的方法写地址时会失败。
稍后,有大神给出了一段代码,可以将只读页变成可读,这样就可以修改系统调用表了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int set_page_ro(long unsigned int _addr) {
struct page *pg;
pgprot_t prot;
pg = virt_to_page(_addr);
prot.pgprot = VM_READ;
return change_page_attr(pg, 1, prot);
}
int set_page_rw(long unsigned int _addr) {
struct page *pg;
pgprot_t prot;
pg = virt_to_page(_addr);
prot.pgprot = VM_READ | VM_WRITE;
return change_page_attr(pg, 1, prot);
}

过了一段时间后,change_page_attr函数也被禁用了,不过还是有别的方法可以绕过,下面给出一个在3.19.0内核下仍然可用的代码:

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
#include <asm/unistd.h>
#include <linux/module.h>
#include <linux/highmem.h>
// 由于sys_call_table符号不再被导出,需要hardcode地址,
// 地址需要在bash下键入下面命令进行查找:
// $ grep sys_call_table /boot/System.map-3.19.0-25-generic
unsigned long *sys_call_table = (unsigned long*) 0xffffffff81600480;
asmlinkage int (*original_open)(const char *, int, int);
asmlinkage int hijack_open(const char *filename, int flags, int mode) {
// do hijack logic, just print the parameter
printk(KERN_INFO "hijack: open(%s, %d, %d)\n", filename, flags, mode);
return original_open(filename, flags, mode);
}
int make_ro(unsigned long address) {
unsigned int level;
pte_t *pte = lookup_address(address, &level);
pte->pte = pte->pte &~ _PAGE_RW;
return 0;
}
int make_rw(unsigned long address) {
unsigned int level;
pte_t *pte = lookup_address(address, &level);
if(pte->pte &~ _PAGE_RW) {
pte->pte |= _PAGE_RW;
}
return 0;
}
int init_module(void) {
make_rw((unsigned long)sys_call_table);
original_open = (void*)*(sys_call_table + __NR_open);
*(sys_call_table + __NR_open) = (unsigned long)hijack_open;
make_ro((unsigned long)sys_call_table);
return 0;
}
void cleanup_module(void) {
make_rw((unsigned long)sys_call_table);
*(sys_call_table + __NR_open) = (unsigned long)original_open;
make_ro((unsigned long)sys_call_table);
}

加载模块后,通过dmesg查看日志:

$ dmesg
...
[116465.803107] 'hijack: open("/proc/33162/status", 80000, 0)
[116465.803107] 'hijack: open("/etc/passwd", 0, 1B6)
...

Summary

通过编写一个劫持open()系统调用的Linux Kernel Module,现在我们可以对系统调用监听、过滤和审计了。

(本文出自csprojectedu.com,转载请注明出处)