拦截系统调用

今天在ubuntu中玩了下“拦截系统调用”,记录下自己对整个实现的理解。

原理

在linux kernel中,系统调用都放在一个叫做“sys_call_table”的分配表里面,在进入一个系统调用的最后一步,会调用与eax中包含的系统调用号对应的特定服务例程:

call *sys_call_table(,%eax,4)

因为分派表中的每个表项占4个字节,因此首先把系统调用号乘以4,再加上sys_call_table分配表的起始地址,然后从从这个地址单元获取指向服务例程的指针,内核就找到了要调用的服务例程。我们只要修改对应的分配表项,即可实现系统调用的拦截。

获取sys_call_table的地址

网上介绍了很多种方法得到sys_call_table的地址,我使用了相对简单的一种方法——从内核导出的符号表中获取。

图中,十六进制数c15b3000即为sys_call_table的地址。同时,我们也得到了一个重要的信息,该符号对应的内存区域是只读的!

清除写保护

因为sys_call_table分配表的内存属性为只读,因此,我们要先清除对应地址的写保护。暂时使用了两种方法实现该目的:

第一种方法,修改cr0读写保护位:

/* 清除写保护 */
unsigned int clear_and_return_cr0(void)
{
        unsigned int cr0 = 0;
        unsigned int ret;

        asm volatile ("movl %%cr0, %%eax"
                        : "=a"(cr0)
                     );
        ret = cr0;

        /* clear the 16 bit of CR0, a.k.a WP bit */
        cr0 &= 0xfffeffff;

        asm volatile ("movl %%eax, %%cr0"
                        :
                        : "a"(cr0)
                     );

        return ret;
}

/* 设置cr0,--本程序用来恢复写保护 */
void setback_cr0(unsigned int val)
{
        asm volatile ("movl %%eax, %%cr0"
                        :
                        : "a"(val)
                     );
}

第二种方法,设置虚拟地址对应页表项的读写属性:

/* make the page writable */
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;
}


/* 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;
}

附:完整代码

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



/* grep sys_call_table  /boot/System.map-`uname -r` */
unsigned long **sys_call_table = (unsigned long **)0xc15b3000;
unsigned long *orig_mkdir = NULL;


/* make the page writable */
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;
}


/* 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;
}



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*/
}


static int syscall_init_module(void)
{
        printk(KERN_ALERT "sys_call_table: 0x%lx\n", sys_call_table);
        orig_mkdir = (unsigned long *)(sys_call_table[__NR_mkdir]);
        printk(KERN_ALERT "orig_mkdir: 0x%lx\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");


参考:

1、《深入理解linux内核(第三版)》

2、Hijack Linux System Calls: Part III. System Call Table

你可能感兴趣的:(拦截系统调用)