CVE-2023-2598 内核提权详细分析
默文 历史精选 187浏览 · 2025-03-07 05:44

CVE-2023-2598 内核提权详细分析

漏洞简介

漏洞编号: CVE-2023-2598

影响版本:6.3 <= Linux Kernel < v6.3.2

漏洞产品: linux kernel - io_uring & io_sqe_buffer_register io_uring & folio

利用效果: 本地提权

图片加载失败


环境搭建

复现环境:qemu + linux kernel v6.3.1

环境附件: mowenroot/Kernel

复现流程: 执行exp后,账号:hacker的root用户被添加。su hacker完成提权。

图片加载失败


漏洞原理

漏洞本质是物理地址越界读取(oob)io_uring 模块中提供 io_sqe_buffers_register 来创建一个 Fixed Buffers 的区域,这块区域会锁定,不能被交换,专门用来数据的读取。但是在进行连续多个大页的优化,尝试合并页的时候,使用了新机制 foliofolio 是物理内存、虚拟内存都连续的 page 集合,在进行页合并时只判断 page 是否属于当前复合页,而未判断是否连续。当用户传入同一个物理地址时,长度是整个复合页长度,地址是指向一个地址,这个时候就会造成物理地址越界读取

漏洞技术点涉及 io_uring 、复合页机制folio,下面详细分析。

参考链接:

CVE-2023-2598 io_uring内核提权分析 | CTF导航

https://chompie.rip/Blog+Posts/Put+an+io_uring+on+it+-+Exploiting+the+Linux+Kernel

https://anatomic.rip/cve-2023-2598/#folio

虽然参考了很多资料,但是网传的EXP手法都过于复杂,并且还需要内核基地址的校准,并没有通杀性,作者在这里使用这个漏洞转化为稳定强大的“脏牛”。

folio

Linux 内核 v5.16 引入了内存管理特性 folio,旨在解决传统 struct page 在处理内存页时的效率问题,特别是在管理大块内存和页缓存(page cache)时。

Linux 内核中,物理内存以 页(page) 为单位管理,每个页对应一个 struct page 结构体。传统方式中,无论是 4KB 的小页还是 2MB 的大页(如透明大页,THP),每个页都需独立的 struct page 实例,说人话就是不管你申请多少大的内存,我都 4KB、4KB 的给你。但是吧,可能在内存操作少的时候无感,但使用大页的时候开销就会变的巨大。总结一下就会有以下缺点:

内存开销大:每页的元数据(struct page)占用额外内存,尤其是大页场景下,多个连续页的元数据导致冗余。

操作效率低:处理大块内存时,需遍历每个页的 struct page,这就有点搬砖的意思了。

代码复杂度高:内存管理代码需区分单页与复合页,逻辑复杂。在没引入 folio 之前,操作复合页很复杂。

在没 folio 之前都是通过 "复合页" ,来抽象多个物理连续页的。当 __alloc_pages 分配标志GFP FLAGS指定了__GFP_COMP,那么内核必须将这些页组合成复合页,第一个页称为 head page,其余的所有页称为 tail page

复合页通过以下方式区分 headtail 页:

(1) Head 页

PG_head 标志head_page ->flags 设置 PG_head,表示它是复合页的首个页。

private 字段:head 页的 page->private 指向自身,即 head->private = (unsigned long)head

(2) Tail 页

compound_head 字段compound_head 上的最后一位设置为 1 ,表示 tail page

private 字段:tail 页的 page->private 指向 head 页,即 tail->private = (unsigned long)head

复合页的总页数由分配时的 order 决定,计算公式为 2^order。例如,order=3 表示复合页包含 8 个页。总页数信息存储为第一个 tail 页的 compound_order:通过 compound_order() 函数从 compound_order 中提取 order 值,进而计算总页数。

看到这里可能以及感觉到有点复杂了,当一个概念强加在另一个概念上,这就会变的非常糅杂。“复合页”就是如此,而 folio是一个全新的结构体,相对概念也不干扰 page 。所以在这个时候 Linus 认为 folio 的优势也是明显的,能够更直白的处理复合页,避免一些混乱的问题,最终 folio 被采用。

folio 是 过去复合页的替代品,来看一下两者对比的基本操作。显而易见的 folio 更容易理解操作。

Folio 机制 的引入,通过将一个或多个连续的物理页抽象为逻辑上的 folio 单元,统一管理单页和大页,优化内存操作效率。

folio 本质上可以看作是一个集合,是物理连续、虚拟连续2^n 次的 PAGE_SIZE 的一些 bytes 的集合,n可以是0,也就是说单个页也算是一个folio。

Folio 并非全新结构,而是对 struct page 的封装扩展:

图片加载失败


而且 folio 里面还内嵌了 page 结构。

漏洞分析

关于io_uring的一些基础知识之前的文章已经详细介绍过,如果师傅们感兴趣可以看看之前的文章。接下来只介绍漏洞相关的点。

NVD描述:在 Linux 内核中io_uring的固定缓冲区注册代码(io_sqe_buffer_register io_uring/rsrc.c)中发现一个缺陷,该缺陷允许对缓冲区末尾以外的物理内存进行越界访问。此缺陷可实现完全本地权限提升。

io_uring_register 提供了接口 io_sqe_buffer_register 来注册一块 fixed_buffers 空间,这块区域会锁定,不能被交换,专门用来数据的读取。申请的大小和锁定地址都有用户来控制。

图片加载失败


io_sqe_buffers_register

「1」 首先就会使用 io_buffers_map_alloc上下文(ctx) 分配用户缓冲区数组,分配数组大小由用户控制。

「2」 然后遍历每个 ctx->user_bufs[] 来进行初始化操作,先复制用户参数到内核态的 kernel_iov ,用户控制的 usr_iov 主要就两个字段 {iov_base,iov_len} ,所以在申请缓冲区参数完全由用户控制。调用io_sqe_buffer_register传入iov,ctx->user_bufs[n] 进一步对每个页完成注册。

io_sqe_buffer_register

「1」 使用 io_pin_pages() 对用户传入 iov 锁定对应长度的物理页,作为io_uring的共享内存区域。

「2」 然后开始连续多个大页的优化,尝试合并页。 如果 nr_pages > 1,就开始合并页,这里的 nr_pages 取决于用户的 iov_len,比如说 iov_len = 50* PAGE_SIZE;,那在这里就有50个页的长度,在这 nr_pages == 50>1,就可以开始尝试合并。

「3」 漏洞点就在于此: 在循环判断当前页是否属于同一复合页的时候,只判断是否在同一复合页,没判断是否连续。在最开始介绍folio 时,就很明确说明 folio虚拟连续、物理连续的一块内存空间。那当我传入所有的 page 虽然虚拟地址连续,但都指向一个物理地址。当你在读写时kernel 认为你的读取的长度和地址都是连续的没问题,但实际都在一个固定的物理地址读取,长度却是整个复合页的长度。

「4」 继续跟进,如果满足复合页要求,会删除其他 page 的引用,只保留第一个(保留pages[0]),动态申请 struct io_mapped_ubuf *imu空间包含 nr_pagesbvec。调用 io_buffer_account_pin() ,锁定 pagesimu,然后把 iov 参数传递给imu。这里的 imu 通过指针传递给 &ctx->user_bufs[i],说人话就是这些操作都是对 user_bufs[i]进行操作。所以后续用户可以直接对这块区域进行操作。



网传EXP复现

网传EXP地址:ysanatomic/io_uring_LPE-CVE-2023-2598

这个师傅使用的手法是通过设置 sock 然后oob找到sock,然后劫持 protoCALL_USERMODEHELPER_EXEC来提权。

缺点也很明显,依赖于内核基地址,不通用,且伪造 subprocess_info 过于复杂,并且有崩溃情况。

我直接修改了sock->ops->set_rcvlowat.然后导致加入work队列报错。也就懒得继续修复了,拿到这个漏洞的时候我就在想物理地址越界读写,直接转换为dirty_pipe对pipe的flags进行篡改,然后对只读文件进行篡改,但是我都对文件进行篡改了,那就直接劫持filp。后面开始实验。

〔1〕 初始化:绑定CPU,注册io_uring,设置最大可打开文件数 (把rlim_cur设置为rlim_max),nr_memfds —— 映射漏洞物理页的文件最大打开数量。nr_sockets 最大打开 socket 数量。

〔2〕 创建receiver_fd,映射receiver_buffer内存(mmap()),用于存放数据(越界读取的数据和伪造的数据);

〔3〕 打开 nr_sockets 个 socket :

设置 sk_pacing_rate / sk_max_pacing_rate == 0xdeadbeefdeadbeef, 便于找到本sock

原作者使用sk_sndbuf ,设置sk->sk_sndbuf = max((fd+4608)*2, 4608), 通过sk->sk_sndbuf值可以识别其所属的fd。

但是在 sock 中有更简单的标识符 SO_RCVBUF ,设置接受缓冲区大小,在这里 fd 只需要除以2就能获取到。不用在原作者中过于复杂的转换。

图片加载失败


使用SO_RCVBUF,设置sk->rcvbuf= 0x20000+fd, 通过sk->rcvbuf值可以识别其所属的fd。

〔4〕 打开 nr_memfds 个匿名共享文件(memfd_create()),分配物理页(fallocate()),这一步为文件分配物理地址。这里的物理地址不是顺序的,这一步很重要。

〔5〕 接着为文件映射 虚拟内存,在固定地址处映射 65000 个连续的虚拟页(绑定该匿名文件),但是对应的物理页只有1个;并向 io_uring 注册该缓冲区。

〔6〕 越界读,定位到sock位置,泄露堆地址,内核基地址,然后伪造 subprocess_info 篡改sock.__sk_common.skc_protCALL_USERMODEHELPER_EXEC

图片加载失败




作者脏牛EXP

本质就是篡改filp->f_mode为可写,然后篡改/etc/passwd

〔1〕 初始化:绑定CPU,注册io_uring,设置最大可打开文件数 (把rlim_cur设置为rlim_max),nr_memfds —— 映射漏洞物理页的文件最大打开数量。nr_files 最大打开 file 数量。

〔2〕 创建receiver_fd,映射receiver_buffer内存(mmap()),用于存放数据(越界读取的数据和伪造的数据);

〔3〕 打开 nr_files/etc/passwd,因为通过 O_RDONLY 标识符打开,f_mode 固定为 0x484a801d

图片加载失败


〔4〕 接着为文件映射 虚拟内存,在固定地址处映射 65000 个连续的虚拟页(绑定该匿名文件),但是对应的物理页只有1个;并向 io_uring 注册该缓冲区。

nr_pages 来源于用户设置的 iov.len/page_size

图片加载失败


pages里面就只有一个地址,所以很容易就绕过了之前说的判断

图片加载失败


〔5〕 通过固定的 f_flags+f_mode == 0x484a801d00008000 定位到文件处,修改f_mode

图片加载失败


〔6〕因为无法知道文件描述符,所以暴力对所有/etc/passwd 进行写操作,通过返回值判断是否写入成功,但是实测非常稳定,只要有这个漏洞就能提权。


0 条评论
某人
表情
可输入 255