使用内核模块增加系统调用(不需要重新编译整个内核)

本文环境

ubuntu 20.04(内核版本 5.4.0-42-generic)

你可能需要的资源
ubuntu20.04下载地址
单独内核下载地址

查看预留系统调用号

不同内核版本,文件位置有所不同,我们可以直接查找unistd_64.h文件,命令如下:

sudo find / -name unistd_64.h

选择一个查看内容

命令格式 vim 文件路径
示例vim /usr/src/linux-headers-5.4.0-42-generic/arch/x86/include/generated/uapi/asm/unistd_64.h

查看空闲调用号

J 键控制向下移动
K向上移动

一般下拉到334左右,发现335没有出现,说明335是一个空闲调用号

先按下esc健,再输入:q即可退出文件

获取系统调用表 sys_call_table 的虚拟地址(本步骤可跳过)

  • 法一
    kallsyms包含了kernel image和动态加载模块的符号表,包括内核中的函数符号(包括没有EXPORT_SYMBOL导出的符号)、全局变量(用EXPORT_SYMBOL导出的全局变量),函数如果被编译器内联(inline)或优化掉,则它在/proc/kallsyms有可能找不到。此方法由变量名获取虚拟地址使用如下命令:
    sudo cat /proc/kallsyms | grep sys_call_table
    可以看到系统调用表 sys_call_table 的虚拟地址为ffffffffbae013a0,应写0xbae013a0
    通过R标志可以看出它是只读的。在代码中也可以使用kallsyms_lookup_name()函数获取此值,具体使用方法请看内核模块代码。

  • 法二
    System.map是一份内核符号表kernel symbol table,包含了内核中的变量名和函数名地址,在每次编译内核时,自动生成。由变量名获取虚拟地址使用如下命令:
    sudo cat /boot/System.map-uname -r | grep sys_call_table
    可以看到系统调用表 sys_call_table 的虚拟地址为ffffffff820002a0,与方法一获得的虚拟地址不同,这是因为正在运行的内核可能和System.map不匹配,出现System.map does not match actual kernel,/proc/kallsyms中增加的函数符号是后来安装程序中引入的,而system.map仅仅是kenrel编译时生成的符号表,所以/proc/kallsyms才是参考的主要来源,一般通过/proc/kallsyms获得符号的地址。

编写内核模块代码,添加系统调用

在~/目录下新建syscall.c 和Makefile文件

为了修改内存中的表项,还要修改寄存器写保护位。内核模块代码和详细注释如下
syscall.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/unistd.h>
#include <linux/time.h>
#include <linux/uaccess.h>
#include <linux/sched.h>
#include <linux/kallsyms.h>
#define __NR_syscall 335    /* 系统调用号335 */
unsigned long * sys_call_table;

unsigned int clear_and_return_cr0(void);
void setback_cr0(unsigned int val);
static int sys_mycall(void);

int orig_cr0;   /* 用来存储cr0寄存器原来的值 */
unsigned long *sys_call_table = 0;
static int (*anything_saved)(void); /*定义一个函数指针,用来保存一个系统调用*/
/*
 * 设置cr0寄存器的第17位为0
 */
unsigned int clear_and_return_cr0(void) 
{
    unsigned int cr0 = 0;
    unsigned int ret;
    /* 前者用在32位系统。后者用在64位系统,本系统64位 */
    //asm volatile ("movl %%cr0, %%eax" : "=a"(cr0));   
    asm volatile ("movq %%cr0, %%rax" : "=a"(cr0)); /* 将cr0寄存器的值移动到rax寄存器中,同时输出到cr0变量中 */
    ret = cr0;
    cr0 &= 0xfffeffff;  /* 将cr0变量值中的第17位清0,将修改后的值写入cr0寄存器 */
    //asm volatile ("movl %%eax, %%cr0" :: "a"(cr0));
    asm volatile ("movq %%rax, %%cr0" :: "a"(cr0)); /* 读取cr0的值到rax寄存器,再将rax寄存器的值放入cr0中 */
    return ret;
}

/* 读取val的值到rax寄存器,再将rax寄存器的值放入cr0中 */
void setback_cr0(unsigned int val)
{   

    //asm volatile ("movl %%eax, %%cr0" :: "a"(val));
    asm volatile ("movq %%rax, %%cr0" :: "a"(val));
}

/* 添加自己的系统调用函数 */
static int sys_mycall(void)
{
    int ret = 12345;
    printk("My syscall is successful!\n");
    return ret;
}

/*模块的初始化函数,模块的入口函数,加载模块*/
static int __init init_addsyscall(void)
{
    printk("My syscall is starting....\n");
    sys_call_table = (unsigned long *)kallsyms_lookup_name("sys_call_table"); /* 获取系统调用服务首地址 */
    printk("sys_call_table: 0x%p\n", sys_call_table);
    anything_saved = (int(*)(void))(sys_call_table[__NR_syscall]);  /* 保存原始系统调用 */
    orig_cr0 = clear_and_return_cr0();  /* 设置cr0可更改 */
    sys_call_table[__NR_syscall] = (unsigned long)&sys_mycall;  /* 更改原始的系统调用服务地址 */
    setback_cr0(orig_cr0);  /* 设置为原始的只读cr0 */
    return 0;
}

/*出口函数,卸载模块*/
static void __exit exit_addsyscall(void)
{
    orig_cr0 = clear_and_return_cr0();  /* 设置cr0中对sys_call_table的更改权限 */
        sys_call_table[__NR_syscall] = (unsigned long)anything_saved;   /* 设置cr0可更改 */
        setback_cr0(orig_cr0);  /* 恢复原有的中断向量表中的函数指针的值 */
    printk("My syscall exit....\n");  /* 恢复原有的cr0的值 */
}

module_init(init_addsyscall);
module_exit(exit_addsyscall);
MODULE_LICENSE("GPL");

Makefile文件

obj-m:=syscall.o
PWD:= $(shell pwd)
KERNELDIR:= /lib/modules/$(shell uname -r)/build
EXTRA_CFLAGS= -O0

all:
    make -C $(KERNELDIR)  M=$(PWD) modules
clean:
    make -C $(KERNELDIR) M=$(PWD) clean

编写测试程序

test.c

#include <syscall.h>
#include <stdio.h>
int main(void)
{
    printf("%d\n",syscall(335));
    return 0;
}

编译内核模块并安装

介绍

功能 命令 说明
编译模块 make 执行第一个目标default,生成syscall.ko,这个就是我们需要的内核模块。
编译清理 make clean 清理编译产生的文件,syscall.ko 也会清理掉。
插入模块 sudo insmod syscall.ko 用dmesg就可以看到产生的内核信息,My syscall is starting。内核消息也会输出到日志文件/var/log/kern.log中。
卸载模块 sudo rmmod syscall 用dmesg可以看到Goodbye!

实际操作

make //编译内核模块
sudo insmod syscall.ko //安装

进行测试

gcc syscall.c -o test //编译测试程序
./test //运行测试程序
dmesg //查看日志,可以看到My syscall is successful!

参考文献
文献1