不会写驱动就会导致不会写驱动的问题
[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 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 Kernel: arch /x86/boot/bzImage is ready (
而此时我们也会同时得到两个文件:
第一个是在当前目录下生成的名为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 2 Settings -> Build static binary file (no shared lib)
随后在配置主页面连续按下两次ESC
,选择保存配置并退出。
输入以下命令来编译busybox
:
建立文件系统 初始化文件系统 输入以下命令来初始化文件系统(是的,就是我们平时看到的内核的样子)
1 2 3 4 5 6 cd _installmkdir -pv {bin,sbin,etc,proc,sys,home,lib64,lib/x86_64-linux-gnu,usr/{bin,sbin}}touch etc/inittabmkdir etc/init.dtouch etc/init.d/rcSchmod +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 /tmpmkdir /dev/pts mount -t devpts devpts /dev/ptsecho -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 /devexec 0</dev/consoleexec 1>/dev/consoleexec 2>/dev/consoleecho -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n" setsid cttyhack setuidgid 1000 sh umount /proc umount /sys poweroff -d 0 -f
添加可执行权限:
配置用户组 - 前言 配置用户组 - 前言
这一小节是作为用户组的知识补充,不是内核编译的操作。
若您只需要编译出内核,请跳过这一小节 。
在etc
目录下含有/etc/passwd
和/etc/group
两个文件,都是用于Linux
的用户组管理的。
其中/etc/group
包含系统上用户组的信息,而/etc/passwd
包含具体某个用户的信息。
具体来说,/etc/group
中,每一行表示一个组,每个组的条目由四个字段组成,以冒号分隔,包括组名、组密码、组ID
、组成员。
以以下信息为例:
其中包含两个组,分别为组名为root
的组和组名为chal
的组。其中:
而对于/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
用户的ID
为0
而chal
用户的ID
为1000
。
第四个字段表示组ID
,表示用户所属组的ID
。
第五个字段表示用户信息,通常含有用户全名或其他描述性信息。
第六个字段表示用户的家目录,表示用户的主目录,用户登录后会进入这个目录。
第七个字段表示登录shell
,是用户登录后默认启动的shell
。
了解到上述信息后,我们可以修改rcS
文件来修改qemu
虚拟机启动后的用户。
其中,rcS
文件是一个启动脚本,用于在系统引导过程中启动一些基本的系统服务和设置环境。在部分文件系统中,根目录下有一个名为init
文件即为rcS
文件。有时候也会位于/etc
中。
init
文件中有一行命令如下:
1 setsid /bin/cttyhack setuidgid 1000 /bin/sh
其中setsid
命令可以启动一个新的会话,并连续执行了/bin/cttyhack
、setuidgid 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/passwdecho "ctf:x:1000:1000:ctf:/home/ctf:/bin/sh" >> etc/passwdecho "root:x:0:" > etc/groupecho "ctf:x:1000:" >> etc/groupecho "none /dev/pts devpts gid=5,mode=620 0 0" > etc/fstab
如此我们可以创建两个用户组root
和ctf
,以及属于他们的两个用户root
和ctf
。
打包文件系统为镜像文件 若您已经知晓一些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 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/ sudo umount ./tmp
0x04 使用qemu运行内核 现在,我们已经构建好了运行一个内核的所有的必要/基本组件:
编写启动脚本 首先,我们将刚刚创建的文件系统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连接本地端口进行调试
启动内核 启动内核!
0x05 编写内核驱动(可装载内核模块LKM) 准备工作 我们这里以刚刚下载的Linux 5.11.0
的源码为例子来讲解如何编写内核驱动。
首先切换到Linux 5.11.0
源码目录:
执行如下命令,来准备好编译内核模块所需要的文件:
编写内核驱动代码 准备一个简单的内核驱动代码,该代码在载入/卸载的时候会通过printk
在内核缓冲区输出内容。
放在任意地方都可以,因为我们随后会指定内核源码路径:)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #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) modulesclean: 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) modulesclean: make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean
或者添加如下行,添加调试信息、优化等:
完成后,输入make
,可得到如下输出,表示编译成功,可以在当前目录下获得hello.ko
文件:
1 2 3 4 5 6 7 8 $ 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 corecp 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 /tmpmkdir /dev/pts mount -t devpts devpts /dev/pts insmod /hello.koecho -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);
上面三个参数并不难理解,比较陌生的可能是其中的const struct file_operations* fops
。
这是因为每一个要注册的设备都需要一个自身的struct file_operations
结构体来指示该设备的各种行为,例如open
、write
、ioctl
等操作。
第二步:创建设备类 使用如下函数创建设备类:
1 2 3 struct class *class_create (struct module *owner, const char *name) ;
很好理解。只是注意,假如这一步出错了,需要销毁第一步注册的字符型设备,后面的操作也是类似的。
第三步:创建设备节点,并在/dev目录下生成设备节点文件 使用如下函数进行上述操作:
1 2 3 4 5 6 device_create(struct class *cls, struct device* parent, dev_t devt, void * drvdata, const char *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 #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" ); major_num = register_chrdev(0 , DEVICE_NAME, <_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); 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" ); 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" ); __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" );
随后将其注册到我们的内核中,发现设备已经挂载成功:
亦或者是不在驱动代码里面更改其权限操作,而是放在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_lock
和spin_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 #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" ); major_num = register_chrdev(0 , DEVICE_NAME, <_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); 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" ); 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" ); __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 ; }
如下所示: