本文主要参考 [漏洞分析] CVE-2022-2602 io_uring UAF内核提权详细解析 并做一些补充。
影响版本:Linux Kernel < v6.0.3。v6.0.3已修复。
测试版本:Linux-v6.0.2 (v6.0.2 测试失败,v5.18.19测试成功) exploit及测试环境下载地址—https://github.com/bsauce/kernel-exploit-factory
编译选项:
CONFIG_BINFMT_MISC=y (否则启动VM时报错)
在编译时将.config
中的CONFIG_E1000
和CONFIG_E1000E
,变更为=y。参考
$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v6.x/linux-6.0.2.tar.xz
$ tar -xvf linux-6.0.2.tar.xz
# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。
漏洞描述:io_uring组件中有个功能 IORING_REGISTER_FILES
,可以将文件放入 io_uring 的 sock->receive_queue
队列中。而Linux的垃圾回收机制GC(只处理 io_uring 和 sock 文件的飞行计数)可能会将io_uring中注册的文件当做垃圾释放,io_uring 下次使用该文件时(利用writev写文件,对应IORING_OP_WRITEV
功能)触发UAF。可通过userfaultfd触发该竞争漏洞。
补丁:patch unix_gc()
对于使用 io_uring_register
功能进行注册的文件,其生成的 skb 标记 scm_io_uring
位,加以区分。尽管 unix_gc()
还会对 io_uring
注册文件进行不可破循环检测,但是最后确定了垃圾队列 hitlist
之后,将所有通过 io_uring
注册的文件从 hitlist
移除。也就是说,unix_gc()
不会释放 io_uring
注册的文件,io_uring
自己会释放文件。
diff --git a/include/linux/skbuff.h b/include/linux/skbuff.h
index 9fcf534f2d927..7be5bb4c94b6d 100644
--- a/include/linux/skbuff.h
+++ b/include/linux/skbuff.h
@@ -803,6 +803,7 @@ typedef unsigned char *sk_buff_data_t;
* @csum_level: indicates the number of consecutive checksums found in
* the packet minus one that have been verified as
* CHECKSUM_UNNECESSARY (max 3)
+ * @scm_io_uring: SKB holds io_uring registered files
* @dst_pending_confirm: need to confirm neighbour
* @decrypted: Decrypted SKB
* @slow_gro: state present at GRO time, slower prepare step required
@@ -982,6 +983,7 @@ struct sk_buff {
#endif
__u8 slow_gro:1;
__u8 csum_not_inet:1;
+ __u8 scm_io_uring:1;
#ifdef CONFIG_NET_SCHED
__u16 tc_index; /* traffic control index */
diff --git a/io_uring/rsrc.c b/io_uring/rsrc.c
index 6f88ded0e7e56..012fdb04ec238 100644
--- a/io_uring/rsrc.c
+++ b/io_uring/rsrc.c
@@ -855,6 +855,7 @@ int __io_scm_file_account(struct io_ring_ctx *ctx, struct file *file)
UNIXCB(skb).fp = fpl;
skb->sk = sk;
+ skb->scm_io_uring = 1; // 对于使用 `io_uring_register` 功能进行注册的文件,其生成的 skb 标记 scm_io_uring 成员位
skb->destructor = unix_destruct_scm;
refcount_add(skb->truesize, &sk->sk_wmem_alloc);
}
diff --git a/net/unix/garbage.c b/net/unix/garbage.c
index d45d5366115a7..dc27635403932 100644
--- a/net/unix/garbage.c
+++ b/net/unix/garbage.c
@@ -204,6 +204,7 @@ void wait_for_unix_gc(void)
/* The external entry point: unix_gc() */
void unix_gc(void)
{
+ struct sk_buff *next_skb, *skb;
struct unix_sock *u;
struct unix_sock *next;
struct sk_buff_head hitlist;
@@ -297,11 +298,30 @@ void unix_gc(void)
spin_unlock(&unix_gc_lock);
+ /* We need io_uring to clean its registered files, ignore all io_uring
+ * originated skbs. It's fine as io_uring doesn't keep references to
+ * other io_uring instances and so killing all other files in the cycle
+ * will put all io_uring references forcing it to go through normal
+ * release.path eventually putting registered files.
+ */
+ skb_queue_walk_safe(&hitlist, skb, next_skb) {
+ if (skb->scm_io_uring) { // unix_gc() 不处理通过 `io_uring` 注册的文件
+ __skb_unlink(skb, &hitlist);
+ skb_queue_tail(&skb->sk->sk_receive_queue, skb);
+ }
+ }
+
/* Here we are. Hitlist is filled. Die. */
__skb_queue_purge(&hitlist);
spin_lock(&unix_gc_lock);
+ /* There could be io_uring registered files, just push them back to
+ * the inflight list
+ */
+ list_for_each_entry_safe(u, next, &gc_candidates, link)
+ list_move_tail(&u->link, &gc_inflight_list);
+
/* All candidates should have been detached by now. */
BUG_ON(!list_empty(&gc_candidates));
保护机制:KASLR/SMEP/SMAP/KPTI
利用总结:由于UNIX_GC垃圾回收机制会错误释放 io_uring
中还在使用的文件结构体(正在往"/tmp/rwA"
普通文件写入恶意数据),可以采用DirtyCred方法,打开大量"/etc/passwd"
文件,覆盖刚刚释放的file
结构体,这样最后就会实际往"/etc/passwd"
文件写入恶意数据。
本漏洞由@kiks 和 @LukeGix 共同编写EXP。
1. 漏洞分析
1-1. Linux 垃圾回收机制
关于Linux垃圾回收机制(SCM_RIGHTS
消息发送过程、接收过程、不可破循环的识别过程)可以参见文章-【漏洞分析】CVE-2021-0920 Linux内核垃圾回收机制中的竞争UAF漏洞,该文章是采用 SOCK_STREAM
socket(以发送函数 unix_stream_sendmsg() 为例来讲解SCM发送文件描述符的过程),本文采用 SOCK_DGRAM
socket (以发送函数 unix_dgram_sendmsg() 为例来讲解SCM发送文件描述符的过程)。
static int unix_dgram_sendmsg(struct socket *sock, struct msghdr *msg,
size_t len)
{
struct scm_cookie scm;
···
err = scm_send(sock, msg, &scm, false);// [1] 先调用`scm_send()`获取用户传入的文件描述符对应的文件,然后初始化 scm_cookie 结构(scm_cookie->fp->fp指向待传递的`file`结构列表),此时会增加文件引用计数,表示SCM正在处理该文件,最后在[3]处调用scm_destroy()减少文件引用计数。
···
skb = sock_alloc_send_pskb(sk, len - data_len, data_len,
msg->msg_flags & MSG_DONTWAIT, &err,
PAGE_ALLOC_COSTLY_ORDER);
if (skb == NULL)
goto out;
err = unix_scm_to_skb(&scm, skb, true);// [2] 调用 unix_scm_to_skb()->unix_attach_fds() 将 `scm_cookie->fp` 文件列表绑定到相应的skb对象,并增加文件引用计数(调用scm_fp_dup())和飞行计数(调用unix_inflight())
···
scm_destroy(&scm);// [3] 发送结束后释放scm,将刚刚初始化scm时增加的文件引用计数减少(调用fput())
···
skb_queue_tail(&other->sk_receive_queue, skb);// [4] 将skb添加到对端的接收队列
}
1-2. io_uring 原理
io_uring
具体原理可参考 【kernel exploit】CVE-2021-41073 内核类型混淆漏洞利用分析。
本文简单介绍下漏洞相关的 io_uring
功能。
(1)io_uring_setup()
io_uring_setup() 负责初始化io_uring的两个环形队列,然后为io_uring创建一个文件对象,将文件描述符返回给用户,用户可以使用这个文件描述符来映射出内存来访问两个队列和创建相关资源。重点注意,在 io_uring_setup() -> io_uring_create() -> io_uring_get_file() 中,初始化了一个sock结构体(之后通过io_uring
注册的文件会保存到这个 sk->receive_queue
队列中):
static struct file *io_uring_get_file(struct io_ring_ctx *ctx)
{
struct file *file;
#if defined(CONFIG_UNIX)
int ret;
ret = sock_create_kern(&init_net, PF_UNIX, SOCK_RAW, IPPROTO_IP,
&ctx->ring_sock); // 给 ctx->ring_sock 初始化一个sock结构体
if (ret)
return ERR_PTR(ret);
#endif
file = anon_inode_getfile("[io_uring]", &io_uring_fops, ctx,
O_RDWR | O_CLOEXEC); // 初始化一个file对象,其fd后续给用户使用
#if defined(CONFIG_UNIX)
if (IS_ERR(file)) {
sock_release(ctx->ring_sock);
ctx->ring_sock = NULL;
} else {
ctx->ring_sock->file = file;
}
#endif
return file;
}
重点注意,io_uring初始化完,还会同步存在一个io_uring的文件对象和sock对象,这在后面漏洞触发中很重要。如果初始化io_uring时带 IORING_SETUP_SQPOLL
flag的话,则会在 io_sq_offload_create() 中初始化一个内核线程轮询 io_uring 的任务队列,就不用我们主动调用 io_uring_enter()
通知 io_uring 了,这里不详细分析了。
(2)io_uring_register() - v5.18.19
io_uring
注册文件对应的是 IORING_REGISTER_FILES
功能,入口函数是 io_uring_register() ,该功能允许将若干文件描述符注册进入io_uring,方便后续的io操作。
SYSCALL_DEFINE4(io_uring_register, unsigned int, fd, unsigned int, opcode,
void __user *, arg, unsigned int, nr_args)
{
···
ret = __io_uring_register(ctx, opcode, arg, nr_args);
···
}
直接调用了 __io_uring_register(),在 __io_uring_register() 中会根据opcode类型调用到 io_sqe_files_register() 函数:
-
[1] 会遍历用户传递的所有需要注册的文件描述符,找到对应的
struct file
结构体,这其中会调用fget()
函数,该操作会对文件的引用次数+1; -
[2] 接下来会调用关键函数 io_sqe_files_scm() 对文件进行下一步注册: (v6.0.2 版本的内核有变化,实现该功能的函数是io_scm_file_account() )
static int io_sqe_files_register(struct io_ring_ctx *ctx, void __user *arg,//2602
unsigned nr_args, u64 __user *tags)
{
···
// [1] nr_user_files 用户绑定的文件数量,遍历所有用户传进来的文件描述符
for (i = 0; i < nr_args; i++, ctx->nr_user_files++) {
u64 tag = 0;
if ((tags && copy_from_user(&tag, &tags[i], sizeof(tag))) ||
copy_from_user(&fd, &fds[i], sizeof(fd))) {// [1] 获取用户传递的文件描述符fd
ret = -EFAULT;
goto out_fput;
}
···
file = fget(fd); // [1] 获取文件结构体,fget会对文件引用次数+1
···
if (file->f_op == &io_uring_fops) { // 不能io_uring 注册自己
fput(file);
goto out_fput;
}
ctx->file_data->tags[i] = tag;
io_fixed_file_set(io_fixed_file_slot(&ctx->file_table, i), file);
}
ret = io_sqe_files_scm(ctx); // [2] 关键函数,进一步逻辑
···
}
io_sqe_files_scm() 直接调用 __io_sqe_files_scm():
-
[1] sk是io_uring在初始化时候创建的struct sock结构
-
[2] 先申请一个skb(可以保存多个用户传入的
file
结构,存于skb->fp->fp
列表),这很重要;然后遍历所有文件,对他们使用get_file()
引用计数会+1;然后将这些文件使用unix_inflight()
发送,如果发送的文件是sock类型则会增加飞行计数并添加到全局飞行列表gc_inflight_list
,普通文件不受影响;最后将文件列表fpl赋值给skb->cb.fp
,并把新分配的skb添加到io_uring sock的接收队列(``sk->receive_queue -
[3] 最后处理完之后会对所有文件使用
fput()
,文件引用计数会-1,相当于平衡了最开始的fget()
。
static int __io_sqe_files_scm(struct io_ring_ctx *ctx, int nr, int offset)//ctx,thisfiles total
{
struct sock *sk = ctx->ring_sock->sk; // [1] sock 是最开始 io_uring 初始化的时候创建的
struct scm_fp_list *fpl;
struct sk_buff *skb;
int i, nr_files;
fpl = kzalloc(sizeof(*fpl), GFP_KERNEL);
···
skb = alloc_skb(0, GFP_KERNEL); // [2] 申请一个sk_buff,可以保存多个用户传入的 `file` 结构
···
skb->sk = sk; // skb->sk 指向 io_uring 的 sk
nr_files = 0;
fpl->user = get_uid(current_user());
for (i = 0; i < nr; i++) { // [2] 遍历所有文件
struct file *file = io_file_from_index(ctx, i + offset);// 获得文件结构体
if (!file)
continue;
fpl->fp[nr_files] = get_file(file); // [2] get_file同样会使file引用次数+1,把文件注册到fpl中
unix_inflight(fpl->user, fpl->fp[nr_files]); // [2] 把文件添加到发送队列,会增加sock类型文件的inflight飞行计数
nr_files++;
}
if (nr_files) {
fpl->max = SCM_MAX_FD;
fpl->count = nr_files;
UNIXCB(skb).fp = fpl; // [2] fpl 给skb
skb->destructor = unix_destruct_scm;
refcount_add(skb->truesize, &sk->sk_wmem_alloc);
skb_queue_head(&sk->sk_receive_queue, skb);// [2] 将新分配的skb添加到sk->sk_receive_queue中 !!!!!! 漏洞点 !!!!!!
for (i = 0; i < nr_files; i++)
fput(fpl->fp[i]); // [3] 对这些文件使用fput,平衡刚刚使用的get_file
} else {
kfree_skb(skb);
kfree(fpl);
}
return 0;
}
文章 分析过 unix_inflight()
和 sk_receive_queue
等,这里的代码比较奇怪,将注册到io_uring中的sock文件通过 unix_inflight()
增加飞行计数,这没有问题,但是后面又将注册文件添加到io_uring文件的 sk_receive_queue
队列中,sk_receive_queue
代表一个socket还未接收的消息队列,socket总是成对出现,而io_uring只有一个。(推测,这里加入到io_uring文件的 sk_receive_queue
队列中,表示还未被io_uring
处理过的文件,而io_uring
文件也有可能会被关闭,可以被看作是socket,也需要识别不可破循环以进行垃圾处理,这样便于UNIX_GC统一识别不可破循环)
(3)io_uring_register() - v6.0.2
不同点:负责进一步注册的函数有变化,从 io_sqe_files_scm() 变为 io_scm_file_account(),且在遍历用户传入file的循环内部调用了注册处理函数,后续不需要再遍历用户传入的file了。
io_uring_register() -> __io_uring_register() -> io_sqe_files_register()
- [1] 遍历用户传递的所有需要注册的文件描述符,找到对应的
file
结构体,其中会调用fget()
对文件引用次数+1; - [2] 接下来调用 io_scm_file_account() 函数对文件进行下一步注册。
int io_sqe_files_register(struct io_ring_ctx *ctx, void __user *arg,
unsigned nr_args, u64 __user *tags)
{
...
ret = io_rsrc_data_alloc(ctx, io_rsrc_file_put, tags, nr_args, // 读取 tags
&ctx->file_data);
...
for (i = 0; i < nr_args; i++, ctx->nr_user_files++) { // [1] 遍历 nr_args 个用户传进来的文件描述符
struct io_fixed_file *file_slot;
if (fds && copy_from_user(&fd, &fds[i], sizeof(fd))) { // 获取文件描述符 fd
ret = -EFAULT;
goto fail;
}
...
file = fget(fd); // 获取 file 结构体,fget对文件引用次数+1
...
if (io_is_uring_fops(file)) { // 判断 file->f_op == &io_uring_fops
fput(file);
goto fail;
}
ret = io_scm_file_account(ctx, file); // [2] 关键漏洞函数
if (ret) {
fput(file);
goto fail;
}
file_slot = io_fixed_file_slot(&ctx->file_table, i);
io_fixed_file_set(file_slot, file);
io_file_bitmap_set(&ctx->file_table, i);
}
...
}
io_scm_file_account() 直接调用 __io_scm_file_account():
-
[1] sk是io_uring在初始化时候创建的struct sock结构
-
[2] 先申请一个skb,或者从
io_uring
文件的sk->sk_receive_queue
中取出现有的skb,这很重要;然后遍历所有文件,对他们使用get_file()
引用计数会+1;然后将这些文件使用unix_inflight()
发送,如果发送的文件是sock类型则会增加飞行计数并添加到全局飞行列表gc_inflight_list
,普通文件不受影响;最后将文件列表fpl赋值给skb->cb.fp
,并把新分配的skb添加到io_uring sock的接收队列(``sk->receive_queue -
[3] 最后处理完之后会对所有文件使用
fput()
,文件引用计数会-1,相当于平衡了最开始的fget()
。
int __io_scm_file_account(struct io_ring_ctx *ctx, struct file *file)
{
#if defined(CONFIG_UNIX)
struct sock *sk = ctx->ring_sock->sk; // [1] sock 是最开始 io_uring 初始化的时候创建的
struct sk_buff_head *head = &sk->sk_receive_queue;
struct scm_fp_list *fpl;
struct sk_buff *skb;
// 看能否将本 file 整合到现有的 skb SCM_RIGHTS file 集合(sk->sk_receive_queue)中,如果没有则分配新的skb
spin_lock_irq(&head->lock);
skb = skb_peek(head);
if (skb && UNIXCB(skb).fp->count < SCM_MAX_FD)
__skb_unlink(skb, head);
else
skb = NULL;
spin_unlock_irq(&head->lock);
if (!skb) {
fpl = kzalloc(sizeof(*fpl), GFP_KERNEL);
if (!fpl)
return -ENOMEM;
skb = alloc_skb(0, GFP_KERNEL); // [2] 每注册一个文件,都会申请一个 sk_buff (skb)
fpl->user = get_uid(current_user());
fpl->max = SCM_MAX_FD;
fpl->count = 0;
UNIXCB(skb).fp = fpl;
skb->sk = sk; // skb->sk 指向 io_uring 的 sk
skb->destructor = unix_destruct_scm;
refcount_add(skb->truesize, &sk->sk_wmem_alloc);
}
fpl = UNIXCB(skb).fp;
fpl->fp[fpl->count++] = get_file(file); // 文件引用次数+1,将用户传入的文件加入到 sk->fp->fp 中
unix_inflight(fpl->user, file); // 把文件添加到发送队列,会增加sock类型文件的inflight飞行计数
skb_queue_head(head, skb); // 将新分配的skb添加到sk->sk_receive_queue中 !!!!!! 漏洞点 !!!!!!
fput(file); // [3] 文件引用次数-1
#endif
return 0;
}
(4)io_submit_sqe() - 提交读写任务
io_submit_sqe()函数是在我们向io_uring中提交任务之后(不管是不是 IORING_SETUP_SQPOLL
模式都会走到这里),io_uring准备完成这个sqe的任务的时候会触发,这里简单看一下io_uring如何完成 IORING_OP_WRITEV
类型任务,也就是writev写任务。
static int io_submit_sqe(struct io_ring_ctx *ctx, struct io_kiocb *req,
const struct io_uring_sqe *sqe)
{
struct io_submit_link *link = &ctx->submit_state.link;
int ret;
ret = io_init_req(ctx, req, sqe); // 初始化调用任务
···
ret = io_req_prep(req, sqe); // 准备调用任务,这里会进行文件权限的判断
···
} else {
io_queue_sqe(req); // 尝试执行
···
}
主要分为三步,在 io_init_req() 中进行初始化,然后调用 io_req_prep() 函数准备调用任务(注意,v5.18.19版本中是在io_init_req()函数末尾直接调用 io_req_prep()),对于writev任务会在这里先调用 io_write_prep()
进行文件权限校验:
static int io_write_prep(struct io_kiocb *req, const struct io_uring_sqe *sqe)
{
if (unlikely(!(req->file->f_mode & FMODE_WRITE)))
return -EBADF;
return io_prep_rw(req, sqe);
}
最后会在 io_queue_sqe() -> __io_queue_sqe() -> io_issue_sqe() -> io_write() 中完成实际写入工作。
所以虽然是writev任务,但并没有直接去调用系统调用writev相关内容,而且整个过程中也没有改变文件的引用计数。
注意:v5.18.19版本内核中是直接在 io_write() -> io_rw_init_file() 函数中进行文件权限校验。
static int io_write(struct io_kiocb *req, unsigned int issue_flags)
{
...
ret = io_rw_init_file(req, FMODE_WRITE);
...
}
static int io_rw_init_file(struct io_kiocb *req, fmode_t mode)
{
...
if (unlikely(!file || !(file->f_mode & mode))) // 检查文件写权限
return -EBADF;
...
}
1-3. 漏洞的触发
触发:重点关注相关文件的引用计数变化和飞行计数变化(只有发送 io_uring
和 sock类型的文件描述符才涉及飞行计数的变化)。
-
(1)准备一对 socket,文件描述符记为
s[0]
与s[1]
,准备好之后默认的引用计数均为1; -
(2)初始化io_uring,获取一个文件描述符记为fd,初始状态引用计数为1;
-
(3)打开一个普通可读写文件(文件描述符rfd[1]
"/tmp/rwA"
),初始状态引用计数为1; -
(4)使用
io_uring_register
的注册功能注册s[1]
和rfd[1]
(注册到了fd中,且s[1]
和rfd[1]
生成对应的skb保存到了fd的sk->sk_receive_queue
队列中),在这期间会将文件的引用计数+1,并且io_uring_register
中会对注册的文件调用unix_inflight()
函数,sock类型的s[1]
的飞行计数+1,并会将s[1]
和rfd[1]
放入同一个skb中;s[1]
:引用计数2;飞行计数1;rfd[1]
:引用计数2;不涉及飞行计数;
-
(5)关闭
rfd[1]
,引用计数-1,变为1(因为前面已经使用io_uring_register
注册到了fd中,rfd[1]
对应生成的skb保存到了fd的sk->sk_receive_queue
中,所以还存在引用计数1); -
(6)使用
s[0]
将fd发送给s[1]
,这期间会使 fd 的引用计数+1,并且由于发送过程中调用unix_inflight()
函数,io_uring类型的fd 同样会使飞行计数+1;- fd:引用计数2;飞行计数1;
-
(7)分别关闭s[0]和s[1],他们的引用计数都会减1;
s[0]
:引用计数0,被释放;s[1]
:引用计数1,飞行计数1,暂时不会被释放;
-
(8)先往
"/tmp/rwA"
文件写入大量数据,占据inode文件锁(参考Dirty Cred分析中的方法);再向fd(io_uring)
提交一个文件写(writev -IORING_OP_WRITEV
)的任务,往"/tmp/rwA"
文件写入恶意数据(新的root账户和密码),这个任务就会阻塞在文件权限检查之后,实际写之前; -
(9)调用
io_uring_queue_exit
关闭fd,fd的引用计数-1;- fd:引用计数1;飞行计数1,暂时不会被释放;
-
(10)创建一个socket,并且关闭,触发垃圾回收
unix_gc()
(在socket 关闭过程中触发); -
(11)
s[1]
和fd(io_uring)
都满足引用计数==飞行计数
的条件,并且**s[1]
和fd
都在对方的sk_receive_queue
** 中,属于不可破循环;unix_gc()
会将他们从对方的sk_receive_queue
中取下并且加入hitlist中调用__skb_queue_purge()
完成释放(释放的是skb),并且减少飞行计数;
-
(12)释放 skb 过程中会对 skb 中的 file 调用
fput()
;- fd所在的skb中只有fd自己,对
fd(io_uring)
使用fput()
,文件引用计数-1,变为0成功释放; s[1]
所在的skb中有s[1]
自己和rfd[1]
,对这两个文件使用fput()
;(fd的sk->sk_receive_queue
中保存的skb的sk->fp->fp
列表包含了s[1]
和rfd[1]
两个文件描述符);s[1]
经过fput()
之后引用计数变为0,被释放,没问题;rfd[1]
经过fput()
之后引用计数变为0,被释放,但上面的(8)
步还有因阻塞而没有完成的任务,所以导致任务阻塞完毕之后尝试写入时访问了已经被释放的文件结构体,造成UAF。
- fd所在的skb中只有fd自己,对
漏洞利用:在非法释放之后,文件写阻塞结束之前,使用堆喷射喷射其他 file
结构体(打开大量的"/etc/passwd"
文件)覆盖这个被释放的结构体所在内存,就可以写到"/etc/passwd"
文件了。
1-4. 漏洞修复
修复方法:在 unix_gc()
确定了垃圾队列 hitlist
之后,从 hitlist
队列中移除通过 io_uring
注册的文件,因为 io_uring
自己会处理释放文件。
问题:为什么不直接让 unix_gc()
不处理 io_uring
相关的飞行计数?因为从以上EXP示例可以看出,fd 是需要被释放清空的(属于不可破循环中的情况,socket s[1]
和 fd都已经被关闭,但是fd仍然位于 s[1]
的接收队列中),如果不处理 io_uring
相关的飞行计数,就不会将 fd 识别为垃圾。
2. 漏洞利用
原理:在 io_uring
执行IO任务之前,利用漏洞将文件释放掉,然后采用 DirtyCred 利用方法。
io_uring
虽然是无系统调用的IO操作,但本质上还是完成的对应系统调用的功能,比如可以给 io_uring
下发writev系统调用的任务,接着直接套用DirtyCred方法。
进程A | 进程B(启动比A慢) | |
---|---|---|
打开"/tmp/rwA" 文件,写入大量数据(0x80000 * 0x1000 字节); | ||
通过文件权限校验,并获取inode文件锁; | 打开"/tmp/rwA" 文件,尝试写入恶意数据(新的root账号和密码),提交写任务到io_uring; | |
长时间写入… | 通过文件权限校验,等待获取inode文件锁; | |
释放inode文件锁; | ||
触发漏洞,释放本file 结构体; | ||
打开大量"/etc/passwd" 文件,覆盖刚刚释放的file 结构体; | ||
获得inode文件锁,但实际会写入"/etc/passwd" 文件。 |
本漏洞和传统DirtyCred相关利用的漏洞不同的点在于,该漏洞的io_uring自带了写功能,并且逻辑和writev类似,无需自己再writev。
3. 测试结果
如果ssh连进去执行expoit时报格式错误,重传一遍exploit即可。v5.18.19版本的内核测试成功,但v6.0.2测试失败。
4. 常用命令
参考 CVE-2022-34918
liburing 安装:
# 安装 liburing 生成 liburing.a / liburing.so.2.2
$ make
$ sudo make install
常用命令:
# ssh连接与测试
$ ssh -p 10021 hi@localhost # password: lol
$ ./exploit
# 编译exp
$ make CFLAGS="-I /home/hi/lib/libnftnl-1.2.2/include"
$ gcc -static ./get_root.c -o ./get_root
$ gcc -no-pie -static -pthread ./exploit.c -o ./exploit
# scp 传文件
$ scp -P 10021 ./exploit hi@localhost:/home/hi # 传文件
$ scp -P 10021 hi@localhost:/home/hi/trace.txt ./ # 下载文件
$ scp -P 10021 ./exploit.c ./get_root.c ./exploit ./get_root hi@localhost:/home/hi
问题:原来的 ext4文件系统空间太小,很多包无法安装,现在换syzkaller中的 stretch.img
试试。
# 服务端添加用户
$ useradd hi && echo lol | passwd --stdin hi
# ssh连接
$ sudo chmod 0600 ./stretch.id_rsa
$ ssh -i stretch.id_rsa -p 10021 -o "StrictHostKeyChecking no" root@localhost
$ ssh -p 10021 hi@localhost
# 问题: Host key verification failed.
# 删除ip对应的相关rsa信息即可登录 $ sudo nano ~/.ssh/known_hosts
# https://blog.csdn.net/ouyang_peng/article/details/83115290
ftrace调试:注意,QEMU启动时需加上 no_hash_pointers
启动选项,否则打印出来的堆地址是hash之后的值。trace中只要用 %p
打印出来的数据都会被hash,所以可以修改 TP_printk() 处输出时的格式符,%p
-> %lx
。
# host端, 需具备root权限
cd /sys/kernel/debug/tracing
echo 1 > events/kmem/kmalloc/enable
echo 1 > events/kmem/kmalloc_node/enable
echo 1 > events/kmem/kfree/enable
# ssh 连进去执行 exploit
cat /sys/kernel/debug/tracing/trace > /home/hi/trace.txt
# 下载 trace
scp -P 10021 hi@localhost:/home/hi/trace.txt ./ # 下载文件
参考
-
【漏洞分析】CVE-2021-0920 Linux内核垃圾回收机制中的竞争UAF漏洞
-
CVE-2022-2602: DirtyCred File Exploitation applied on an io_uring UAF —— 英文博客1
-
DirtyCred Remastered: how to turn an UAF into Privilege Escalation —— 英文博客2:UAF 转化为DirtyCred
-
https://github.com/kiks7/CVE-2022-2602-Kernel-Exploit
-
https://github.com/LukeGix/CVE-2022-2602
-
https://seclists.org/oss-sec/2022/q4/57 —— 漏洞披露
-
[漏洞分析] CVE-2022-2602 io_uring UAF内核提权详细解析 —— 中文翻译
-
io_uring, SCM_RIGHTS, and reference-count cycles —— file引用计数知识
-
The quantum state of Linux kernel garbage collection CVE-2021-0920 (Part I) —— Linux垃圾回收知识
-
【bsauce读论文】2022-CCS-DirtyCred: Escalating Privilege in Linux Kernel
-
【kernel exploit】CVE-2021-41073 内核类型混淆漏洞利用分析 —— io_uring 相关漏洞