天天看点

系统调用捕获和分析—使用LKM方法添加系统调用

本文为毕业设计过程中学习相关知识、动手实践记录下来的完整笔记,通过阅读本系列文章,您可以从零基础了解系统调用的底层原理并对系统调用进行拦截。由于本人能力有限,文章中可能会出现部分错误信息,如有错误欢迎指正。

完整系列文章列表

系统调用捕获和分析—通过ptrace获取系统调用信息

系统调用捕获和分析—通过strace获取系统调用信息

系统调用捕获和分析—必备的系统安全的知识点

文章目录

  • LKM相关概念
  • 什么是LKM
  • 使用LKM可以干什么
  • 用户态和内核态代码区别
  • 管理内核模块
  • 制作第一个LKM—helloworld
  • 通过内核加载模块的方法添加一个系统调用

LKM相关概念

什么是LKM

LKM(Load Kernel Modules)是Linux内核为了扩展其功能所使用的可加载内核模块。

LKM的优点:动态加载,无须重新实现整个内核。

如果一个驱动程序被直接编译到了内核中,那么即使这个驱动程序没有运行,它的代码和静态数据也会占据一部分空间。但是如果这个驱动程序被编译成一个模块,就只有在需要内存并将其加载到内核时才会真正占用内存空间。

使用LKM可以干什么

如果想要自己向内核添加一个系统调用,有两种方法。

一是修改内核源码,然后重新编译整个内核来实现。少说一个小时!!!

二是使用LKM,可以编写并编译并连接成一组目标文件,这些文件能被插入到正在运行的内核,可省去编译新内核并用新内核重新启动的麻烦。

用户态和内核态代码区别

应用程序 内核模块
使用函数 libc库 内核函数
运行空间 用户空间 内核空间
运行权限 普通用户 超级用户
入口函数 main() module_init
出口函数 exit() module_exit
编译 gcc make
链接 gcc insmod
运行 直接运行 insmod
调试 gdb kdbug、kdb、kgdb

管理内核模块

使用insmod套件

列出所有内核模块​​

​lsmod​

​​ 加载或插入模块​

​insmod ​

​​ 删除模块​

​rmmod​

使用modprobe命令

获取模块信息 ​​

​modinfo module_name​

​​ 添加模块 ​

​modprobe -a module_name​

​​ 删除模块 ​

​modporbe -r module_name​

模块间的依赖关系

如果模块a引用了模块b,那么在加载的过程中如果先加载模块a再加载模块b,则会发生错误。

使用insmod就可能会发生这个错误,而modprobe由于自己的机制不同会智能地先加载模块b。

制作第一个LKM—helloworld

编写hello.c文件

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>

static int __init hello_init(void){
    printk(KERN_INFO "Hello world\n");
    return 0;
}

static void __exit hello_exit(void){
    printk(KERN_INFO "Goodbye world\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");    //许可证      

编写Makefile文件

obj-m := hello.o
  all:
  make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules
  clean:
  make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) clean      

​-C​

​​ linux内核源代码的目录

​​

​M=​

​ hello.c和Makefile所在的目录

注意all和clean以两个空格开头,make以tab键开头

编译

make      
系统调用捕获和分析—使用LKM方法添加系统调用

查看编译生成的模块信息

modinfo hello.ko      
系统调用捕获和分析—使用LKM方法添加系统调用

装载,查看装载结果

insmod hello.ko
lsmod | grep hello      
系统调用捕获和分析—使用LKM方法添加系统调用

查看系统开机和内核模块装载信息,发现报错

dmesg      
系统调用捕获和分析—使用LKM方法添加系统调用

报错解决方案

在Makefile中第一行加入​

​CONFIG_MODULE_SIG=n​

​​关闭签名,insmode hello.ko时再次报错​

​File exists​

​,原因是刚才加载了同名文件没有卸载掉,卸载重装后成功。

sudo rmmod hello.ko

sudo insmod hello.ko

dmesg

查看日志信息

日志信息位置​​

​/var/log/messages​

​​,但是没有找到。

编辑​​

​vim /etc/rsyslog.d/50-default.conf​

​​,取消messages注释(好几行都取消注释)

重启服务​​

​sudo service rsyslog restart​

​​ 重装模块​

​sudo rmmod hello.ko​

​​, ​

​sudo insmod hello.ko​

​​

​cat /var/log/messages​

​找到信息

内核入门链接​

​https://tldp.org/LDP/lkmpg/2.6/html/index.html​

通过内核加载模块的方法添加一个系统调用

先查看自己本次开机后的sys_call_table的地址,待会放在代码里。需要注意地址0x不能省略,且每次重启地址都会变化。

sudo cat /proc/kallsyms | grep sys_call_table      

编写hello.c文件

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/unistd.h>
#include <linux/sched.h>
 
//许可证
MODULE_LICENSE("Dual BSD/GPL");

//获取当前sys_call_table地址 -> sudo cat /proc/kallsyms | grep sys_call_table
#define SYS_CALL_TABLE_ADDRESS 0xffffffffbc200280  //sys_call_table对应的地址
#define NUM 334  //添加的系统调用号为334
unsigned long orig_cr0;  //用来存储cr0寄存器原来的值
unsigned long *sys_call_table_my=0;
 
static unsigned long(*anything_saved)(void);  //定义一个函数指针,用来保存一个系统调用

//使cr0寄存器的第17位设置为0,让内核空间可写
static unsigned long clear_cr0(void) 
{
    unsigned long cr0=0;
    unsigned long ret;
    asm volatile("movq %%cr0,%%rax":"=a"(cr0));//将cr0寄存器的值移动到eax寄存器中,同时输出到cr0变量中
    ret=cr0;
    cr0&=0xfffffffffffeffff;//将cr0变量值中的第17位清0,将修改后的值写入cr0寄存器
    asm volatile("movq %%rax,%%cr0"::"a"(cr0));//将cr0变量的值作为输入,输入到寄存器eax中,同时移动到寄存器cr0中
    return ret;
}

//恢复cr0寄存器的值,设置为内核不可写
static void setback_cr0(unsigned long val)
{
    asm volatile("movq %%rax,%%cr0"::"a"(val));
}

//定义自己的系统调用
asmlinkage int sys_mycall(void) 
{   
    printk("模块系统调用-当前pid:%d,当前comm:%s\n",current->pid,current->comm);
    printk("hello,world!\n");
    return current->pid;    
}

//添加模块时执行的代码
static int __init call_init(void)
{
    sys_call_table_my=(unsigned long*)(SYS_CALL_TABLE_ADDRESS);
    printk("call_init......\n");
    anything_saved=(unsigned long(*)(void))(sys_call_table_my[NUM]);//保存系统调用表中的NUM位置上的系统调用
    orig_cr0=clear_cr0();//使内核地址空间可写
    sys_call_table_my[NUM]=(unsigned long) &sys_mycall;//用自己的系统调用替换NUM位置上的系统调用
    setback_cr0(orig_cr0);//使内核地址空间不可写
    return 0;
}

//卸载模块时执行的代码
static void __exit call_exit(void)
{
    printk("call_exit......\n");
    orig_cr0=clear_cr0();
    sys_call_table_my[NUM]=(unsigned long)anything_saved;//将系统调用恢复
    setback_cr0(orig_cr0);
}
 
module_init(call_init);
module_exit(call_exit);      

代码相关解释

1、module_init在insmod时调用,module_exit在rmmod时调用

2、C语言中内嵌汇编语法格式

asm (assembler template

:output operands /* optional */

:input operands /* optional */

:list of clobbered registers /* optional */

);

3、movl 操作32位数据,movq 操作64位数据

4、CR寄存器

CR0~CR3是控制寄存器,用于控制和确定处理器的操作模式以及当前执行任务的特性。

CR0中含有控制处理器操作模式和状态的系统控制标志;

CR1保留不用;

CR2含有导致页错误的线性地址;

CR3中含有页目录表物理内存基地址,因此该寄存器也被称为页目录基地址寄存器PDBR(Page-Directory Base addressRegister)。

5、系统调用号的选择

由于需要查看添加的系统调用号是多少,网上说/usr/include/asm/unistd_32.h文件中有,

但是比较高的版本内核里的这个文件中的内容和网上说的不太一样,所以这里使用上一次编译好的4.13.10版本内核。

添加的系统调用号为334,即原本系统调用号到333,这里使用334新添加一个系统调用号。

在​​

​linux-source-4.13.0/arch/x86/entry/syscalls​

​​下的​

​syscall_64.tbl​

​​文件中查看。

当然也可以选择原有的内核版本,只不过没有源码看不到系统调用号信息,但是可以猜一个。

编写Makefile文件

CONFIG_MODULE_SIG=n
obj-m:=hello.o
CURRENT_PATH:=$(shell pwd)
LINUX_KERNEL_PATH:=/lib/modules/$(shell uname -r)/build
all:
  make -C  $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
  make -C  $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean      

编译并加载模块

sudo make
sudo insmod hello.ko
lsmod | grep hello  查看添加模块的信息
dmesg  获取内核信息      

测试系统调用,编写lab.c

#include<stdio.h>
#include<stdlib.h>
#include<linux/kernel.h>
#include<sys/syscall.h>
#include<unistd.h>
 
int main(){
    int x = 0;
    x = syscall(334);        //测试334号系统调用
    printf("res = %d\n", x);
    return 0;
}      

编译运行,查看dmesg的信息

gcc lab.c -o lab
./lab
dmesg      

继续阅读