跳转至

实战通过内核模块hook全部系统调用

使用内核模块添加系统调用

参考

https://mp.weixin.qq.com/s/m2eZ0lEzQHjrNVC6YCC_IA
https://zhuanlan.zhihu.com/p/446784403
https://zhuanlan.zhihu.com/p/446785414
https://zhuanlan.zhihu.com/p/446783415
https://zhuanlan.zhihu.com/p/296750228
https://blog.csdn.net/ShirokoYae/article/details/119414124
https://blog.csdn.net/ShirokoYae/article/details/119036040
https://zhuanlan.zhihu.com/p/480934356
https://blog.csdn.net/thugkd/article/details/50117125
https://www.cnblogs.com/uestc-mm/p/7644966.html

系统调用其实就是函数调用,只不过调用的是内核态的函数,但是我们知道,用户态是不能随意调用内核态的函数的,所以采用软中断的方式从用户态陷入到内核态。在内核中通过软中断0X80,系统会跳转到一个预设好的内核空间地址,它指向了系统调用处理程序(不要和系统调用服务例程混淆),这里指的是在entry.S文件中的system_call函数。就是说,所有的系统调用都会统一跳转到这个地址执行system_call函数,那么system_call函数如何派发它们到各自的服务例程呢?

我们知道每个系统调用都有一个系统调用号。同时,内核中一个有一个system_call_table数组,它是个函数指针数组,每个函数指针都指向了系统调用的服务例程。这个系统调用号是system_call_table的下标,用来指明到底要执行哪个系统调用。当int ox80的软中断执行时,系统调用号会被放进eax寄存器中,system_call函数可以读取eax寄存器获得系统调用号,将其乘以4得到偏移地址,以sys_call_table为基地址,基地址加上偏移地址就是应该执行的系统调用服务例程的地址。

系统调用的传参问题

当一个系统调用的参数个数大于5时(因为5个寄存器(eax, ebx, ecx, edx,esi)已经用完了),执行int 0x80指令时仍需将系统调用功能号保存在寄存器eax中,所不同的只是全部参数应该依次放在一块连续的内存区域里,同时在寄存器ebx中保存指向该内存区域的指针。系统调用完成之后,返回值扔将保存在寄存器eax中。由于只是需要一块连续的内存区域来保存系统调用的参数,因此完全可以像普通函数调用一样使用栈(stack)来传递系统调用所需要的参数。但是要注意一点,Linux采用的是c语言的调用模式,这就意味着所有参数必须以相反的顺序进栈,即最后一个参数先入栈,而第一个参数则最后入栈。如果采用栈来传递系统调用所需要的参数,在执行int 0x80指令时还应该将栈指针的当前值复制到寄存器ebx中。

编译内核法

拿到linux源码之后

  • 修改内核的系统调用库函数 /usr/include/asm-generic/unistd.h,在这里面可以使用在syscall_table中没有用到的223号或者 Nr_bpf(321号)
  • 添加系统调用号,让系统根据这个号,去找到syscall_table中的相应表项。在 /arch/x86/kernel/syscall_table_32.s文件中添加系统调用号和调用函数的对应关系
  • 接着就是my_syscall的实现了,在这里有两种方法:第一种方法是在kernel下自己新建一个目录添加自己的文件,但是要编写Makefile,而且要修改全局的Makefile。第二种比较简便的方法是,在kernel/sys.c中添加自己的服务函数,这样子不用修改Makefile.

以上准备工作做完之后,然后就要进行编译内核了,以下是我编译内核的一个过程。

  1. make menuconfig (使用图形化的工具,更新.config文件)
  2. make -j3 bzImage (编译,-j3指的是同时使用3个cpu来编译,bzImage指的是更新grub,以便重新引导)
  3. make modules (对模块进行编译)
  4. make modules_install(安装编译好的模块)
  5. depmod (进行依赖关系的处理)
  6. reboot (重启看到自己编译好的内核)

编译内核的方式费时间,一般的PC机都要两三个小时。

不方便调试,一旦出现问题前面的工作都前功尽弃。

内核模块法

这种方法是采用系统调用拦截的一种方式,改变某一个系统调用号对应的服务程序为我们自己的编写的程序,从而相当于添加了我们自己的系统调用。具体实现,我们来看下:

系统调用服务程序的地址是放在sys_call_table中通过系统调用号定位到具体的系统调用地址,那么我们通过编写内核模块来修改sys_call_table中的系统调用的地址为我们自己定义的函数的地址,就可以实现系统调用的拦截。 想法有了:那就是通过模块加载时,将系统调用表里面的那个系统调用号的那个系统调用号对应的系统调用服务例程改为我们自己实现的系统历程函数地址。但是内核已经不知道从哪个版本就不支持导出sys_call_table了。所以首先要获取sys_call_table的地址。 网上介绍了好多种方法来得到sys_call_table的地址,这里介绍最简单的一种方法

$ sudo cat /proc/kallsyms | grep sys_call_table
$ sudo cat /boot/System.map-`uname -r` | grep sys_call_table

还有一个运行时获得地址的方式, 注意此方法在linux内核吧按本5.7之后不再支持

kallsyms_lookup_name() 接受一个字符串格式内核函数名, 返回那个内核函数的地址.

kallsyms_lookup_name("函数名");

kallsyms_lookup_name()能找到很多没有导出的符号。它的原理和insmod插入时的寻找符号是不一样的,它取决于一个config(CONFIG_KALLSYMS),开启了这个开关就会从一个数组中找到很多符号。

这样就得到了sys_call_table的地址,但同时也得到了一个重要的信息,该符号对应的内存区域是只读的。所以我们要修改它,必须对它进行清楚写保护

编写Makefile

obj-m := sys_module_stu.o
CURRENT_PATH := $(shell pwd)
LINUX_KERNEL := $(shell uname -r)
LINUX_KERNEL_PATH := /usr/lib/modules/6.1.12-1-MANJARO/build/include

all:
    make -C $(LINUX_KERNEL_PATH)  M=$(CURRENT_PATH) modules

clean:
    rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions modules.order
  1. obj-m:在Kbuild系统中,obj-m表示是要编译为可加载module,此处填写的生成模块名需同C代码名一致
  2. LINUX_KERNEL:获取内核版本
  3. LINUX_KERNEL_PATH:获取内核源码存放路径
  4. all:默认编译目标,实质是调用外部Makefile(位于内核源码根目录下)(make -C 在执行前先切换到指定目录])(make -M=dir 设置编译的生成目录)
  5. modules:位于内核源码根目录下的那个Makefile文件中定义的一个编译目标

编译模块

安装模块之前先安装gcc``g++

    $ yum install gcc
    $ yum install gcc-c++

出现错误需要安装错误出现的顺序从上到下的修改

$ make

如果make 失败, 提示缺少内核头文件 请检查kernel-headers 或安装

$ install -y kernel-devel-$(uname -r)
# 或者
$ install -y kernel-devel

错误: arch/x86/Makefile:96: stack-protector enabled but compiler support broken

出现这个错误 直接编辑 /usr/src/kernels/3.10.0-1160.el7.x86_64/include/config/auto.conf/usr/src/kernels/3.10.0-1160.el7.x86_64/.config搜索CONFIG_RETPOLINE=y等关键字, 然后注释掉

错误: -fstack-protector-strong not supported by compiler

我们当前的gcc编译器版本不支持-fstack-protector-strong这个参数,这个参数的调用时在linux-headers-4.4.0-96-generic相关文件中的,既然不支持,那我们就找个支持的gcc版本来编译运行就可以了,通过查阅相关资料得到:‘-fstack-protector-strong’ 选项是gcc4.9以后的版本才加入的,也就是说需要安装gcc4.9以后的版本才可以编译通过,所以直接安装gcc5.4就行了,关于安装多个版本的gcc的内容请参考我的这篇文章:Ubuntu16.04多个版本GCC编译器的安装和切换完成安装之后直接make编译就不会出现上述问题了

Assembler messages: Error: unsupported instruction `mov'

代码去64位linux系统下编译, gcc则会报错:unsupported instruction mov , 你内联会用应该用了movl或者mov, 使用movq即可

安装模块

$ sudo sys_module_stu.ko
$ lsmod | grep sys_module_stu #检查是否插入成功
$ dmesg  # 校验是否输出了我们的init日志

卸载模块

$ rmmod sys_module_stu
$ lsmod | grep sys_module_stu #检查是否插入成功
$ dmesg  # 校验是否输出了我们的删除日志

校验模块

这里再编写一个程序校验我们刚才编写的模块是否能够成功调用

模块代码

sys_module_stu.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/unistd.h>
#include <linux/kallsyms.h>
#include <linux/sched.h>
#include <linux/sched/task.h>  // 引入了for_each_process 这个宏


typedef int i32;
typedef unsigned int u32;
typedef float f32;
typedef double f64;
typedef long long i64;
typedef unsigned long long usize;
typedef void None;

static usize _NR_MY_SYS_CALL = 312;
static usize *ANYTHING_SAVED_CALL_ADDR;
static usize *SYS_CALL_TABLE_ADDR;


u32 set_cr0_writeable(None) {
    // 设置cr0寄存器为新的值, 返回原有的cr0寄存器的值
    // movl用在32位系统, movq用在64位系统,本系统64位
    // 32位使用 eax寄存器, 64位使用rax寄存器
    // 需要设置的cr0
    u32 my_cr0 = 0;

    // 内联汇编: mov eax, cr0; mov my_cr0, eax; 把当前cr0的值保存到变量中
    asm volatile("movq %%cr0, %%rax"
        : "=a"(my_cr0)
        :
    );
    u32 old_cr0 = my_cr0;

    // 设置新值 将cr0变量值中的第17位清0,将修改后的值写入cr0寄存器
    my_cr0 &= 0xfffeffff;

    // 内联汇编: mov eax, my_cr0; mov cr0, eax;
    asm volatile("movq %%rax, %%cr0"
        :
        :"a"(my_cr0)
    );
    return old_cr0;
}


None set_cr0_readable(u32 new_cr0) {
    asm volatile("movq %%rax, %%cr0"
        :
        :"a"(new_cr0)
    );
}


static i32 my_sys_call(u32 rdi, u32 rsi, u32 rdx, u32 r10, u32 r8, u32 r9){
    printk("rdi %d, rsi %d, rdx %d, r10 %d, r10 %d, r8 %d, r9 %d", rdi, rsi, rdx, r10, r8, r9);

    // 遍历所有的进程
    struct task_struct * task;
    for_each_process(task) {
        printk("%s[%d]\n", task->comm, task->pid);
    }

    // 找到当前进程
    task = current;
    printk("now name: %s, pid: %d\n", task->comm, task->pid);
    return rdi;
}


static i32 __init init_my_sys(None)
{
    printk("My syscall is starting。。。\n");
    // 获得系统调用表的地址
    SYS_CALL_TABLE_ADDR = (usize *)kallsyms_lookup_name("sys_call_table");
    printk("SYS_CALL_TABLE_ADDR: 0x%p\n", SYS_CALL_TABLE_ADDR);

    // 获得需要替换的函数在调用表中的地址
    ANYTHING_SAVED_CALL_ADDR = (i32(*)(None))(SYS_CALL_TABLE_ADDR[_NR_MY_SYS_CALL]);
    printk("ANYTHING_SAVED_CALL_ADDR: 0x%p\n", ANYTHING_SAVED_CALL_ADDR);

    // 设置cr0可改
    u32 old_cr0 = set_cr0_writeable();

    // 替换
    SYS_CALL_TABLE_ADDR[_NR_MY_SYS_CALL] = (usize)&my_sys_call;

    // 设置cr0不可改
    set_cr0_readable(old_cr0);
    printk("OK!");

    return 0;
}

static None __exit clear_my_sys(None)
{
    // 设置cr0可改
    u32 old_cr0 = set_cr0_writeable();

    // 替换
    SYS_CALL_TABLE_ADDR[_NR_MY_SYS_CALL] = ANYTHING_SAVED_CALL_ADDR;

    // 设置cr0不可改
    set_cr0_readable(old_cr0);
    printk("My syscall exit....\n");
}

module_init(init_my_sys);          //注册加载函数
module_exit(clear_my_sys);        //注册卸载函数
MODULE_LICENSE("GPL");             //表明本module具有GNU公共许可证

测试代码

gcc -o test test.c
#include<stdio.h>
int main()
{
    int ret = syscall(312, 1, 2, 3, 4);
    printf("%d", ret);
    return 0;
}

在Arch linux中的代码

模块代码

注意有两个系统调用__NR_mmap__NR_mprotect一替换立马就宕机, 不知道为啥

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/unistd.h>
#include <linux/kallsyms.h>
#include <linux/sched/task.h>
#include <linux/moduleparam.h>
#include <asm/ptrace.h>

typedef int i32;
typedef unsigned int u32;
typedef float f32;
typedef double f64;
typedef long long i64;
typedef unsigned long long usize;
typedef void None;
typedef int (*CALL_FUNC)(struct pt_regs*);

static usize NR_Syscall_hook_id = __NR_rseq;
static usize SYS_CALL_TABLE_ADDR;
static usize PARENT_PID = 0xf1f2f3f4f5f6f7f8;
static usize SET_PARENT_KEY = 0xf1f2f3f4f5f6f7f8;
static usize SYS_CALL_TABLE_BCK[NR_syscalls];

module_param(SYS_CALL_TABLE_ADDR, ullong, S_IRUSR);


u32 set_cr0_writeable(None) {
    u32 my_cr0 = 0;

    asm volatile("movq %%cr0, %%rax"
            : "=a"(my_cr0)
            :
            );
    u32 old_cr0 = my_cr0;

    my_cr0 &= 0xfffeffff;

    asm volatile("movq %%rax, %%cr0"
            :
            :"a"(my_cr0)
            );
    return old_cr0;
}


None set_cr0_readable(u32 new_cr0) {
    asm volatile("movq %%rax, %%cr0"
            :
            :"a"(new_cr0)
            );
}


i32 set_parent(struct pt_regs *regs) {
    if (regs->di == SET_PARENT_KEY) {
        PARENT_PID = regs->si;
        return 0;
    }
    return -1;
}


static u64 base_sys_call(struct pt_regs *regs) {
    if (current->parent->pid == PARENT_PID) {
        printk("my hook: %lu, pid: %d\n", regs->orig_ax, current->pid);
    }
    CALL_FUNC fun = (CALL_FUNC)((usize *)SYS_CALL_TABLE_BCK[regs->orig_ax]);
    return fun(regs);
}


static i32 __init init_my_sys(None) {
    printk("SYS_CALL_TABLE_ADDR: %llu\n", SYS_CALL_TABLE_ADDR);
    // 设置cr0可改
    u32 old_cr0 = set_cr0_writeable();

    for (usize nr = 0; nr < NR_syscalls; nr++) {
        // 不知道为啥, 这俩替换立马就宕机
        if (nr == __NR_mmap || nr == __NR_mprotect) {
            continue;
        }

        usize old_addr = ((usize *)SYS_CALL_TABLE_ADDR)[nr];
        printk("init nr: %llu, addr: %llu\n", nr, old_addr);
        if (nr == NR_Syscall_hook_id) {
            SYS_CALL_TABLE_BCK[nr] = old_addr;
            ((usize *)SYS_CALL_TABLE_ADDR)[nr] = (usize)&set_parent;
        } else {
            SYS_CALL_TABLE_BCK[nr] = old_addr;
            ((usize *)SYS_CALL_TABLE_ADDR)[nr] = (usize)&base_sys_call;
        }
    };

    set_cr0_readable(old_cr0);
    printk("INIT END");
    return 0;
}

static None __exit clear_my_sys(None) {
    u32 old_cr0 = set_cr0_writeable();
    for (usize nr = 0; nr < NR_syscalls; nr++) {

        if (nr == __NR_mmap || nr == __NR_mprotect) {
            continue;
        }

        printk("exit SYS_CALL_TABLE_ADDR %llu", SYS_CALL_TABLE_ADDR);
        printk("exit nr: %llu, addr: %llu\n", nr, SYS_CALL_TABLE_BCK[nr]);
        ((usize *)SYS_CALL_TABLE_ADDR)[nr] = SYS_CALL_TABLE_BCK[nr];
    };
    set_cr0_readable(old_cr0);
    printk("syscall exit....\n");
}

module_init(init_my_sys);
module_exit(clear_my_sys);
MODULE_LICENSE("GPL");

Makefile

PROJECT_NAME := sys_module_stu1
obj-m := $(PROJECT_NAME).o
CURRENT_PATH := $(shell pwd)
LINUX_KERNEL := $(shell uname -r)
SYS_CALL_TABLE_ADDR := $(shell cat /proc/kallsyms | grep " sys_call_table" | cut -d " " -f 1)

LINUX_KERNEL_PATH := /usr/lib64/modules/$(LINUX_KERNEL)/build
LINUX_KERNEL_INCLUDE1 := /usr/lib64/modules/$(LINUX_KERNEL)/build/include
LINUX_KERNEL_INCLUDE2 := /usr/lib64/modules/6.1.12-1-MANJARO/build/arch/x86/include/

all:
    make -C $(LINUX_KERNEL_PATH) -L $(LINUX_KERNEL_INCLUDE1) -L $(LINUX_KERNEL_INCLUDE2) M=$(CURRENT_PATH) modules

ins:
    make -C $(LINUX_KERNEL_PATH) -L $(LINUX_KERNEL_INCLUDE1) -L $(LINUX_KERNEL_INCLUDE2) M=$(CURRENT_PATH) modules
    insmod $(PROJECT_NAME).ko SYS_CALL_TABLE_ADDR=0x$(SYS_CALL_TABLE_ADDR)

test:
    gcc -o test test.c
    ./test
rm:
    rmmod $(PROJECT_NAME)

clean:
    rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions modules.order

测试代码

#include<stdio.h>
#include <unistd.h>

int main()
{
    unsigned long long ret = syscall(334, 0xf1f2f3f4f5f6f7f8, 48471);
    printf("%llu\n", ret);
    return 0;
}