pwnKernel从0开始(一)
前言
网上一大堆教编译内核的,但由于我的水平太菜,很多教程我看得特别迷糊。还有第一次编译内核时,没设置好参数,直接把虚拟机编译炸开了。所以就想着能不能先做个一键获取内核源码和相关vmlinux以及bzImage的脚本,先试试题,后期再深入探究编译内核,加入debug符号什么的,所以就有了这个一键脚本。
这个直接看我的项目就好了,我是直接拖官方的docker,然后把编译所需要的环境都重新安装了一遍,基本可以适配所有环境,安装各个版本的内核,外加调试信息也可以配置
PIG-007/kernelAll (github.com)
前置环境,前置知识啥的在上面已经足够了,如果还是感觉有点迷糊可以再去搜搜其他教程。看雪的钞sir
师傅和csdn上的ha1vk
师傅就很不错啊,还有安全客上的ERROR404
师傅
钞sir师傅:Ta的论坛 (pediy.com)
ha1vk师傅:kernel- CSDN搜索
error404师傅:Kernel Pwn 学习之路(一) - 安全客,安全资讯平台 (anquanke.com)
这个系列记录新的kernel解析,旨在从源码题目编写,不同内核版本来进行各式各样的出题套路解析和exp的解析。另外内核的pwn基本都是基于某个特定版本的内核来进行模块开发,而出漏洞地方就是这个模块,我们可以借助这个模块来攻破内核,所以我们进行内核pwn的时候,最应该先学习的就是一些简单内核驱动模块的开发。
一、例子编写
首先最简单和经典的的Hello world
1 | //注释头 |
1.头文件简介
module.h
:包含可装载模块需要的大量符号和函数定义。
init.h
:指定初始化模块方面和清除函数。
另外大部分模块还包括moduleparam.h
头文件,这样就可以在装载的时候向模块传递参数。而我们常常用的函数_copy_from_user
则来自头文件uaccess.h
2.模块许可证
1 | //注释头 |
这个就是模块许可证,具体有啥用不太清楚,如有大佬恳请告知。可以通过下列命令查询
1 | grep "MODULE_LICENSE" -B 27 /usr/src/linux-headers-`uname -r`/include/linux/module.h |
或者网址Linux内核许可规则 — The Linux Kernel documentation
3.模块加载卸载
加载
一般以 __init标识声明,返回值为0表示加载成功,为负数表示加载失败。用来初始化,定义之类的。
1 | static int __init hello_init(void) |
在整个模块的最后加上
1 | module_init(hello_init); |
来通过这个init函数加载模块
卸载
一般以 __exit标识声明,用来释放空间,清除一些东西的。
1 | static void __exit hello_exit(void) |
同样的模块最后加上以下代码来卸载
1 | module_exit(hello_exit); |
▲其实加载和卸载有点类似于面向对象里的构造函数和析构函数。
以上是一个最简单的例子,下面讲讲实际题目的编写,实际的题目一般涉及驱动的装载。
二、题目编写
由于模块装载是在内核启动时完成的(root下也可以设置再insmod装载),所以一般需要安装驱动,通过驱动来启动模块中的代码功能。而驱动类型也一般有两种,一种是字符型设备驱动,一种是globalmem虚拟设备驱动。
分配得到的设备号可由cat /proc/devices
来查看
1.字符型设备驱动
(1)安装套路
首先了解一下驱动设备的结构体:
1 | ///linux/cdev.h kernel 5.14.8 |
然后就是套路编写
1 | // 设备结构体 |
这样简单的驱动就安装完了,安装完了之后,我们想要使用这个驱动的话,还需要进行交互,向驱动设备传递数据,所以上面的xxx_fops
,即file_operations
这个结构体就起到了这个功能。
有的时候安装注册设备驱动需要用到class来创建注册,原因未知:
1 | static int __init xxx_init(void) |
(2)交互套路
安装完成之后还需要交互,用到file_operations
结构体中的成员函数,首先了解下这个结构体。
1 | ///linux/fs.h kernel 5.14.8 |
具体一点使用如下
参考:https://www.youtube.com/watch?v=EJ0JQKvxf70&list=PLEJtkJ02eJVX9I66wJD1tn07R52DxVXG0&index=4
其中常用的就是read,write等函数。
之后也是正常的调用函数,套路编写
1 | // 读设备 |
然后需要file_operations
结构体中的函数来重写用户空间的write,open,read等函数:
1 | static struct file_operations xxx_fops = |
这样当用户空间打开该设备,调用该设备的write
函数,就能通过.write
进入到xxx_write
函数中。
▲这样一些常规kernel题的编写模板就总结出来了。
(3)具体的题目
原题:https://github.com/black-bunny/LinKern-x86_64-bypass-SMEP-KASLR-kptr_restric
①代码和简单的解析
1 |
|
②内核函数解析
printk
:
1 | printk(日志级别 "消息文本"); |
其中日志级别定义如下:
1 | #defineKERN_EMERG "<0>"/*紧急事件消息,系统崩溃之前提示,表示系统不可用*/ |
kmalloc
:
1 | static inline void *kmalloc(size_t size, gfp_t flags) |
其中flags一般设置为GFP_KERNEL或者GFP_DMA,在堆题中一般就是
GFP_KERNEL模式,如下:
|– 进程上下文,可以睡眠 GFP_KERNEL
|– 进程上下文,不可以睡眠 GFP_ATOMIC
| |– 中断处理程序 GFP_ATOMIC
| |– 软中断 GFP_ATOMIC
| |– Tasklet GFP_ATOMIC
|– 用于DMA的内存,可以睡眠 GFP_DMA | GFP_KERNEL
|– 用于DMA的内存,不可以睡眠 GFP_DMA |GFP_ATOMIC
具体可以看
Linux内核空间内存申请函数kmalloc、kzalloc、vmalloc的区别【转】 - sky-heaven - 博客园 (cnblogs.com)
kzmalloc
类似,就是分配空间并且内存初始化为0
kfree
:
这个就不多说了,就是简单的释放。
copy_from_user
:
1 | copy_from_user(void *to, const void __user *from, unsigned long n) |
copy_to_user
:
1 | copy_to_user(void __user *to, const void *from, unsigned long n) |
这两个就不讲了,顾名思义。
注册函数
剩下的好多就是常见的注册函数了
1 | alloc_chrdev_region(&t_dev, 0, 1, "xxx");//动态分配主设备号 |
2.globalmem虚拟设备驱动
这个不太清楚,题目见的也少,先忽略。
3.块设备
register_blkdev
申请设备号
1 | int ret = register_blkdev(xxx_dev_no,DEV_NAME); |
- 如果
xxx_dev_no
为0则由该函数自动分配设备号,ret即为返回的设备号 xxx_dev_no
不为0则依据该设备号进行分配
unregister_blkdev
释放设备号
1 | unregister_blkdev(xxx_dev_no,DEV_NAME); |
printk()函数的总结 - 深蓝工作室 - 博客园 (cnblogs.com)
Linux kernel pwn notes(内核漏洞利用学习) - hac425 - 博客园 (cnblogs.com)