通过一道简单的例题了解Linux内核PWN
写在前面
这篇文章目的在于简单介绍内核PWN题,揭开内核的神秘面纱。背后的知识点包含Linux驱动和内核源码,学习路线非常陡峭。也就是说,会一道Linux内核PWN需要非常多的铺垫知识,如果要学习可以先从UNICORN、QEMU开始看起,然后看Linux驱动的内容,最后看Linux的内存管理、进程调度和文件的实现原理。至于内核API函数不用死记硬背,用到的时候再查都来得及。
题目概述
这题是参考ctf-wiki上的内核例题,题目名称CISCN2017_babydriver,是一道简单的内核入门题,所牵涉的知识点并不多。题目附件可以在ctf-wiki的GitHub仓库找到:https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/kernel/CISCN2017-babydriver。
-
首先将题目附件下载下来,解压后得到所有的文件如下:
-
查看启动脚本boot.sh内容如下:
-
文件bzImage是压缩编译的内核镜像文件。有些题目会提供vmlinux文件,它是未被压缩的镜像文件。这个题目没有提供,但也不要紧,可以用脚本提取出vmlinux,而使用vmlinux的目的也就是找gadget,提取vmlinux的脚本也可以在Linux的GitHub上找到:https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux。把代码复制到文件中,保存为extract-vmlinux,然后赋予执行权限。提取vmlinux命令如下:
可以使用ropper在提取的vmlinux中搜寻gadget,ropper比ROPgadget快很多:
-
rootfs.cpio是启动内核的RAM磁盘文件,可以把它看作一个微型Linux文件系统。使用
file
命令查看可以看到它是gzip格式:我们将rootfs.cpio改名为rootfs.cpio.gz,然后将它解压出来:
因为rootfs.cpio里面包含一些文件系统,它的文件比较多,我们可以创建一个文件夹,然后用
cpio
命令把所有文件提取到新建的文件夹下,保证一个干净的根目录,后面也将内容重新打包:
启动文件和驱动程序函数
-
在我们上一步解压完rootfs.cpio之后可以看到它就是Linux的文件系统。在根目录下里面有一个「init」文件,它决定启动哪些程序,比如执行某些脚本和启动shell。它的内容如下,除了
insmod
命令之外都是Linux的基本命令便不再赘述: -
在init文件中看到用
insmod
命令加载了babydriver.ko驱动,那么我们把这个驱动拿出来,检查一下开启的保护:可以看到程序保留了符号信息,其他保护都没有开启
-
把驱动程序放到IDA里面查看程序逻辑,除了
init
初始化和exit
外还有5个函数:-
babyrelease
:主要功能是释放空间 -
babyopen
:调用kmem_cache_alloc_trace
函数申请一块大小为64字节的空间,返回值存储在device_buf
中,并设置device_buf_len
-
babyioctl
:定义0x10001的命令,这条命令可以释放刚才申请的device_buf
,然后重新申请一个用户传入的内存,并设置device_buf_len
-
babywrite
:copy_from_user
是从用户空间拷贝数据到内核空间,应当接受三个参数copy_from_user(char*, char*,int)
,IDA里面是没有识别成功,需要手动按Y键修复。babywrite
函数先检查长度是否小于device_buf_len
,然后把buffer
中的数据拷贝到device_buf
中 -
babyread
:和babywrite
差不多,不过是把device_buf
拷贝到buffer
中
-
漏洞点和利用思路
-
值得注意的是驱动程序中的函数操作都使用同一个变量
babydev_struct
,而babydev_struct
是全局变量,漏洞点在于多个设备同时操作这个变量会将变量覆盖为最后改动的内容,没有对全局变量上锁,导致条件竞争 -
我们使用
ioctl
同时打开两个设备,第二次打开的内容会覆盖掉第一次打开设备的babydev_struct
,如果释放第一个,那么第二个理论上也被释放了,实际上并没有,就造成了一个UAF -
释放其中一个后,使用
fork
,那么这个新进程的cred
空间就会和之前释放的空间重叠 -
利用那个没有释放的描述符对这块空间写入,把
cred
结构体中的uid
和gid
改为0,就可实现提权 -
还有在修改时需要知道
cred
结构的大小,可以根据内核版本可以查看源码,计算出cred
结构大小是0xa8,不同版本的内核源码这个结构体的大小都不一样
exp代码
执行exp
需要将编写的exp编译成可执行文件,然后把它复制到rootfs.cpio提取出来的文件系统中,再将文件系统重新打包成cpio,这样在内核重新运行的时候就有exp这个文件了。
-
将exp编译好,注意需要改为静态编译,因为我们的内核是没有动态链接的:
-
接下来我们复制exp到文件系统下,然后使用
cpio
命令重新打包: -
下一步就可以重新运行内核了。执行boot.sh启动内核后,在刚才拷贝的/tmp目录下找到exp可执行程序:
-
执行后可得到root权限,提权成功:
调试
-
可以在boot.sh文件中添加
-s
参数来使用gdb调试,它默认端口1234。也可以指定端口号进行调试,只需要使用-gdb tcp:port
即可。在启动的内核中使用lsmod
查看加载的驱动基地址,得到0xffffffffc0000000,然后启动gdb,使用target remote
指定调试IP和端口号进行调试,然后添加babydriver的符号信息,过程如下: -
这里建议使用gef插件,pwndbg和peda调试内核总有一些玄学问题。如果gef报错context相关问题(如下图),在gdb中输入命令
python set_arch()
就可以查看调试上下文了: -
我们之前在gdb中使用
add-symbol-file
命令加载了babydriver.ko的符号信息,并指定了加载基地址,在下断点的时候可以直接使用符号来打断点:
总结
通过一道题认识了内核PWN的解题步骤,以及如何对内核进行调试。对于不知道用法的内核函数和结构体,可以在manned.org网站或者源码中查看。
参考资料
CTF-WIKI链接:https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/uaf/#_2
Linux在线源码:https://elixir.bootlin.com/linux/v4.4.72/source/mm/slab.c#L3431
MannedOrg:https://manned.org/kmalloc.3
QEMU手册:https://www.qemu.org/docs/master/system/quickstart.html
UNICORN:https://www.unicorn-engine.org/docs/
__EOF__

本文链接:https://www.cnblogs.com/unr4v31/p/15725128.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 上周热点回顾(3.3-3.9)
· AI 智能体引爆开源社区「GitHub 热点速览」