什么是 LKMs
LKMs 称为可加载核心模块(内核模块),其可看作是运行在内核空间的可执行程序,类似于Linux 下的ELF ,包括:
LKMs 的文件格式和用户态的可执行程序相同,Linux 下为ELF ,Windows 下为exe/dll ,mac 下为MACH-O ,因此我们可以使用IDA 等工具来分析内核模块
模块可以被单独编译,但不能单独运行,它在运行时被链接到内核作为内核的一部分在内核空间运行,这与运行在用户空间的进程不同
模块通常用来实现一种文件系统,一个驱动程序或者其它内核上层的功能。
Linux 内核之所以提供模块机制,是因为它本身是一个单内核 (monolithic kernel)。单内核的优点是效率高,因为所有的内容都集合在一起,但缺点是可扩展性和可维护性相对较差,模块机制就是为了弥补这一缺陷。
通常情况下Kernel 漏洞的发生也常见于加载的LKMs 出现问题。
相关指令
insmod :将制定模块加载到内核中rmmod :从内核中卸载制定模块lsmod :列出已经加载的模块modprobe :添加或删除模块,modprobe 在加载模块时会查找依赖关系
文件系统
在Linux系统的视角下,无论是文件、设备、管道、还是目录,进程,甚至是磁盘,套接字等等,一切都可以被抽象成文件,一切都可以使用访问文件的方式进行操作。 图中所示为Linux中虚拟文件系统(VFS)、磁盘/Flash文件系统及一般的设备文件与设备驱动程序之间的关系。 应用程序和 VFS 之间的接口是系统调用,而 VFS 与文件系统以及设备文件之间的接口是 file_operations 结构体成员函数,这个结构体包含对文件进行打开、关闭、读写、控制的一系列成员函数。
file 结构体
file 结构体代表一个打开的文件,系统中每个打开的文件在内核空间都有一个关联的 struct file 。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。在内核和驱动源代码中,struct file 的指针通常被命名为 file 或 filp 。 linux-5.17/include/linux/fs.h: file
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode;
const struct file_operations *f_op;
spinlock_t f_lock;
enum rw_hint f_write_hint;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
void *private_data;
#ifdef CONFIG_EPOLL
struct hlist_head *f_ep;
#endif
struct address_space *f_mapping;
errseq_t f_wb_err;
errseq_t f_sb_err;
} __randomize_layout
__attribute__((aligned(4)));
inode结构体
VFS inode包含文件访问权限、所有者、组、大小、生成时间、访问时间、最后修改时间等信息。它是Linux管理文件系统的最基本单位,也是文件系统连接任何子目录、文件的桥梁。 include/linux/fs.h: inode
struct inode {
umode_t i_mode;
unsigned short i_opflags;
kuid_t i_uid;
kgid_t i_gid;
unsigned int i_flags;
...
dev_t i_rdev;
loff_t i_size;
struct timespec i_atime;
struct timespec i_mtime;
struct timespec i_ctime;
spinlock_t i_lock;
unsigned short i_bytes;
unsigned int i_blkbits;
blkcnt_t i_blocks;
...
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
};
...
查看 /proc/devices 文件可以获知系统中注册的设备,第一列为主设备号,第二列为设备名:
$ cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
...
Block devices:
259 blkext
7 loop
8 sd
9 md
11 sr
65 sd
...
查看 /dev 目录可以获知系统中包含的设备文件,日期前的两列对应设备的主设备号和次设备号:
$ ls -al /dev
total 0
drwxr-xr-x 8 root root 2940 May 8 14:17 .
drwxr-xr-x 11 root root 0 May 8 14:18 ..
drwxr-xr-x 2 root root 60 May 8 14:17 bsg
crw-rw---- 1 root root 5, 1 May 8 14:17 console
主设备号是与驱动对应的概念,同一类设备一般用相同的主设备号,不同类设备的主设备号一般不同。
内核模块开发
这篇文章介绍了一种方法,可以使 IDE 支持自动补全,并且可以方便查看源码。
Hello World模块
编写一个输出内容的内核模块。
首先在内核源码目录下创建一个用于编译内核模块的文件夹,这里我创建的文件夹的名称是 myko 。 在该目录下创建 myko.c ,内容如下:
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
static int hello_init(void) {
printk("Hello, world!\n");
return 0;
}
static void hello_exit(void) {
printk("Goodbye, cruel world!\n");
}
module_init(hello_init);
module_exit(hello_exit);
-
linux/module.h 是Linux 内核模块变成必须包含的头文件 -
头文件kernel.h 包含了常用的内核函数 -
头文件init.h 包含了宏_init 和_exit ,它们允许释放内核占用的内存。 -
hello_init 函数是模块初始化函数,他会在内核模块被加载的时候执行,使用__init 进行修饰,一般用它来初始化数据结构等内容; -
hello_exit 函数是模块的退出函数,他会在模块在退出的时候执行。 -
函数module_init() 和clearnup_exit() 是模块编程中最基本也是必须得两个函数,它用来指定模块加载和退出时调用的函数,这里加载的是我们上面定义好的两个函数,module_init() 向内核注册模块提供新功能,而cleanup_exit() 注销由模块提供的所用功能。 -
这段代码中使用了printk 函数,这是内核打印函数,可以使用dmesg 指令来看到内核打印信息。
创建 Makefile ,内容如下:
obj-m := myko.o
KERNELDR := ~/Desktop/linux-5.17/
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDR) M=$(PWD) modules
moduels_install:
$(MAKE) -C $(KERNELDR) M=$(PWD) modules_install
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
编译 生成 myko.ko 驱动 参考环境搭建,将其打包到文件系统中,然后启动系统。
可以看到,模块运行正常。
带参数的模块
myko.c 内容修改为:
#include<linux/init.h>
#include<linux/module.h>
#include<linux/moduleparam.h>
MODULE_LICENSE("Dual BSD/GPL");
static char *whom = "world";
static int howmany = 1;
module_param(howmany, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);
static int hello_init(void) {
int i;
for (i = 0; i < howmany; i++)
printk("(%d) Hello, %s\n", i, whom);
return 0;
}
static void hello_exit(void) {
printk("Goodbye, cruel world\n");
}
module_init(hello_init);
module_exit(hello_exit);
参数必须使用module_param 宏来声明,这个宏在moduleparam.h 中定义。module_param 需要三个参数:变量的名称、类型以及用于sysfs 入口项的访问许可掩码,这个宏必须放在任何函数之外,通常在源文件头部。
字符设备驱动
字符设备驱动结构
cdev 结构体
cdev 为 linux 描述字符设备的一个结构。 include/linux/cdev.h
struct cdev {
struct kobject kobj;
struct module *owner;
struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
}
dev_t 定义了设备号,为 32 位,其中 12 位为主设备号,20 位为次设备号。下面的宏可以获得主设备号和次设备号:
MAJOR(dev_t dev)
MINOR(dev_t dev)
使用下面的宏可以用主设备号和次设备号生成 dev_t :
MKDEV(int major, int minor)
Linux 内核提供了一组函数用于操作 cdev 结构体:
void cdev_init(struct cdev *, struct file_operations *);
struct cdev *cdev_alloc(void);
void cdev_put(struct cdev *p);
int cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev *);
在调用 cdev_add() 函数向系统注册字符设备之前,应首先调用 register_chrdev_region() 或 alloc_chrdev_region() 函数向系统申请设备号:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
register_chrdev_region() 函数用于已知起始设备的设备号的情况,而 alloc_chrdev_region() 用于设备号未知,向系统动态申请未被占用的设备号的情况。
file_operations 结构体
file_operations 结构体中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在应用程序进行 Linux 的 open() 、write() 、read() 、close() 等系统调用时最终被内核调用。 include/linux/fs.h: file_operations
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
int (*show_fdinfo)(struct seq_file *m, struct file *f);
};
下面对 file_operations 结构体中的主要成员简要介绍: llseek() 函数用来修改一个文件的当前读写位置,并将新位置返回,在出错时,这个函数返回一个负值。 read() 函数用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值。 write() 函数向设备发送数据,成功时该函数返回写入的字节数。如果次函数未被实现,当用户进行 write() 系统调用时,将得到 -EINVAL 返回值。 unlocked_ioctl() 提供设备相关控制命令的实现,当调用成功时,返回给调用程序一个非负值。
字符设备驱动组成
这里以一个简单的内存读写驱动为例。
头文件、宏及设备结构体
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
MODULE_LICENSE("Dual BSD/GPL");
#define MAX_SIZE 0x1000
#define MEM_CLEAR 0x1
static int hello_major = 230;
static int hello_minor = 0;
module_param(hello_major, int, S_IRUGO);
module_param(hello_minor, int, S_IRUGO);
struct hello_dev {
struct cdev cdev;
unsigned char mem[MAX_SIZE];
} * hello_devp;
加载与卸载设备驱动
static int __init hello_init(void) {
int ret;
dev_t devno = MKDEV(hello_major, hello_minor);
if (hello_major)
ret = register_chrdev_region(devno, 1, "myko");
else {
ret = alloc_chrdev_region(&devno, 0, 1, "myko");
hello_major = MAJOR(devno);
}
if (ret < 0) return ret;
hello_devp = kzalloc(sizeof(struct hello_dev), GFP_KERNEL);
if (!hello_devp) {
unregister_chrdev_region(devno, 1);
return -ENOMEM;
}
cdev_init(&hello_devp->cdev, &hello_fops);
hello_devp->cdev.owner = THIS_MODULE;
int err = (int) cdev_add(&hello_devp->cdev, devno, 1);
if (err) printk("[-] Error %d adding myko %d\n", err, hello_minor);
return 0;
}
module_init(hello_init);
static void __exit hello_exit(void) {
cdev_del(&hello_devp->cdev);
kfree(hello_devp);
unregister_chrdev_region(MKDEV(hello_major, hello_minor), 1);
}
module_exit(hello_exit);
cdev_init 初始化 cdev 结构体,其中与驱动的 cdev 关联的 file_operations 结构体如下:
static const struct file_operations hello_fops = {
.owner = THIS_MODULE,
.llseek = hello_llseek,
.read = hello_read,
.write = hello_write,
.unlocked_ioctl = hello_ioctl,
.open = hello_open,
.release = hello_releace,
};
使用文件私有数据
大多数Linux驱动遵循一个”潜规则”,那就是将文件的私有数据 private_data 指向设备结构体,再用 read() 、write() 、ioctl() 、llseek() 等函数通过 private_data 访问设备结构体。
static int hello_open(struct inode *id, struct file *filp) {
filp->private_data = hello_devp;
return 0;
}
static int hello_releace(struct inode *id, struct file *filp) {
filp->private_data = NULL;
return 0;
}
读写函数
static ssize_t hello_read(struct file *filp, char __user *buf, size_t size, loff_t *pos) {
if (*pos >= MAX_SIZE) return 0;
unsigned int count = (unsigned int) size;
struct hello_dev *dev = filp->private_data;
if (count > MAX_SIZE - *pos) count = MAX_SIZE - *pos;
if (copy_to_user(buf, dev->mem + *pos, count))
return -EFAULT;
*pos += count;
printk("[+] Read %u bytes(s) from %llu\n", count, *pos);
return count;
}
static ssize_t hello_write(struct file *filp, const char __user *buf, size_t size, loff_t *pos) {
if (*pos >= MAX_SIZE) return 0;
unsigned int count = (unsigned int) size;
struct hello_dev *dev = filp->private_data;
if (count > MAX_SIZE - *pos) count = MAX_SIZE - *pos;
if (copy_from_user(dev->mem + *pos, buf, count))
return -EFAULT;
*pos += count;
printk("[+] Written %u bytes(s) from %llu\n", count, *pos);
return count;
}
由于用户空间不能直接访问内核空间的内存,因此借助了函数 copy_from_user() 完成用户空间缓冲区到内核空间的复制,copy_to_user() 完成内核空间到用户空间缓冲区的复制。它们的原型如下:
unsigned long copy_from_user(void *to, const void __user *from, unsigned long count);
unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);
完全复制成功返回值为 0 ,如果复制失败,则返回负值。 读和写函数中的 __user 是一个宏,表明其后的指针指向用户空间,实际上更多地充当了代码自注释功能。
seek函数
seek() 函数对文件定位的起始地址可以是文件开头(SEEK_SET,0)、当前位置(SEEK_CUR,1)和文件尾(SEEK_END,2),这里只实现了支持从文件开头和当前位置的相对偏移。
static loff_t hello_llseek(struct file *filp, loff_t offset, int op) {
if (op != 0 && op != 1) return -EINVAL;
if (op == 1) offset += filp->f_pos;
if (offset < 0 || offset > MAX_SIZE) return -EINVAL;
return filp->f_pos = offset;
}
ioctl函数
用来自定义的函数,这里自定义了清内存的函数。
static long hello_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
struct hello_dev *dev = filp->private_data;
if (dev == NULL) {
printk("[-] No device\n");
return -EINVAL;
}
switch (cmd) {
case MEM_CLEAR:
memset(dev->mem, 0, sizeof(dev->mem));
printk("[+] Clear success\n");
break;
default:
printk("[-] Error command\n");
return -EINVAL;
}
return 0;
}
完整代码
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
MODULE_LICENSE("Dual BSD/GPL");
#define MAX_SIZE 0x1000
#define MEM_CLEAR 0x1
static int hello_major = 230;
static int hello_minor = 0;
module_param(hello_major, int, S_IRUGO);
module_param(hello_minor, int, S_IRUGO);
struct hello_dev {
struct cdev cdev;
unsigned char mem[MAX_SIZE];
} * hello_devp;
static int hello_open(struct inode *id, struct file *filp);
static int hello_releace(struct inode *id, struct file *filp);
static long hello_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);
static ssize_t hello_read(struct file *filp, char __user *buf, size_t size, loff_t *pos);
static ssize_t hello_write(struct file *filp, const char __user *buf, size_t size, loff_t *pos);
static loff_t hello_llseek(struct file *filp, loff_t offset, int op);
static const struct file_operations hello_fops = {
.owner = THIS_MODULE,
.llseek = hello_llseek,
.read = hello_read,
.write = hello_write,
.unlocked_ioctl = hello_ioctl,
.open = hello_open,
.release = hello_releace,
};
static int __init hello_init(void) {
int ret;
dev_t devno = MKDEV(hello_major, hello_minor);
if (hello_major)
ret = register_chrdev_region(devno, 1, "myko");
else {
ret = alloc_chrdev_region(&devno, 0, 1, "myko");
hello_major = MAJOR(devno);
}
if (ret < 0) return ret;
hello_devp = kzalloc(sizeof(struct hello_dev), GFP_KERNEL);
if (!hello_devp) {
unregister_chrdev_region(devno, 1);
return -ENOMEM;
}
cdev_init(&hello_devp->cdev, &hello_fops);
hello_devp->cdev.owner = THIS_MODULE;
int err = (int) cdev_add(&hello_devp->cdev, devno, 1);
if (err) printk("[-] Error %d adding myko %d\n", err, hello_minor);
return 0;
}
module_init(hello_init);
static void __exit hello_exit(void) {
cdev_del(&hello_devp->cdev);
kfree(hello_devp);
unregister_chrdev_region(MKDEV(hello_major, hello_minor), 1);
}
module_exit(hello_exit);
static int hello_open(struct inode *id, struct file *filp) {
filp->private_data = hello_devp;
return 0;
}
static int hello_releace(struct inode *id, struct file *filp) {
filp->private_data = NULL;
return 0;
}
static long hello_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
struct hello_dev *dev = filp->private_data;
if (dev == NULL) {
printk("[-] No device\n");
return -EINVAL;
}
switch (cmd) {
case MEM_CLEAR:
memset(dev->mem, 0, sizeof(dev->mem));
printk("[+] Clear success\n");
break;
default:
printk("[-] Error command\n");
return -EINVAL;
}
return 0;
}
static ssize_t hello_read(struct file *filp, char __user *buf, size_t size, loff_t *pos) {
if (*pos >= MAX_SIZE) return 0;
unsigned int count = (unsigned int) size;
struct hello_dev *dev = filp->private_data;
if (count > MAX_SIZE - *pos) count = MAX_SIZE - *pos;
if (copy_to_user(buf, dev->mem + *pos, count))
return -EFAULT;
*pos += count;
printk("[+] Read %u bytes(s) from %llu\n", count, *pos);
return count;
}
static ssize_t hello_write(struct file *filp, const char __user *buf, size_t size, loff_t *pos) {
if (*pos >= MAX_SIZE) return 0;
unsigned int count = (unsigned int) size;
struct hello_dev *dev = filp->private_data;
if (count > MAX_SIZE - *pos) count = MAX_SIZE - *pos;
if (copy_from_user(dev->mem + *pos, buf, count))
return -EFAULT;
*pos += count;
printk("[+] Written %u bytes(s) from %llu\n", count, *pos);
return count;
}
static loff_t hello_llseek(struct file *filp, loff_t offset, int op) {
if (op != 0 && op != 1) return -EINVAL;
if (op == 1) offset += filp->f_pos;
if (offset < 0 || offset > MAX_SIZE) return -EINVAL;
return filp->f_pos = offset;
}
验证
/ # insmod myko.ko
[ 17.837662] myko: loading out-of-tree module taints kernel.
[ 17.840020] myko: module verification failed: signature and/or required key l
/ # [ 17.887877] random: fast init done
/ # lsmod
myko 16384 0 - Live 0xffffffffc002e000 (OE)
/ # mknod /dev/myko c 230 0 #创建设备节点,c表明是字符设备,230是主设备号,0是次设备号
/ # chmod 777 /dev/myko
/ # echo "hello,world!" > /dev/myko
[ 103.447740] [+] Written 13 bytes(s) from 13
/ # cat dev/myko
[ 111.699740] [+] Read 4096 bytes(s) from 4096
hello,world!
|