CVE-2022-2602 内核提权详细分析
默文 历史精选 463浏览 · 2025-02-17 14:15

CVE-2022-2602 内核提权详细分析

漏洞简介

漏洞编号: CVE-2022-2602

影响版本:Linux Kernel < v6.0.3。v6.0.3已修复。

漏洞产品: linux kernel - io_uring & unix_gc

利用效果: 本地提权

环境搭建

复现环境:qemu + linux kernel v5.18.19

环境附件: mowenroot/Kernel

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

图片加载失败


漏洞原理

漏洞本质是 filpUAFio_uring 模块提供的 io_uring_register 系统调用中的 IORING_REGISTER_FILES 能注册文件。调用后会把文件放入 io_uring->sk->receive_queue 。在 Linux gc 垃圾回收机制中又会将该列表的文件取出,尝试会将文件取出并释放。导致下次使用 io_uring 利用该文件时会触发 UAF

漏洞技术点涉及 io_uring 、飞行计数、gc回收机制等,涉及的还是比较多的,下面详细分析。

参考链接:

【kernel exploit】CVE-2022-2602垃圾回收错误释放iouring的file导致UAF — bsauce

[漏洞分析] CVE-2022-2602 内核提权详细分析

io_uring, SCM_RIGHTS, and reference-count cycles(https://lwn.net/Articles/779472/)

The quantum state of Linux kernel garbage collection CVE-2021-0920 (Part I)(https://googleprojectzero.blogspot.com/2022/08/the-quantum-state-of-linux-kernel.html)

飞行计数

Linux 进程间通信是很重要的一个功能。Linux sock就允许建立一个双向socket来传递消息,通常会在两个进程中通信使用。SCM_RIGHTS 是 Unix 域套接字(Unix Domain Socket)中用于 进程间传递文件描述符的控制消息类型。其核心作用在于允许两个进程通过 IPC 机制共享已打开的文件、套接字或其他资源,而无需重复打开或复制数据。接下来主要针对 SCM_RIGHTS 方面分析。

但是传递总会有影响,比如我这边发送过去,对方还没接收到就关闭了文件。对方该怎么处理,不可能再去使用已经释放的资源。所以在文件准备发送的阶段,就会对文件引用计数 +1 。即使在对方没接收到就关闭文件,他的引用计数才不会为 0 而被释放。这样在socket 被关闭时,调用sock_release对未接受到的文件,使用 fput() 来减少引用计数,这样就可以正常使用文件。

在资源释放比较依赖于 socket 被关闭,但是我发送 socket fd 本身,这样就会造成无法释放。比如下图

图片加载失败


1、打开两个sock A、B,初始引用计数都是为 1 。

2、把 A 发送给 B ,B 发送给 A。两个引用计数都 +1 ,这时 ref_Aref_B2 。

3、关闭用户态的文件描述符,即关闭 A、B。引用计数都会 -1 ,这时 ref_Aref_B1。

4、用户态已经无法再次对 A、B 操作,也就无法调用sock_release,但是内核又不会主动对 A、B 操作,就导致了资源无法释放。

这样 Linux gc 垃圾回收机制就诞生了,内核会主动回收这种类似的资源,下面详细分析。先来了解发送的一个过程,再来分析回收机制。

发送SCM_RIGHTS

在用户态使用 socketpair(AF_UNIX,SOCK_DGRAM,0,s); 申请 SOCK_DGRAM类型的 socket 然后使用 sendmsg() 发送 SCM_RIGHTS类型的信息的时候,会调用unix_dgram_sendmsg()

「1」 首先会调用 scm_send 获取文件结构体 file ,初始化 scm_cookie 。这个 scm_cookie 涉及文件存放的列表,并且前面提到的准备发送阶段的文件引用计数增加,就在这个函数中实现。 相关结构体如下。

「2」初始化了相关文件,接着需要处理发送目的,other 为目标 socket,如果没指定地址就会通过 unix_peer_get()获取已连接的对端,比如在最开始的描述中,把 A 发送给 Bother 在这里就是为 B

「3」 接下来处理发送数据,把 scm_cookie 中的文件添加到 skb ,并且还会对文件引用计数与飞行计数增加操作等。

「4」 把当前 skb 挂载到对方的 sk_receive_queue (未接收列表),然后通知对方有数据要来,最后通过 scm_destroy 销毁局部变量 scm_cookie ,减少最开始增加的引用计数。需要注意的是 scm_destroy 减少的只是局部变量引用增加的,不是准备发送阶段增加的。

scm_send函数

约等于直接调用了__scm_send(),重点关注 SCM_RIGHTS ,在这个选择中调用 scm_fp_copy 来获取file结构体,初始化 scm_cookie->fp文件列表,并把 file 结构体放入文件数组( fp->fp )中。

scm_fp_copy

「1」 首先会对文件列表进行初始化,再尝试把文件放入文件数组。

「2」 会循环使用 fget_raw()从文件描述符中获取 file 结构体,过程中引用计数 +1 ,这里 +1 是前面提到的局部变量引用增加的,不是准备发送阶段增加的。在之后的 scm_destroy() 中会对应减少,到这里我们对 scm_cookie 有了更深的理解,无非就是两层结构,大致可看为 文件列表 -> 文件数组的关系。现在做的操作就是需要初始化文件列表,然后把要发送的文件存放进文件数组中。

unix_scm_to_skb

「1」 该函数主要工作是将 scm_cookie 添加到 skb,进行文件的发送工作。设置一些 cb 信息后调用 unix_attach_fds()

unix_attach_fds

「1」 到这里就是核心发送阶段了,先调用 scm_fp_dup() 对每个文件增加文件计数引用,在这里才是准备发送阶段增加的计数引用,之前获取 file 使用的 fget_raw()增加是临时的,代表 scm 正在引用。

「2」 然后调用 unix_inflight() 尝试添加飞行计数。

scm_fp_dup

unix_inflight

「1」 因为只有在发送 socket 自身的 fd 才会导致资源无法释放的问题,在普通文件引用的情况都会在 sock_release()中释放,所以在添加飞行引用计数只考虑 sock 类(socket和io_uring)。通过 unix_get_socket() 获取 sock,而这只有 socketio_uring 才能找到。普通文件返回 NULL 。接着会对 sock 类进行特殊处理。

unix_get_socket

「2」 如果存在则会 inc 自增u->inflight,前面提到飞行计数并不涉及普通文件,只针对 sock 类,所以当然的飞行计数不存放在 file ,而是在文件对应的 sock 。当第一次增加 inflight 时,添加到全局飞行列表 gc_inflight_list 。这点很关键,当使用 unix_gc()回收时就会在 gc_inflight_list找出符合的释放。

即如果传进来的是 socket 或者 io_uring会进行以下操作:

增加飞行计数——u->inflight++

添加到全局飞行队列 gc_inflight_list

全局飞行计数增加 unix_tot_inflight++

用户飞行计数增加 user->unix_inflight++



scm_destroy

「1」首先销毁cred,释放文件列表调用 __scm_destroy()

「2」遍历文件列表,使用 fput() 减少文件计数引用,抵消最开始 fget_raw() 增加的文件引用计数。

飞行计数小结

通过上面的分析,我们大致对发送 SCM_RIGHTS飞行计数 有了一个大致的了解。

文件初始阶段——在发送文件描述符时,会先通过 fd 获取 file 结构体,以数组列表形式挂载到局部变量 scm_cookie

文件准发阶段——再把局部变量 scm_cookie 中的文件列表添加到 skb 上。同时增加文件计数引用,防止未接受就关闭的风险。并增加飞行引用计数和挂载到全局飞行列表 gc_inflight_list,而飞行引用计数只有 sock 类(socket、io_uring)再会增加,普通文件不做处理。

文件发送阶段——添加各种附加信息,把当前 skb 挂载到对端未接受列表(sk_receive_queue),并通知进程有数据到达。

资源销毁阶段——因为 scm_cookie 为局部变量,并在第一阶段采用 fget_raw()增加文件计数引用,在这一阶段需要安全释放资源见减少文件引用计数然后函数返回。

unix_gc垃圾回收机制

垃圾回收的触发在一个 socket 被关闭时,主要通过 unix_gc()函数,调用栈:

图片加载失败


在了解了飞行计数的概念后,最开始的图完善成如下图。在发送 socket 自身 fd 后关闭,文件引用计数为 1 ,并且用户态再无法对文件描述符操作,因为文件引用计数不为 0 导致还是无法释放文件资源。但在增加了飞行计数引用后,对于 sock 类增加了飞行计数,这时A、B的 文件引用计数==飞机计数==1 ,并且对应的 skb 挂载在全局飞行列表 gc_inflight_list

unix_gc 回收本质就是遍历全局飞行列表 gc_inflight_list,查看是否有符合这种闭环条件,然后进行释放回收内存。

在分析 unix_gc 源码前,我们需要有一个清楚的认知就是导致 sock fd 无法释放的真正原因,是因为增加 ref 后,但对方又未接受到而被挂于对端的 sk_receive_queue 上所导致的闭环。你可以理解为unix_gc 就是在处理闭环(不准确,还有多持有僵死状态),而 unix_gc 机制核心在于对 gc_inflight_list->sock->sk_receive_queue 上 的文件检查并释放。

图片加载失败


unix_gc

gc_inflight_list全局飞行列表,挂载的是发送的 sock 文件。

sk_receive_queue未接受列表,存放的是 skbskb 中存放未接受文件的列表 fp

「1」首先遍历全局飞行列表 gc_inflight_list ,如果当 文件引用计数==飞行计数 就判定为“垃圾”,被放进备选垃圾队列gc_candidates ,如果两个引用计数都不相同就不满足闭环的条件。

「2」上面提及 unix_gc 机制核心在于对 sk_receive_queue的处理,unix_gc 就是在处理闭环(不准确,还有多持有僵死状态),而这里引用的 scan_children则是核心,有关scan_children函数建议师傅们先看过一遍,相关分析在下面。所以需要遍历备选垃圾队列gc_candidates,遍历队列每个 sock 中的 sk_receive_queue,进行飞行引用计数的减少闭环,这里减少的目的就在于下一步判断是否是闭环(文件循环发送)。

「3」接下来判断是否是闭环,遍历 gc_candidates 列表,判断飞行引用计数是否大于0,如果大于0就说明文件还在使用,为什么这么说呢。原因在于上一步调用 scan_children() 减少的飞行计数引用是 sk_receive_queue 中的,如果不是闭环或者多持有状态,那么就不会在 sk_receive_queue 找到该文件并减少飞行计数,简而言之文件还在使用。合法文件就添加到 not_cycle_list,并恢复飞行计数引用。

「4」 遍历 gc_candidates,把之前减少的飞行引用计数加回来,然后添加到 hitlist 以供之后恢复使用。

「5」 遍历 not_cycle_list,恢复合法文件重新添加回gc_inflight_list

「6」 调用__skb_queue_purge(&hitlist);进行释放操作。

scan_children

scan_children 直接调用 scan_inflight,所以直接放了 scan_inflight相关源码。

「1」在之前分析发送的过程中 skb 会被挂载到对端的sk_receive_queue上。所以这里从文件对应的 sock 中的 sk_receive_queue(未接收列表)取出 skb,而 skb里面有 文件列表。所以相当于最终遍历 sk_receive_queue中的每一个文件,如果遇到 sock 类文件,进行func(u) 操作(一般是进行飞行计数的加减)。

「2」 如果需要提取出来使用,就会把 skbsk_receive_queue 取出,然后添加到 hitlist

__skb_queue_purge

「1」对于传进来的列表,遍历其中的 skb 进行逐个释放。

io_uring原理

基础分析在上篇文章已经分析过,这里就只展开有关漏洞的部分

io_sqe_files_register注册文件

主要调用了一个关键函数 io_sqe_files_scm() 来进行绑定操作。

io_sqe_files_scm函数

该函数是 __io_sqe_files_scm()的封装,直接看__io_sqe_files_scm

__io_sqe_files_scm函数

「1」 根据传进来的 nr 文件数量进行遍历,使用 get_file() 获取 file 结构体,文件引用计数 +1,并添加到文件列表中。并对每个文件调用 unix_inflight 尝试对每个 sock 文件添加飞行计数。

「2」 如果有文件列表有文件,则会把文件列表挂载到 skb 上,然后将 skb 添加到 io_uring 自己的 sk_receive_queue。最后遍历文件调用 fput() 抵消最开始的文件引用计数增加。

在之前分析了 unix_inflightsk_receive_queue 再来看注册文件的一个过程,会显得有点奇怪,因为我们之前分析过sk_receive_queue 为对端未接受文件的列表,当时现在挂载到自己的 ctx->ring_sock->sk->sk_receive_queue 上,在这样操作之后也导致了非法释放 filp 的操作,比如现在 io_uring->sk_receive_queue = {s[1],fd}; s[1]->sk_receive_queue={io_uring_fd};这样造成的结果就是io_uring和s[1]完成闭环,释放资源的时候非法释放了fd

io_submit_sqe提交SQE

函数会先初始化IO请求,然后进行一些link的操作这里忽略,重点关注io_queue_sqe()会尝试执行。因为非法释放 filp ,我们可以采用 dirty cred 的手法往"/etc/passwd"中写入数据,而 io_uring 中有自带的写任务,所以我们跟一下 io_uring 如何完成IORING_OP_WRITEV 类型任务。

调用链如下

需要注意的是文件鉴权在 io_write()中的 io_rw_init_file() 中,而在之前是在 io_submit_sqe() 中就完成鉴权。在一定程度上缓减了 dirty cred

图片加载失败


漏洞复现

概况:在非法释放后,实际写之前,喷射大量 "/etc/passwd" 文件,覆盖由gc回收非法释放的file结构体,实际写恶意数据会到 "/etc/passwd" 文件。

「1」 使用 socketpair() 申请一对socket,文件描述符分别为s[0]s[1]。默认计数引用都为 1

「2」 调用 io_uring_setup(模式使用SQ轮询) ,初始化 io_uring ,文件描述符为io_fd ,引用计数为 1

「3」 打开一个普通可读写文件,比如 "/tmp/mowen" ,文件描述符为file_fd,初始状态引用计数为 1

「4」 调用 io_uring_register 中的注册文件模式 IORING_REGISTER_FILES ,来注册 s[1]file_fd。在注册的时候文件引用计数会 +1 。其中还会调用 unix_inflightsocks[1] 飞行引用计数 +1 。值得注意的是: 会将 file_fds[1] 放进同一个 skb

s[1]:引用计数 2 ,飞行引用计数 1

file_fd:引用计数 2 ,没有飞行计数。「4」——调试图,当前 skio_uring 自带的,和之前的io_sqe_files_register配合分析,当前io_uring自己的sk_receive_queue,一共有两个文件,skb 只有一个。
图片加载失败


「5」 关闭 file_fd ,这时 file_fd 引用计数为 1,在 skb 中。

「5」——调试图,这时filp并未被释放,仍然在 skb 中。

图片加载失败


通过f_op指针判断 0xffff8881018a7900 所在slub 为 file_fd的file结构体。

图片加载失败


图片加载失败




「6」 往 s[0],发送 io_fd,即通过 s[0]io_fd 发送给 s[1],发送过程会调用 unix_inflight ,因为 io_uring 属于 sock 类,故飞行计数会 +1 ,并且引用计数也会 +1

io_fd:引用计数 2 ,飞行引用计数 1

「7」依次关闭 s[0]、s[1] ,关闭后引用计数会 -1

s[0]:引用计数为 0 ,被释放。

s[1]:引用计数 1 ,飞行计数 1,暂时不会被释放。

「8」 使用线程占用文件 inode 锁——往 "/tmp/mowen" 中写入大量数据,占用 inode 文件锁,致使其他进程无法对该文件进行操作。

「9」 提交 io_uring 任务,使用 IORING_OP_WRITEV 提交 writev 任务,往 "/tmp/mowen" 中写入恶意数据(新root),因为 inode 锁的存在,这个任务会阻塞在——拷贝数据并检查权限后,实际写之前。

「10」 造成gc回收的非法释放,先关闭 io_uring ,调用 io_uring_queue_exit()关闭 io_fd ,过程中引用计数 -1

io_fd:引用计数 1 ,飞行引用计数 1 ,不会被释放。

「11」关闭一个 socket ,触发 unix_gc(),这个时候只要满足 引用计数==飞行计数 unix_gc就会从sk_receive_queue中取下并调用__skb_queue_purge释放对应的skb,其中会减少飞行引用计数并调用fput(),减少引用计数

io_fdskb 中只有自己,文件引用计数 -1,飞行计数 -1 ,这时引用计数 0 ,飞行引用计数 0 ,被释放没问题。

因为之前往s[1]中发送了 file_fd ,所以 io_fdsk->sk_receive_queue 中保包含了 s[1]、file_fd。s[1] 也被释放,但file_fd 在sock_release()中调用 fput(),对应 filp 也被释放 。但上面还有因阻塞而没有完成的任务,所以导致任务阻塞完毕之后尝试写入时访问了已经被释放的文件结构体,造成 UAF 。

图片加载失败




「12」大量的"/etc/passwd"文件,覆盖这个块内存 ,就可以写到"/etc/passwd"文件。

图片加载失败


exp


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

没有评论