编辑
2023-03-08
Kernel Pwn
00
请注意,本文编写于 624 天前,最后修改于 624 天前,其中某些信息可能已经过时。

目录

基础知识
LKMs
单内核系统
微内核系统
Linux
ioctl
First LKM!
与LKM进行IO通信
设备
主设备号&次设备号
设备节点
file_operations结构体
class结构体
设备的注册和注销
设备权限管理
First Device!
实现IO通信

吼吼

基础知识

LKMs

全称Loadable Kernel Modules 可加载核心模块 也称为内核模块 其实就是内核态的可执行程序 主要包括驱动程序 设备驱动 文件系统驱动 内核扩展驱动模块等 其实只是名称上的区别和用户态的可执行文件没有区别

内核模块可以单独编译 但是不能单独运行 只能链接到内核中作为内核的一部分在内核态运行

单内核系统

单内核系统简单理解就是一个大进程 其内部包含许多模块 在运行时 模块之间的通信是通过调用其他模块中的函数实现的 而不是消息传递 相对应的 单一内核在运行效率上有很大的优势 但是编写难 修改难 内核代码高度集成 以上所有模块均在内核态执行

微内核系统

虽然微内核和单内核名字很相似 但是实现方式是截然不同的 有点类似RISC和CISC的区别(个人理解) 微内核系统只提供必要服务的操作系统内核 包括任务 线程 交互进程通信 内存管理等 所有的服务都在用户态下进行

Linux

Linux是一个典型的单内核系统 所以Linux本身也提供模块机制 一般来说Pwn中的漏洞就是存在这些模块之中

比较常见的命令有

insmod: 将指定的模块加载到内核中

rmmod: 将指定模块从内核中写挨着

lsmod: 列出所有已经加载的模块

modprobe: 添加或者删除模块 在加载模块时会自动寻找依赖

dmesg: 输出内核态缓冲区

ioctl

ioctl是一个C语言的函数 是一个系统调用 可以用于与设备通信

First LKM!

C语言程序 保存为helloworld.c

c
#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> static int __init kernel_module_init(void) // 初始化函数 和构造函数一样 在载入内核后自动调用 { // printk() 内核态输出函数 只不过所有内核态输出不能输出到屏幕 只能输出到缓冲区 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("Du4t");

Makefile

makefile
target=helloworld kernel_dir=/usr/src/linux-headers-$(shell uname -r) pwd=$(shell pwd) obj-m+=$(target).o all: make -C $(kernel_dir) M=$(pwd) modules

测试模块

bash
sudo insmod helloworld.ko sudo rmmod helloworld sudo dmesg

与LKM进行IO通信

上面的LKM实例 我们只实现了LKM能自己对缓冲区进行输出 显然作为一个内核模块光这些功能是不够的..最起码需要能够实现一定的IO操作 并且对相应的IO操作进行相应的反应 而LKM是不能直接与用户态的用户进行通信的 LKM规定的通信是通过设备进行的 所以我们要先构建一个我们想要的设备 然后编写对应的LKM从我们创造的设备中取出数据 并且输出

设备

在Linux中设备主要分为两类 分别是字符设备块设备 其实区别在名字上就有所体现了

字符设备只允许以字符流的形式进行IO操作 例如键盘 串口等 不能进行随机存取

而块设备是以数据块的方式进行交互 例如硬盘等 可以实现随机存取

主设备号&次设备号

Linux中使用设备号来标记一个设备 这个设备号在内核中是以dev_t(unsigned long)类型来进行存储 其中高字节是主设备号 低字节是次设备号

主设备号主要负责标识设备类型 使用宏MAJOR(dev_t dev)可以获取主设备号

次设备号主要负责区分同类型设备 使用宏MINOR(dev_t dev)可以获取次设备号

LInux创建一个设备号使用宏MKDEV(int major, int minor) 通过传入主设备号和次设备号自动生成设备号

其实简单来说就是 主设备号标记类型 次设备号标记具体ID

设备节点

这个其实就是Linux的基础知识了 所有Linux中的设备都是以文件的形式存在的 这些文件都放在/dev下 每个设备都是一个设备节点

所有设备的都是结构体device的实例化 device结构体定义在include/linux/device.h

在设备树中使用device_node表示一个设备

file_operations结构体

file_operations结构体定义在include/linux/fs.h中 包含许多关于文件操作的函数指针 例如read write等函数指针

c
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 *); ... } __randomize_layout;

class结构体

结构体class定义于include/linux/device/class.h中 用以表示高层次抽象的设备

每个设备节点实例中都应当包含着一个指向其自身class结构体的指针

设备的注册和注销

注册一个设备的流程很直球 我们先来创造一个字符型设备

  1. 调用内核提供的函数register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)来创造主设备号

函数定义在include/linux/fs.h

c
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops) { return __register_chrdev(major, 0, 256, name, fops); }

major : 主设备号 若为0则由内核分配主设备号

name: 设备名 用户自己指定

fops: file_operations结构体指针

  1. 调用宏class_create(owner, name)创建设备对应的class结构体

宏定义在include/linux/device/class.h

c
#define class_create(owner, name) \ ({ \ static struct lock_class_key __key; \ __class_create(owner, name, &__key); \ })

owner: 指向拥有此class结构体的模块的指针 通常直接传入THIS_MODULE

name: class结构体的名字

这里这个THIS_MODULE定义在include/linux/export.h

c
extern struct module __this_module; #define THIS_MODULE (&__this_module)

即是保存了__this_module这个对象的地址 而这个__this_module定义在..我们刚刚编译的内核模块时多出来的helloworld.mod.c

c
__visible struct module __this_module __section(".gnu.linkonce.this_module") = { .name = KBUILD_MODNAME, .init = init_module, #ifdef CONFIG_MODULE_UNLOAD .exit = cleanup_module, #endif .arch = MODULE_ARCH_INIT, };
  1. 最后调用函数device_create(struct class *cls, struct device *parent, dev_t devt,void *drvdata, const char *fmt, ...)来创建设备节点文件

cls: class结构体的指针

parent: 父设备的设备节点 一般为某种总线或者主机控制器 若为顶级设备则为NULL

devt: 设备号

drvdata: 驱动相关信息 若无则为NULL

fmt: 设备名称

设备注销的流程就是反着来

device_destroy(struct class *cls, dev_t devt)-> class_destroy(struct class *cls)->unregister_chrdev(unsigned int major, const char *name)

设备权限管理

内核模块运行在内核空间 所创建的设备节点只能在root用户使用 这显然不是我们想要的 所以我们需要想办法使所有用户都有权限使用该设备和我们的内核模块进行交互

在内核中使用iNode结构体来表示一个文件(没错 就是组成原理中那个iNode节点) INode结构体定于include/linux/fs.h

c
struct inode { umode_t i_mode; unsigned short i_opflags; kuid_t i_uid; kgid_t i_gid; unsigned int i_flags; #ifdef CONFIG_FS_POSIX_ACL struct posix_acl *i_acl; struct posix_acl *i_default_acl; #endif const struct inode_operations *i_op; struct super_block *i_sb; struct address_space *i_mapping; ... }

其中文件的权限管理就是由这个i_mode成员管理的 而在内核中打开一个文件是通过flip_open()函数进行的 在内核态中 文件以file结构体进行描述 该结构体同样定义于include/linux/fs.h

c
struct file { union { struct llist_node fu_llist; struct rcu_head fu_rcuhead; } f_u; struct path f_path; struct inode *f_inode; /* cached value */ const struct file_operations *f_op; ... }

可以注意到 结构体之中是包含针对inode结构体的指针的 且内核提供file_inode()函数来获得file结构体中的inode结构体指针

那么其实怎么修改权限也很简单了.. 首先我们在内核态使用flip_open()打开我们的设备节点 然后使用file_inode()来取得对应设备节点的inode结构体指针 然后修改对应结构体中的i_mode成员即可

First Device!

c
#include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/device.h> #define DEVICE_NAME "Du4t" #define DEVICE_PATH "/dev/Du4t" #define CLASS_NAME "C_Du4t" 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 PW_module_fo= { .owner=THIS_MODULE }; static int __init kernel_module_init(void) { printk(KERN_INFO "[du4t_TestModule]: Module loaded. Start to register device...\n"); major_num=register_chrdev(0,DEVICE_NAME,&PW_module_fo); // 创造主设备号 if (major_num<0) { printk(KERN_INFO "[du4t_TestModule]: Failed to register a major number...\n"); return major_num; } printk(KERN_INFO "[du4t_TestModule]: Major number is %d...\n",major_num); module_class=class_create(THIS_MODULE,CLASS_NAME); // 创造class结构体 if(IS_ERR(module_class)) { unregister_chrdev(major_num,DEVICE_NAME); // 谁创造谁注销 设备号作为操作系统的一种资源 在退出时应当注销 printk(KERN_INFO "[du4t_TestModule]: Failed to register class device...\n"); return PTR_ERR(module_class); } printk(KERN_INFO "[du4t_TestModule]: Have created class...\n"); module_device=device_create(module_class,NULL,MKDEV(major_num,0),NULL,DEVICE_NAME); // 创造设备节点 if(IS_ERR(module_class)) { class_destroy(module_class); unregister_chrdev(major_num,DEVICE_NAME); printk(KERN_INFO "[du4t_TestModule]: Failed to create device...\n"); return PTR_ERR(module_device); } __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 "[du4t_TestModule]: Failed to open device...\n"); } __inode=file_inode(__file); __inode->i_mode=0666; filp_close(__file,NULL); printk(KERN_INFO "[du4t_TestModule]: Have changed the device's privilege...\n"); return 0; } static void __exit kernel_module_exit(void) { printk(KERN_INFO "[du4t_TestModule]: 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 "[du4t_TestModule]: See you next time...\n"); } module_init(kernel_module_init); module_exit(kernel_module_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Du4t");

实现IO通信

显然上面只能在内核内部自己进行输出的操作是不符合我们要求的 我们的想法是构建一个可以响应用户输入的驱动程序 所以我们要实现相应的IO操作 这个时候就要用到上面我们提到的file_operations结构体了 这里再展示下其结构

c
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 *); ... } __randomize_layout;

其中结构体之中的函数指针就是给我们自定义实现read write ioctl来用的 那么就是来写一套实现函数了

首先是P_Device.h

c
#include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/device.h> #define DEVICE_NAME "Du4t" #define DEVICE_PATH "/dev/Du4t" #define CLASS_NAME "C_Du4t" #define NOT_INIT 0xffffffff #define READ_ONLY 0x1000 #define ALLOW_WRITE 0x1001 #define BUFFER_RESET 0x1002 static int major_num; //主设备号 static int module_mode=READ_ONLY; static struct class *module_class=NULL; static struct device *module_device=NULL; static struct file *__file=NULL; struct inode *__inode=NULL; static void *buffer=NULL; static spinlock_t spin; // 自定义以下这些函数以实现使用read write ioctl来对LKM进行通信 static int __init kernel_module_init(void); static void __exit kernel_module_exit(void); static int module_open(struct inode *,struct file *); static ssize_t module_read(struct file *,char __user *,size_t,loff_t *); static ssize_t module_write(struct file *,const char __user * ,size_t,loff_t *); static int module_release(struct inode *, struct file *); static long module_ioctl(struct file *,unsigned int,unsigned long); static long __internal_module_ioctl(struct file * __file,unsigned int cmd,unsigned long param); static struct file_operations PW_module_fo = { //descripe the device .owner = THIS_MODULE, .unlocked_ioctl = module_ioctl, .open = module_open, .read = module_read, .write = module_write, .release = module_release, };

然后是IO_Device.c

c
#include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/device.h> #include <linux/slab.h> #include "P_Device.h" static int __init kernel_module_init(void) { spin_lock_init(&spin); //增加自旋锁以支持多线程 printk(KERN_INFO "[du4t_TestModule]: Module loaded. Start to register device...\n"); major_num=register_chrdev(0,DEVICE_NAME,&PW_module_fo); // 创造主设备号 if (major_num<0) { printk(KERN_INFO "[du4t_TestModule]: Failed to register a major number...\n"); return major_num; } printk(KERN_INFO "[du4t_TestModule]: Major number is %d...\n",major_num); module_class=class_create(THIS_MODULE,CLASS_NAME); // 创造class结构体 if(IS_ERR(module_class)) { unregister_chrdev(major_num,DEVICE_NAME); // 谁创造谁注销 设备号作为操作系统的一种资源 在退出时应当注销 printk(KERN_INFO "[du4t_TestModule]: Failed to register class device...\n"); return PTR_ERR(module_class); } printk(KERN_INFO "[du4t_TestModule]: Have created class...\n"); module_device=device_create(module_class,NULL,MKDEV(major_num,0),NULL,DEVICE_NAME); // 创造设备节点 if(IS_ERR(module_class)) { class_destroy(module_class); unregister_chrdev(major_num,DEVICE_NAME); printk(KERN_INFO "[du4t_TestModule]: Failed to create device...\n"); return PTR_ERR(module_device); } __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 "[du4t_TestModule]: Failed to open device...\n"); } __inode=file_inode(__file); __inode->i_mode=0666; filp_close(__file,NULL); printk(KERN_INFO "[du4t_TestModule]: Have changed the device's privilege...\n"); return 0; } static void __exit kernel_module_exit(void) { printk(KERN_INFO "[du4t_TestModule]: 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 "[du4t_TestModule]: See you next time...\n"); } static long module_ioctl(struct file *__file,unsigned int cmd,unsigned long param) { long ret; spin_lock(&spin); ret=__internal_module_ioctl(__file,cmd,param); spin_unlock(&spin); return ret; } static long __internal_module_ioctl(struct file * __file,unsigned int cmd,unsigned long param) { printk(KERN_INFO "[du4t_TestModule]: Received operation code %d...\n",cmd); switch (cmd) { case READ_ONLY: if(!buffer) { printk(KERN_INFO "[du4t_TestModule]: Please reset buffer firstly...\n"); return -1; } printk(KERN_INFO "[du4t_TestModule]: Module operation mode set to READ_ONLY...\n"); module_mode=READ_ONLY; break; case ALLOW_WRITE: if(!buffer) { printk(KERN_INFO "[du4t_TestModule]: Please reset buffer firstly...\n"); return -1; } printk(KERN_INFO "[du4t_TestModule]: Module operation mode set to ALLOW_WRITE...\n"); module_mode=ALLOW_WRITE; break; case BUFFER_RESET: if(!buffer) { buffer=kmalloc(0x500,GFP_ATOMIC); // 内核态malloc GFP_ATOMIC不可睡眠 GFP_KERNEL可睡眠 if(buffer==NULL) { printk(KERN_INFO "[du4t_TestModule]: Malloc Error...\n"); module_mode=NOT_INIT; return -1; } } printk(KERN_INFO "[du4t_TestModule]: Buffer reset, Module operation mode set to READ_ONLY...\n"); memset(buffer,0,0x500); module_mode=READ_ONLY; break; case NOT_INIT: printk(KERN_INFO "[du4t_TestModule]: Module operation mode set to NOT_INIT...\n"); module_mode=NOT_INIT; kfree(buffer); return 0; default: printk(KERN_INFO "[du4t_TestModule]: Module operation mode error...\n"); return -1; } return 0; } static int module_open(struct inode *__inode,struct file *__file) { spin_lock(&spin); if (buffer==NULL) { buffer=kmalloc(0x500,GFP_ATOMIC); if(buffer==NULL) { printk(KERN_INFO "[du4t_TestModule]: Malloc Error...\n"); module_mode=NOT_INIT; return -1; } memset(buffer,0,0x500); module_mode=READ_ONLY; printk(KERN_INFO "[du4t_TestModule]: Device open, buffer initialized successfully...\n"); } else { printk(KERN_INFO "[du4t_TestModule]: Device has opened...\n"); } spin_unlock(&spin); return 0; } static int module_release(struct inode *__inode, struct file *__file) { spin_lock(&spin); if (buffer) { kfree(buffer); buffer=NULL; } printk(KERN_INFO "[du4t_TestModule]: Device has closed...\n"); spin_unlock(&spin); return 0; } static ssize_t module_read(struct file *__file,char __user *user_buffer,size_t size,loff_t *__loff) { const char * const buf=(char *)buffer; int count; spin_lock(&spin); if(module_mode==NOT_INIT) { printk(KERN_INFO "[du4t_TestModule]: Module mode is NOT_INIT...\n"); return -1; } count=copy_to_user(user_buffer,buf,size>0x500?0x500:size); // copy_to_user(to,from,size) 具体功能是将数据从内核空间复制到用户空间 from是内核空间的指针 to是用户空间的指针 spin_unlock(&spin); return count; } static ssize_t module_write(struct file *__file,const char __user *user_buf,size_t size,loff_t *__loff) { const char * const buf=(char *)buffer; int count; spin_lock(&spin); if(module_mode==NOT_INIT) { printk(KERN_INFO "[du4t_TestModule]: Module mode is NOT_INIT...\n"); return -1; } else if(module_mode==READ_ONLY) { printk(KERN_INFO "[du4t_TestModule]: Module mode is READ_ONLY...\n"); return -1; } else { count=copy_from_user(buf,user_buf,size>0x500?0x500:size); // copy_to_user(to,from,size) 具体功能是将数据从内核空间复制到用户空间 from是内核空间的指针 to是用户空间的指针 } spin_unlock(&spin); return count; } module_init(kernel_module_init); module_exit(kernel_module_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Du4t");

整体逻辑还是很简单的 下面就是写一个可以跟我们驱动交互的程序了

c
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<string.h> #include<fcntl.h> #include<sys/ioctl.h> char * buf = "Test Module.."; int main(void) { char output[100]; int fd=open("/dev/Du4t",2); int len=strlen(buf); ioctl(fd,0x1000,NULL); // READ_ONLY write(fd,buf,len); ioctl(fd,0x1001,NULL); // ALLOW_WRITE write(fd,buf,len); read(fd,output,len); write(0,output,len); ioctl(fd,0x1002,NULL); // BUFFER_RESET read(fd,output,len); write(0,output,len); close(fd); return 0; }

本文作者:Du4t

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!