0x00. Linux kernel基础:编译内核与驱动编写

不会写驱动就会导致不会写驱动的问题

[toc]

前言

大量参考了arttnba3师傅的博客内容。

0x01 准备工作

首先安装必要库:

1
sudo apt-get install git fakeroot build-essential ncurses-dev xz-utils qemu flex libncurses5-dev libssl-dev bc bison libglib2.0-dev libfdt-dev libpixman-1-dev zlib1g-dev libelf-dev

随后,我们在这里下载Linux内核源码。

或者采用如下方式:

1
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.11.tar.xz

将其解压:

1
tar -xzvf ./linux-5.11.tar.gz

或者是:

1
tar -xvf ./linux-5.11.tar.xz

0x02 内核编译

切换到刚刚的内核源码目录,例如:

1
cd linux-5.11

输入以下命令来配置编译选项:

1
make menuconfig

保证勾选如下配置:

1
2
3
4
Kernel hacking —> Kernel debugging
Kernel hacking —> Compile-time checks and compiler options —> Compile the kernel with debug info
Kernel hacking —> Generic Kernel Debugging Instruments –> KGDB: kernel debugger
kernel hacking —> Compile the kernel with frame pointers

完成后,选择save,保存为默认文件名.config即可。

输入以下命令编译内核,生成名为bzImage的内核镜像:

(该操作比较费时,耐心等待)

1
make -j$(nproc) bzImage

成功后得到如下输出:

1
Kernel: arch/x86/boot/bzImage is ready  (#1)

而此时我们也会同时得到两个文件:

  • 第一个是在当前目录下生成的名为vmlinux的文件,为原始内核文件,可以通过其获取gadgets等信息。
  • 第二个是位于当前目录下的arch/x86/boot/目录下的bzImage文件,为压缩后的内核文件。可以使用extract-vmlinux脚本解压得到vmlinux

我们要启动内核时,选用bzImage文件;我们做Linux Kernel Pwn题目时,选用vmlinux提取gadgets

记录下bzImage的位置,待会我们要用。

0x03 使用busybox构建文件系统

我们刚刚已经成功编译了内核,但若我们没有文件系统,自然是难以启动该内核。因此,我们可以借助busybox来构建一个文件系统。

busybox集成了一些最常用的Linux命令和工具,包含ls、cat、echo等简单的用户常用命令,可以让我们借助其构建一个基本的用户环境。

编译busybox

可以在这里获取busybox的源码。

选好想要的版本之后,我们通过如下命令来下载(此处我另起了一个文件夹,没有选择在刚刚编译好的kernel目录):

1
wget https://busybox.net/downloads/busybox-1.33.0.tar.bz2

下载后得到一个后缀为.tar.bz2的文件,我们通过如下命令解压:

1
tar -jxvf busybox-1.33.0.tar.bz2

使用如下命令进入配置界面:

1
make menuconfig

勾选如下配置:

1
2
Settings -> Build static binary file (no shared lib)
# 这是为了不需要往busybox单独配置libc

随后在配置主页面连续按下两次ESC,选择保存配置并退出。

输入以下命令来编译busybox

1
make install

建立文件系统

初始化文件系统

输入以下命令来初始化文件系统(是的,就是我们平时看到的内核的样子)

1
2
3
4
5
6
cd _install
mkdir -pv {bin,sbin,etc,proc,sys,home,lib64,lib/x86_64-linux-gnu,usr/{bin,sbin}}
touch etc/inittab
mkdir etc/init.d
touch etc/init.d/rcS
chmod +x ./etc/init.d/rcS

配置初始化脚本 - rcS

配置/etc/inittab。写入如下内容:

1
2
3
4
5
6
::sysinit:/etc/init.d/rcS
::askfirst:/bin/ash
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r
::restart:/sbin/init

配置rcS文件来挂载文件系统,位于/etc/init.d/rcS

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs tmpfs /tmp
mkdir /dev/pts
mount -t devpts devpts /dev/pts

echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh

poweroff -d 0 -f

配置初始化脚本 - init

和上一步二选一即可。

配置/etc/inittab,写入如下内容:

1
2
3
4
5
6
::sysinit:/init
::askfirst:/bin/ash
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r
::restart:/sbin/init

在根目录下创建名为init的文件,等效于rcS文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev

exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
poweroff -d 0 -f

添加可执行权限:

1
chmod +x ./init

配置用户组 - 前言

配置用户组 - 前言这一小节是作为用户组的知识补充,不是内核编译的操作。

若您只需要编译出内核,请跳过这一小节

etc目录下含有/etc/passwd/etc/group两个文件,都是用于Linux的用户组管理的。

其中/etc/group包含系统上用户组的信息,而/etc/passwd包含具体某个用户的信息。

具体来说,/etc/group中,每一行表示一个组,每个组的条目由四个字段组成,以冒号分隔,包括组名、组密码、组ID、组成员。

以以下信息为例:

1
2
root:x:0:
chal:x:1000:

其中包含两个组,分别为组名为root的组和组名为chal的组。其中:

  • 两个组的第二个字段均为空,表示密码信息实际上已经不再使用(现在通常由/etc/shadow管理)。

  • 第三个字段01000表示组的ID,其中root组的ID0chal组的ID1000

  • 第四个字段为空,表示没有写明组成员具体有哪些。

而对于/etc/passwd文件,每一行表示一个用户账户,由七个字段组成,同样由冒号分隔,包括用户名、密码、用户ID、组ID、用户信息、家目录、登录shell

以以下信息为例:

1
2
root:x:0:0:root:/root:/bin/sh
chal:x:1000:1000:chal:/home/chal:/bin/sh

其中包含两个用户,root用户和chal组。其中:

  • 第二个字段为密码,不再使用而交由/etc/shadow管理。
  • 第三个字段表示用户的ID,其中root用户的ID0chal用户的ID1000
  • 第四个字段表示组ID,表示用户所属组的ID
  • 第五个字段表示用户信息,通常含有用户全名或其他描述性信息。
  • 第六个字段表示用户的家目录,表示用户的主目录,用户登录后会进入这个目录。
  • 第七个字段表示登录shell,是用户登录后默认启动的shell

了解到上述信息后,我们可以修改rcS文件来修改qemu虚拟机启动后的用户。

其中,rcS文件是一个启动脚本,用于在系统引导过程中启动一些基本的系统服务和设置环境。在部分文件系统中,根目录下有一个名为init文件即为rcS文件。有时候也会位于/etc中。

init文件中有一行命令如下:

1
setsid /bin/cttyhack setuidgid 1000 /bin/sh

其中setsid命令可以启动一个新的会话,并连续执行了/bin/cttyhacksetuidgid 1000 /bin/sh。其中,以setuidgid命令来以用户组1000启动了一个shell,而1000表示用户组chal。因此,我们将其修改为0,即可让其启动一个拥有root权限的shell来进行调试。

配置用户组 - 操作

输入以下命令来配置用户组:

1
2
3
4
5
echo "root:x:0:0:root:/root:/bin/sh" > etc/passwd
echo "ctf:x:1000:1000:ctf:/home/ctf:/bin/sh" >> etc/passwd
echo "root:x:0:" > etc/group
echo "ctf:x:1000:" >> etc/group
echo "none /dev/pts devpts gid=5,mode=620 0 0" > etc/fstab

如此我们可以创建两个用户组rootctf,以及属于他们的两个用户rootctf

打包文件系统为镜像文件

若您已经知晓一些Linux Kernel Pwn的相关题目,则对这里的内容并不陌生。

打包为cpio文件

使用如下命令打包刚刚编写的文件系统:

1
find . | cpio -o --format=newc > ../../rootfs.cpio

也可以这样写:

1
find . | cpio -o -H newc > ../../core.cpio

位置是随便选的,待会我们拿到它就可以。

对cpio重新打包

我们有时候打包好后会修改里面的文件,例如放置我们的exploit文件。使用如下方式解压:

1
cpio -idv < ./rootfs.cpio

并仍然使用如下命令打包即可:

1
find . | cpio -o --format=newc > ../new_rootfs.cpio

打包为ext4镜像

替代操作,虽然常用cpio,但是ext4也可以学一下。

首先创建空白的ext4镜像文件,其中bs表示块大小,count表示块的数量:

1
dd if=/dev/zero of=rootfs.img bs=1M count=32

格式化,转换为ext4格式:

1
mkfs.ext4 rootfs.img 

挂载镜像,将文件拷贝进去即可:

1
2
3
4
mkdir tmp
sudo mount rootfs.img ./tmp/
sudo cp -rfp _install/* ./tmp/
sudo umount ./tmp

对ext4镜像重新打包

1
2
3
sudo mount rootfs.img ./tmp/
# do something
sudo umount ./tmp

0x04 使用qemu运行内核

现在,我们已经构建好了运行一个内核的所有的必要/基本组件:

  • 文件系统镜像*.cpio
  • 内核镜像bzImage

编写启动脚本

首先,我们将刚刚创建的文件系统rootfs.cpio和编译好的内核文件bzImage放到同一个目录下。

编写启动脚本boot.sh

1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./rootfs.cpio \
-monitor /dev/null \
-append "root=/dev/ram rdinit=/sbin/init console=ttyS0 oops=panic panic=1 loglevel=3 quiet kaslr" \
-cpu kvm64,+smep \
-smp cores=2,threads=1 \
-nographic \
-s

参数说明如下:

  • -m:虚拟机内存大小
  • -kernel:内存镜像路径
  • -initrd:磁盘镜像路径
  • -append:附加参数选项
    • nokalsr:关闭内核地址随机化,方便我们进行调试
    • rdinit:指定初始启动进程,/sbin/init进程会默认以 /etc/init.d/rcS 作为启动脚本
    • loglevel=3 & quiet:不输出log
    • console=ttyS0:指定终端为/dev/ttyS0,这样一启动就能进入终端界面
  • -monitor:将监视器重定向到主机设备/dev/null,这里重定向至null主要是防止CTF中被人给偷了qemu拿flag
  • -cpu:设置CPU安全选项,在这里开启了smep保护
  • -s:相当于-gdb tcp::1234的简写(也可以直接这么写),后续我们可以通过gdb连接本地端口进行调试

启动内核

启动内核!

image-20240424164001278

0x05 编写内核驱动(可装载内核模块LKM)

准备工作

我们这里以刚刚下载的Linux 5.11.0的源码为例子来讲解如何编写内核驱动。

首先切换到Linux 5.11.0源码目录:

1
cd linux-5.11

执行如下命令,来准备好编译内核模块所需要的文件:

1
make modules_prepare

编写内核驱动代码

准备一个简单的内核驱动代码,该代码在载入/卸载的时候会通过printk在内核缓冲区输出内容。

放在任意地方都可以,因为我们随后会指定内核源码路径:)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* hello.c
* developed by ltfall
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init kernel_module_init(void)
{
printk("<1>Hello the Linux kernel world!\n");
return 0;
}

static void __exit kernel_module_exit(void)
{
printk("<1>Good bye the Linux kernel world! See you again!\n");
}

module_init(kernel_module_init);
module_exit(kernel_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ltfall");

头文件

  • linux/module.h:对于LKM而言这是必须包含的一个头文件
  • linux/kernel.h:载入内核相关信息
  • linux/init.h:包含着一些有用的宏

通常情况下,这三个头文件对于内核模块编程都是不可或缺的

入口点/出口点

一个内核模块的入口点应当为 module_init(),出口函数应当为module_exit(),在内核载入/卸载内核模块时会缺省调用这两个函数

在这里我们将自定义的两个函数的指针作为参数传入LKM入口函数/出口函数中,以作为其入口/出口函数

其他…

  • __init & __exit:这两个宏用以在函数结束后释放相应的内存
  • MODULE_AUTHOR() & MODULE_LICENSE():声明内核作者与发行所用许可证
  • printk():内核态函数,用以在内核缓冲区写入信息,其中<1>标识着信息的紧急级别(一共有8个优先级,0为最高,相关宏定义于linux/kernel.h中)

编写makefile

如下是一个makefile样例:

1
2
3
4
5
6
7
8
9
10
11
obj-m += hello.o

CURRENT_PATH := $(shell pwd)
LINUX_KERNEL := 5.11.0
LINUX_KERNEL_PATH := /kernel/kernel_source/linux-5.11 # 指定的源码路径

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

clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean

以下是各个参数的解释:

  • obj-m:指定了编译的结果应当为.ko文件,即可装载内核模块,类似命令有: obj-y 编译进内核 ,obj-n 不编译
  • CURRENT_PATH:表示通过shell命令来获取当前路径
  • LINUX_KERNEL:指示内核版本
  • LINUX_KERNEL_PATH:指示内核源码路径

由于我们此处是针对我们刚刚编译的Linux-5.11.0内核,因此我直接指定了内核的版本和源码,arttnba3师傅的makefile如下所示:

1
2
3
4
5
6
7
8
obj-m += hellokernel.o
CURRENT_PATH := $(shell pwd)
LINUX_KERNEL := $(shell uname -r)
LINUX_KERNEL_PATH := /usr/src/linux-headers-$(LINUX_KERNEL)
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean

或者添加如下行,添加调试信息、优化等:

1
2
# 添加调试信息
EXTRA_CFLAGS += -g -O0

完成后,输入make,可得到如下输出,表示编译成功,可以在当前目录下获得hello.ko文件:

1
2
3
4
5
6
7
8
# ltfall @ DESKTOP-3540H1R in /kernel/drivers/hello [16:45:42] C:2
$ make
make -C /kernel/kernel_source/linux-5.11 M=/home/ltfall/pwn/kernel/drivers/hello modules
make[1]: Entering directory '/home/ltfall/pwn/kernel/kernel_source/linux-5.11'
WARNING: Symbol version dump "Module.symvers" is missing.
Modules may not have dependencies or modversions.
LD [M] /home/ltfall/pwn/kernel/drivers/hello/hello.ko
make[1]: Leaving directory '/home/ltfall/pwn/kernel/kernel_source/linux-5.11'

在rcS启动脚本中注册驱动

切换回我们编写的kernel启动脚本的目录,如下所示:

1
2
$ ls
boot.sh bzImage core.cpio

我们解压core.cpio,放置我们编写的hello.ko驱动,并修改其中的rcS启动文件,然后重新打包。

解压core.cpio

1
2
3
4
5
mkdir core
cp core.cpio core/
cp hello.ko core/
cd core
core -idmv < ./core.cpio

修改位于文件系统中的/etc/init.d/rcS启动文件,通过insmod来注册该驱动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs tmpfs /tmp
mkdir /dev/pts
mount -t devpts devpts /dev/pts

# register our ko
insmod /hello.ko

echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh

poweroff -d 0 -f

重新打包:

1
find . | cpio -o --format=newc > ../core.cpio

运行boot.sh,发现驱动成功注册:

1
2
3
4
5
/ $ lsmod
hello 16384 0 - Live 0x0000000000000000 (O)
/ $ dmesg | grep 'Hello'
[ 1.799002] <1>Hello the Linux kernel world!
/ $

0x06 设备注册

我们在上一步中,已经编写了一个简单的内核驱动,并将其在我们编译的内核中成功注册。

在本章节中,我们来学习如何注册一个设备到/dev目录下。下一章节我们学习如何与其进行IO交互!

前言

我们知道,Linux中的设备分为字符型设备和块设备,区别如下所示:

  • 字符设备:在I/O传输过程中以字符为单位进行传输的设备,例如键盘、串口等。字符设备按照字符流的方式被有序访问,不能够进行随机读取
  • 块设备:在块设备中,信息被存储在固定大小的块中,每个块有着自己的地址,例如硬盘、SD卡等。用户可以对块设备进行随机访问——从任意位置读取一定长度的数据

而在我们学习Linux kernel pwn时,一般会写一个字符型设备的驱动,并与其进行交互。

要成功挂载一个设备,需要经过如下步骤:

  • 注册字符型设备
  • 创建设备类
  • 创建设备节点并在/dev下生成设备文件
  • 更改设备权限使得普通用户也可以读写执行

注意在这几步操作中,若中途部分操作失败,那么我们需要手动来销毁前面的操作。例如我们在创建设备节点并在/dev下生成设备文件操作时失败,那么需要销毁注册字符型设备,以及销毁创建的设备类。

第一步:注册字符型设备

使用如下函数进行注册:

1
2
3
4
register_chrdev(unsigned int major, const char* name, const struct file_operations* fops);
// major: 代表主设备号,若填写为0则由内核指定
// name:注册的设备名称
// fops:字符型设备的file_operations

上面三个参数并不难理解,比较陌生的可能是其中的const struct file_operations* fops

这是因为每一个要注册的设备都需要一个自身的struct file_operations结构体来指示该设备的各种行为,例如openwriteioctl等操作。

第二步:创建设备类

使用如下函数创建设备类:

1
2
3
struct class *class_create(struct module *owner, const char *name);
// owner:设备拥有者,我们传THIS_MODULE即可
// name:创建的设备类名称

很好理解。只是注意,假如这一步出错了,需要销毁第一步注册的字符型设备,后面的操作也是类似的。

第三步:创建设备节点,并在/dev目录下生成设备节点文件

使用如下函数进行上述操作:

1
2
3
4
5
6
device_create(struct class *cls, struct device* parent, dev_t devt, void* drvdata, const char *fmt);
// cls: 设备的设备类
// parent: 父设备节点,为顶级设备时填写为NULL
// devt: 设备的设备号
// drvdata: 驱动相关信息,填写NULL即可
// fmt: 设备名称

第四步:更改设备权限为普通用户

我们生成的设备节点文件默认只有root权限可以与之进行交互和访问,因此我们需要修改其权限。

利用如下操作来更改设备权限:

  • filp_open():打开文件
  • file_inode()得到inode结构体
  • 通过__inode->i_mode |= 0666更改权限,其中0666为八进制表示rwx

示例代码

完成挂载设备到dev目录下的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
/*
* hello.c
* developed by ltfall
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "ltdevice"
#define DEVICE_PATH "/dev/ltdevice"
#define CLASS_NAME "ltmodule"

static int major_num;
static struct class *module_class = NULL;
static struct device *module_device = NULL;
static struct file *__file = NULL;
struct inode *__inode = NULL;


static struct file_operations lt_module_fo =
{
.owner = THIS_MODULE,
};

static int __init kernel_module_init(void)
{
printk(KERN_INFO "[ltfall] Module Loaded, Start to Register device...\n");

// 第一步:注册字符型设备
// register_chrdev(unsigned int major, const char* name, const struct file_operations* fops);
major_num = register_chrdev(0, DEVICE_NAME, &lt_module_fo);

if (major_num < 0)
{
printk(KERN_INFO "[ltfall] Failed to register a major number.\n");
return major_num;
}
printk(KERN_INFO "[ltfall] Register complete, major number : %d.\n", major_num);

// 第二步:创建设备类
// struct class *class_create(struct module *owner, const char *name);
module_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(module_class))
{
unregister_chrdev(major_num, DEVICE_NAME);
printk(KERN_INFO "[ltfall] Failed to register class device!\n");
return PTR_ERR(module_class);
}
printk(KERN_INFO "[ltfall] Class device register complete.\n");

// 第三步:创建设备节点并在/dev目录下生成设备节点文件
// device_create(struct class *cls, struct device* parent, dev_t devt, void* drvdata, const char *fmt);
// cls: 设备的设备类
// parent: 父设备节点,为顶级设备时填写为NULL
// devt: 设备的设备号
// drvdata: 驱动相关信息
// fmt: 设备名称
module_device = device_create(module_class, NULL, MKDEV(major_num, 0), NULL, DEVICE_NAME);
if (IS_ERR(module_device))
{
class_destroy(module_class);
unregister_chrdev(major_num, DEVICE_NAME);
printk(KERN_INFO "[ltfall] Failed to create the device!\n");
return PTR_ERR(module_device);
}
printk(KERN_INFO "[ltfall] Module register complete.\n");

// 第四步:更改设备权限为普通用户
// flip_open(); 打开文件
// file_inode(); 得到inode结构体
// 修改__inode->i_mode |= 0666; 更改权限,八进制表示rwx
__file = filp_open(DEVICE_PATH, O_RDONLY, 0);
if (IS_ERR(__file))
{
device_destroy(module_class, MKDEV(major_num, 0));
class_destroy(module_class);
unregister_chrdev(major_num, DEVICE_NAME);
printk(KERN_INFO "[ltfall] Unable to change module privilege!\n");
return PTR_ERR(__file);
}
__inode = file_inode(__file);
__inode->i_mode |= 0666;
filp_close(__file, NULL);
printk(KERN_INFO "[ltfall] Module privilege change complete.\n");

return 0;
}

static void __exit kernel_module_exit(void)
{
printk(KERN_INFO "[ltfall] Start to clean up the module.\n");
device_destroy(module_class, MKDEV(major_num, 0));
class_destroy(module_class);
unregister_chrdev(major_num, DEVICE_NAME);
printk(KERN_INFO "[ltfall] Module clean up complete.\n");
}

module_init(kernel_module_init);
module_exit(kernel_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ltfall");

随后将其注册到我们的内核中,发现设备已经挂载成功:

image-20240426155908124

亦或者是不在驱动代码里面更改其权限操作,而是放在rcS里面编写。例如,上述驱动在rcS中注册如下:

1
2
insmod /hello.ko
chmod 666 /dev/ltdevice

0x07 设备驱动I/O操作编写

到这里,我们就可以编写该驱动的I/O操作了(再也不用担心kernel pwn题目都看不懂了

我们会自己实现对该驱动程序的I/O操作,例如read\write\ioctl\open\release等,并能够与之完成交互。

file_operations结构体定义

为什么我们在用户态下写一个open会调用我们写得open呢?这是笔者入门Linux kernel pwn时的一个重大疑问。实际上,这是因为我们注册设备时,会传入一个struct file_operation结构体,其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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 (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, bool spin);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*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 *);
unsigned long mmap_supported_flags;
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 (*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 **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;

可以看到,里面有open\read\write等函数的指针。因此,我们只需要按照该结构体中的函数原型来编写我们的函数,随后传入我们写的函数指针就可以了。

因此,我们的驱动程序可以用如下形式先传递函数指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static long ltfall_ioctl(struct file *__file, unsigned int cmd, unsigned long param);
static int ltfall_open(struct inode *, struct file *);
static int ltfall_release(struct inode *, struct file *);
static ssize_t ltfall_read(struct file *__file, char __user *user_buf, size_t size, loff_t* loff);
static ssize_t ltfall_write(struct file *__file, const char __user *user_buf, size_t size, loff_t* loff);

static struct file_operations lt_module_fo =
{
.owner = THIS_MODULE,
.unlocked_ioctl = ltfall_ioctl,
.open = ltfall_open,
.read = ltfall_read,
.write = ltfall_write,
.release = ltfall_release,
};

注意,函数的返回值类型、参数类型一定要和file_operations结构体里对应上,即使我们可以不使用里面的那些变量。

此外用户态下的close对应file_operations结构体中的release,需要注意

I/O编写

这部分反而没啥了,就和用户态下编写没什么差别。需要注意的是,为了支持多线程,我们可以利用spin_lockspin_unlock来加锁。

写好后的整个代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
/*
* hello.c
* developed by ltfall
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/slab.h>

#define DEVICE_NAME "ltdevice"
#define DEVICE_PATH "/dev/ltdevice"
#define CLASS_NAME "ltmodule"

static int major_num;
static struct class *module_class = NULL;
static struct device *module_device = NULL;
static struct file *__file = NULL;
struct inode *__inode = NULL;
static spinlock_t spin;
static void *buffer = NULL;

static long ltfall_ioctl(struct file *__file, unsigned int cmd, unsigned long param);
static int ltfall_open(struct inode *, struct file *);
static int ltfall_release(struct inode *, struct file *);
static ssize_t ltfall_read(struct file *__file, char __user *user_buf, size_t size, loff_t *loff);
static ssize_t ltfall_write(struct file *__file, const char __user *user_buf, size_t size, loff_t *loff);

static struct file_operations lt_module_fo =
{
.owner = THIS_MODULE,
.unlocked_ioctl = ltfall_ioctl,
.open = ltfall_open,
.read = ltfall_read,
.write = ltfall_write,
.release = ltfall_release,
};

static int __init kernel_module_init(void)
{
printk(KERN_INFO "[ltfall] Module Loaded, Start to Register device...\n");

// 第一步:注册字符型设备
// register_chrdev(unsigned int major, const char* name, const struct file_operations* fops);
major_num = register_chrdev(0, DEVICE_NAME, &lt_module_fo);

if (major_num < 0)
{
printk(KERN_INFO "[ltfall] Failed to register a major number.\n");
return major_num;
}
printk(KERN_INFO "[ltfall] Register complete, major number : %d.\n", major_num);

// 第二步:创建设备类
// struct class *class_create(struct module *owner, const char *name);
module_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(module_class))
{
unregister_chrdev(major_num, DEVICE_NAME);
printk(KERN_INFO "[ltfall] Failed to register class device!\n");
return PTR_ERR(module_class);
}
printk(KERN_INFO "[ltfall] Class device register complete.\n");

// 第三步:创建设备节点并在/dev目录下生成设备节点文件
// device_create(struct class *cls, struct device* parent, dev_t devt, void* drvdata, const char *fmt);
// cls: 设备的设备类
// parent: 父设备节点,为顶级设备时填写为NULL
// devt: 设备的设备号
// drvdata: 驱动相关信息
// fmt: 设备名称
module_device = device_create(module_class, NULL, MKDEV(major_num, 0), NULL, DEVICE_NAME);
if (IS_ERR(module_device))
{
class_destroy(module_class);
unregister_chrdev(major_num, DEVICE_NAME);
printk(KERN_INFO "[ltfall] Failed to create the device!\n");
return PTR_ERR(module_device);
}
printk(KERN_INFO "[ltfall] Module register complete.\n");

// 第四步:更改设备权限为普通用户
// flip_open(); 打开文件
// file_inode(); 得到inode结构体
// 修改__inode->i_mode |= 0666; 更改权限,八进制表示rwx
__file = filp_open(DEVICE_PATH, O_RDONLY, 0);
if (IS_ERR(__file))
{
device_destroy(module_class, MKDEV(major_num, 0));
class_destroy(module_class);
unregister_chrdev(major_num, DEVICE_NAME);
printk(KERN_INFO "[ltfall] Unable to change module privilege!\n");
return PTR_ERR(__file);
}
__inode = file_inode(__file);
__inode->i_mode |= 0666;
filp_close(__file, NULL);
printk(KERN_INFO "[ltfall] Module privilege change complete.\n");

return 0;
}

static void __exit kernel_module_exit(void)
{
printk(KERN_INFO "[ltfall] Start to clean up the module.\n");
device_destroy(module_class, MKDEV(major_num, 0));
class_destroy(module_class);
unregister_chrdev(major_num, DEVICE_NAME);
printk(KERN_INFO "[ltfall] Module clean up complete.\n");
}

static int ltfall_open(struct inode *node, struct file *__file)
{
spin_lock(&spin);
if (buffer == NULL)
{
buffer = kmalloc(0x500, GFP_ATOMIC);
if (buffer)
{
printk(KERN_INFO "[ltfall] Open Test: Success.\n");
}
else
{
printk(KERN_INFO "[ltfall] Open success But kmalloc failed.\n");
}
}
else
{
printk(KERN_INFO "[ltfall] Open Test: Trying to open the device twice!\n");
}

spin_unlock(&spin);
return 0;
}

static int ltfall_release(struct inode *node, struct file *__file)
{
spin_lock(&spin);
if (!buffer)
{
printk(KERN_INFO "[ltfall] The Buffer has not initialized yet, cannot Release!\n");
return -1;
}
kfree(buffer);
buffer = NULL;
printk(KERN_INFO "[ltfall] Release: free the buffer successfully.\n");
spin_unlock(&spin);

return 0;
}

static long ltfall_ioctl(struct file *__file, unsigned int cmd, unsigned long param)
{

spin_lock(&spin);
printk(KERN_INFO "[ltfall] ioctl test: cmd : %u, param: %lu.\n", cmd, param);
spin_unlock(&spin);
return 0;
}

static ssize_t ltfall_read(struct file *__file, char __user *user_buf, size_t size, loff_t *loff)
{
unsigned long user_size = (unsigned long)size > 0x500 ? 0x500 : (unsigned long)size;
spin_lock(&spin);
if (!buffer)
{
printk(KERN_INFO "[ltfall] Buffer Not initialized yet.\n");
return -1;
}
copy_to_user((char *)user_buf, (char *)buffer, user_size);
printk(KERN_INFO "[ltfall] Copy to user success.\n");
spin_unlock(&spin);
return 0;
}

static ssize_t ltfall_write(struct file *__file, const char __user *user_buf, size_t size, loff_t *loff)
{
unsigned long user_size = (unsigned long)size > 0x500 ? 0x500 : (unsigned long)size;
spin_lock(&spin);
if (!buffer)
{
printk(KERN_INFO "[ltfall] Buffer Not initialized yet.\n");
return -1;
}
copy_from_user((char *)buffer, (char *)user_buf, user_size);
printk(KERN_INFO "[ltfall] Copy_from_user success.\n");
spin_unlock(&spin);
return 0;
}

module_init(kernel_module_init);
module_exit(kernel_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ltfall");

编写如下exp来和其进行交互,发现已经成功了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* 
简单的测试程序,将我们用户态下的一个字符串写到内核堆上,再读出来即可。
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <string.h>

const char* user_buffer = "This is ltfall's test!";

int main(){
char buffer[0x30];
int fd = open("/dev/ltdevice", 2);

printf("Writing and Reading Test...\n");
write(fd, user_buffer, strlen(user_buffer));
read(fd, buffer, 0x30);
printf("The content of buffer is %s.\n", buffer);

ioctl(fd, 123, 456);
close(fd);

return 0;
}

如下所示:

image-20240426162005274


0x00. Linux kernel基础:编译内核与驱动编写
http://example.com/2024/07/20/system/kernel/编译内核与驱动编写/
作者
Ltfall
发布于
2024年7月20日
许可协议