Linux内核模块编程-系统调用拦截

时间:2022-01-13 15:46:10

系统调用拦截

本文实验基于Centos6.5 内核版本2.6.32-431.el6.x86_64,系统调用拦截的目的其实就是把系统真正要执行的系统调用替换为我们自己写的内核函数。

系统调用拦截的实现

那么如何去实现系统调用拦截呢,通过学习系统调用的内核实现我们发现其实系统调用的地址是放在sys_call_table中通过系统调用号定位到具体的系统调用地址,然后开始调用,那么通过编写内核模块来修改sys_call_table中指定系统调用的地址,其实就可以实现系统调用拦截的功能,这种方案在早期是很容易做到的,但是现在的内核做了一定的变动将sys_call_table没有导出,也就是说无法在内核模块中直接使用sys_call_table,还有一点就是sys_call_table所在内存页是只读的,无法进行修改。因此要想实现系统调用拦截需要解决上面的两个问题。对于第一个问题,sys_call_table地址的问题,可以通过内核生成的符号表找到其地址,然后进行更改。对于第二个问题,修改内存页的属性,让其变为可写。

代码分析

要添加的头文件,这里添加的头文件可能有些没有用到,但是却引入了,有兴趣的读者可以进行修改。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
//编写内核模块所需要的一些头文件,比如模块注册和注销的宏等
#include <linux/sched.h>
#include <linux/fs.h>
#include <linux/file.h>
#include <linux/fs_struct.h>
#include <linux/fdtable.h>
#include <linux/string.h>
#include <linux/mm.h>
#include <linux/syscalls.h>
#include <linux/list.h>
#include <linux/jiffies.h>
#include <linux/cdev.h>
#include <asm/unistd.h>
#include <asm/uaccess.h>
#include <linux/path.h>
#include <linux/time.h>
#include <linux/stat.h>
#include <net/sock.h>
#include <net/inet_sock.h>
#include <asm/cpufeature.h>

设置sys_call_table的地址

因为sys_call_table没有使用EXPORT宏进行导出,所以需要通过查找符号表来找到sys_call_table,但是网上也有人根据二进制查找的方法找到sys_call_table的地址。这里简单说一下EXPORT宏,这个宏只是利用了.global这个汇编的伪指令将符号声明为全局的而已。下面是如何通过符号表来查找sys_call_table地址

//通过内核符号表查找到的sys_call_table的地址
//grep sys_call_table /boot/System.map-`uname -r`
unsigned long **sys_call_table = (unsigned long **)0xffffffff81600560;
unsigned long *orig_mkdir = NULL; //用来指向系统调用地址的

内存页读写属性修改

为了可以对sys_call_table所在内存地址处,进行读写,需要重新设置该地址对应的页表项的属性。物理地址本来是没有什么读写属性的,内核只通过修改物理地址对应的页表项的一些属性位来设置该物理地址的读写属性而已。下面是具体修改的过程。

/* make the page writable */
int make_rw(unsigned long address)
{
unsigned int level;
pte_t *pte = lookup_address(address, &level);//查找虚拟地址所在的页表地址
//设置页表读写属性
pte->pte |= _PAGE_RW;

return 0;
}
/* make the page write protected */
int make_ro(unsigned long address)
{
unsigned int level;
pte_t *pte = lookup_address(address, &level);
pte->pte &= ~_PAGE_RW; //设置只读属性

return 0;
}

编写要替换的系统调用

为了进行系统调用的替换,需要知道即将被替换的系统调用的函数原型,下面将要替换mkdir这个系统调用。

//mkdir的函数原型,这个函数的原型要和系统的一致
asmlinkage long hacked_mkdir(const char __user *pathname, int mode)
{
printk("mkdir pathname: %s\n", pathname);
printk(KERN_ALERT "mkdir do nothing!\n");

return 0; /*everything is ok, but he new systemcall does nothing*/
}

开始进行替换

在模块载入的时候,先保存原有的系统调用的地址,然后再修改sys_call_table所在页表的属性,为可写的,然后再赋值为新的我们自己的系统调用地址。

//也是内核初始化函数
static int syscall_init_module(void)
{
printk(KERN_ALERT "sys_call_table: 0x%p\n", sys_call_table);
orig_mkdir = (unsigned long *)(sys_call_table[__NR_mkdir]); //获取原来的系统调用地址
printk(KERN_ALERT "orig_mkdir: 0x%p\n", orig_mkdir);

make_rw((unsigned long)sys_call_table); //修改页属性
sys_call_table[__NR_mkdir] = (unsigned long *)hacked_mkdir; //设置新的系统调用地址
make_ro((unsigned long)sys_call_table);

return 0;
}

恢复原状

卸载模块的时候注意需要将原有的系统调用进行还原。

//内核注销函数
static void syscall_cleanup_module(void)
{
printk(KERN_ALERT "Module syscall unloaded.\n");

make_rw((unsigned long)sys_call_table);
sys_call_table[__NR_mkdir] = (unsigned long *)orig_mkdir;
/*set mkdir syscall to the origal one*/
make_ro((unsigned long)sys_call_table);
}

模块的注册相关

module_init(syscall_init_module);
module_exit(syscall_cleanup_module);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("hack syscall");

编译并进行测试

[root@localhost module]# insmod syscall.ko
[root@localhost module]# mkdir test //发现并没有创建test目录,查看下内核输出信息
[root@localhost module]# dmesg
sys_call_table: 0xffffffff81600560
orig_mkdir: 0xffffffff8119ac40
mkdir pathname: test
mkdir do nothing!
[root@localhost module]# rmmod syscall //卸载模块
Module syscall unloaded.
sys_call_table: 0xffffffff81600560
orig_mkdir: 0xffffffff8119ac40
mkdir pathname: test
mkdir do nothing!
Module syscall unloaded.