本文为毕业设计过程中学习相关知识、动手实践记录下来的完整笔记,通过阅读本系列文章,您可以从零基础了解系统调用的底层原理并对系统调用进行拦截。由于本人能力有限,文章中可能会出现部分错误信息,如有错误欢迎指正。
完整系列文章列表
系统调用捕获和分析—通过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
查看编译生成的模块信息
modinfo hello.ko
装载,查看装载结果
insmod hello.ko
lsmod | grep hello
查看系统开机和内核模块装载信息,发现报错
dmesg
报错解决方案
在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