io_uring 模块 pbuf_ring 漏洞
本篇博客主要对以下文章的内容进行复现:
1 | / $ uname -r |
1 | qemu-system-x86_64 \ |
- smep,smap,kaslr,pti
1 | !/bin/sh |
内核源码下载:https://src.fedoraproject.org/repo/pkgs/kernel/linux-5.19-rc2.tar.xz/
- 该漏洞已经在 5.19-rc8 中被修复
- 在编译内核前需要先将修复的部分还原
- 内核编译选项如下:
1 | CONFIG_KCOV=y |
io_uring 模块的使用
IO uring(Unified Resource Gestion)是一个 Linux 内核功能,它允许异步 I/O 操作,从而提高系统性能
- io_uring 的使用案例包括文件读写、网络通信、数据库连接等
- io_uring 通过使用用户空间和内核之间的通信机制,允许用户空间应用程序在异步 I/O 操作完成后立即获取结果,而无需等待内核完成磁盘操作或其他内核操作
io_uring
的实现仅仅使用了三个 syscall:
io_uring_setup
:设置io_uring
上下文io_uring_enter
:提交并获取完成任务io_uring_register
:注册内核用户共享的缓冲区
基于共享内存,io_uring 维护了两个与内核共享的队列:
- submit 队列:用于存储待提交的 I/O 请求
- completion 队列:用于存储 I/O 请求的完成状态
submit 队列中的 I/O 请求与 completion 队列中的 I/O 完成事件之间也没有固定的对应关系,内核会根据 I/O 请求的类型、文件描述符、线程池等信息自动将 I/O 请求分配到合适的队列中
- 由于 submit / completion 队列属于用户态程序与内核的共享空间
- 内核只需要读取 submit 队列中的参数就可以执行相应的内核态函数,不需要执行系统调用
- 当数据执行完毕时,内核又会将返回数据写入 completion 队列
使用案例如下:(文件读写)
1 | // gcc -o io_uring io_uring.c -luring -fno-stack-protector -no-pie -g |
syzkaller 的安装与使用
syzkaller 是一个用于自动生成内核错误测试用例的 fuzz 工具,它通过利用目标内核的漏洞来生成测试用例,这些测试用例可以用于测试内核的安全性
syzkaller 的主要功能包括自动生成、测试和报告内核错误
syzkaller 使用 Go 语言编写,因此需要获取 go 语言的 tool chain(经过测试,现在最新版的 syzkaller 需要 1.19 版本的 go 环境)
1 | wget -c https://dl.google.com/go/go1.19.2.linux-amd64.tar.gz |
安装 syzkaller:
1 | git clone https://github.com/google/syzkaller |
编译完成后,在 syzkaller 目录下会出现一个 bin 目录:
1 | bin |
- 如果
syzkaller/bin
目录下,没有syz-extract
和syz-sysgen
这两个文件的话,需要执行如下命令编译:
1 | make bin/syz-extract |
使用 syzkaller 前,先新建一个 workdir 目录,并新建一个 config 文件用于配置运行所需参数(命名为 test.cfg)
1 | { |
在开始 fuzz 之前需要先配置 Imgage 镜像
首先安装 debootstrap,它是 linux 下用来构建一套基本根文件系统的工具:
1 | sudo apt-get install debootstrap |
之后在 linux 项目目录下键入以下命令,以创建 Debian Stretch Linux image:
1 | wget https://raw.githubusercontent.com/google/syzkaller/master/tools/create-image.sh -O create-image.sh |
上述操作全部完成后,执行以下命令来尝试启动:
1 | qemu-system-x86_64 \ |
然后测试 ssh 能否成功工作,因为 syzkaller 会用到 ssh:
1 | ➜ pwntest ssh -i ./image/bullseye.id_rsa -p 10021 -o "StrictHostKeyChecking no" root@localhost |
如果遇到以下报错可以参考如下的解决方案:
1 | kex_exchange_identification: read: Connection reset by peer |
先启动内核,后启动 fuzz:
1 | ➜ pwntest /home/yhellow/Tools/syzkaller/bin/syz-manager -config=test.cfg |
开始 fuzz 后,在 http://127.0.0.1:56741/ 可以查看详细信息:
syscall description 的编写
syzkaller 自己定义了一套描述系统调用模版的声明式语言 syzlang
- 为了提高 fuzz 效率,我们必须为目标系统量身定制这种声明文件
- 通常一个设备节点对应一个声明文件
- 所谓的声明文件就是一个 txt,根据 syzkaller 定义的语法,在这个 txt 文档中描述设备节点的接口信息以及参数格式
整个定制过程分为4步:
- 根据目标内核模块的信息,撰写符合 syzlang 语法的 txt 声明文件
- syz-extract 根据 txt 及 linux 源码,提取符号常量的值,生成中间文件(.const 文件)
- syz-sysgen 根据 const 文件生成 syzkaller 执行时使用的 go 文件
- 重新编译 syzkaller
使用如下命令编译自定义模块:
1 | bin/syz-extract -os linux -arch amd64 -sourcedir "/home/yhellow/pwntest/code/linux-5.19.2" test.txt |
编译完成后运行 syz-sysgen,然后重新编译 syzkaller:
1 | bin/syz-sysgen |
- 该步骤将更新
/syzkaller/sys/linux/gen/amd64.go
,自动添加上新定义的系统调用
syzkaller 源码中的 /syzkaller/sys/linux
目录下专门记录有各个常用模块的 syzlang 文档(已经编译完成),本实验我们需要使用 io_uring.txt
为了提高 fuzz 效率,增加了 “enable_syscalls” 项,只允许某些系统调用,能更快地触发漏洞:
1 | { |
分析 crash 文件
所有 fuzz 出的 crash 信息都存储在 /workdir/crashes
中
当 syzkaller fuzz 遇到 crash 后会尝试复现该 crash:(并不是每一次都能成功)
当 syzkaller 成功复现 crash 时,会出现如下信息:
- PS:有时候复现的 C 代码特别奇怪,也不能触发 crash,不能完全采信
漏洞分析
分析核心系统调用 syscall(__NR_io_uring_register)
对应的内核源码:
1 | static int __io_uring_register(struct io_ring_ctx *ctx, unsigned opcode, |
1 | static int io_register_pbuf_ring(struct io_ring_ctx *ctx, void __user *arg) |
- 首先检查传入的参数,并在
io_uring_context
中分配io_buffer_list
对象数组ctx->io_bl
- 然后根据参数中的缓冲区组ID找到对应的
io_buffer_list
对象 - 然后调用
io_pin_pages
尝试根据用户给定的地址和长度分配 FOLL_PIN 的页 - 如果分配分配失败,就直接释放掉
io_buffer_list
对象(变量bl
指向的是对象数组中的一项,不能单独释放,因而触发报错)
变量 bl=&ctx->io_bl[bgid]
,如果 bgid=0
,就可以释放整个 ctx->io_bl
(不会触发报错),但是释放之后并没有清除 ctx->io_bl
,后续使用就会造成 UAF
- PS:从后续修复的代码来看,设计者可能只是想释放由
kzalloc(sizeof(*bl), GFP_KERNEL)
申请的内存,但是没有考虑周全
入侵思路
有 UAF 的对象大小为 0x800(使用 kmalloc-2k),可以尝试利用 msg_msg 占用 UAF 堆块,然后利用 io_provide_buffers()
在链表 ctx->io_bl[0].buf_list
上添加一个 io_buffer
对象
测试脚本如下:
1 | init_io_uring(); |
正常状态下的 io_buffer_list
:
1 | pwndbg> telescope 0xffff8880059f6800 |
被 msg_msg
覆盖后:
1 | pwndbg> telescope 0xffff8880059f6800 |
msg_msg
覆盖大小为 0x420,后续的io_buffer_list
正常
1 | pwndbg> telescope 0xffff8880059f6800+0x420 |
添加 io_buffer
对象后:
1 | pwndbg> telescope 0xffff8880059f6800 |
- 对于
msg_msg
而言,程序会误以为io_buffer
对象也是msg_msg
结构体 - 打印位于
ctx->io_buffer_cache
的io_buffer
对象:
1 | pwndbg> telescope 0xffff888005a16000 |
- 新添加的第2,3个
io_buffer
对象都会链接到ctx->io_bl[33]
构成循环链表,利用这一点可以泄露ctx->io_bl
的地址(UAF 对象的地址)
接下来我想尝试正常释放 ctx->io_bl
并用其他内核结构体占位,但不管是 io_unregister_pbuf_ring
还是 io_destroy_buffers
都会因为 io_buffer_list
的结构被破坏而执行失败
这里文章采用的利用思路是:
- 利用
msg_msg
伪造io_buffer_list
对象 - 然后通过
kvfree(bl->buf_pages)
获取 kmalloc-2k 上的任意地址 free - 构造对象重叠以备后续利用
测试脚本如下:
1 | msg_len = 0x800 - 0x30; |
打印 UAF 对象:
1 | pwndbg> telescope 0xffff888005a33800 |
- 此时已经成功伪造了
io_buffer_list
1 | ► 0xffffffff8132fb6f <__io_uring_register+623> call __io_remove_buffers.isra.0 > |
1 | ► 0xffffffff81325ebe <__io_remove_buffers.isra.0+270> call kvfree <kvfree> |
- 这里将会释放
io_buffer_list
内部的内存区域,实现堆重叠
利用堆重叠可以覆盖位于 UAF msg_msg
下方的另一个 msg_msg
,然后溢出读取这个 msg_msg
下方的 io_ring_ctx
对象:
1 | tmp = (uint64_t*)msg->mtext; /* 申请到msg_msg+0x60,覆盖new msg_msg */ |
计算出内核基地址后,就可以考虑打 msg_msg unlink attack,往 modprobe_path
中写入自定义脚本的路径
测试脚本如下:
1 | msg_len = 0x800 - 0x30; |
用同样的方法控制 msg_msg
,不过这次的目的是修改 msg_msg.m_list
:
1 | 100:0800│ 0xffff888005bfd800 —▸ 0xffffffff82e51258 ◂— 0x0 |
最后触发 msg_msg unlink attack 即可完成提权
完整 exp 如下:
1 |
|