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 |
... 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 |
// 由于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,现在我们可以对系统调用监听、过滤和审计了。