# Inctf2021 pwn kqueue heap_overflow

# 环境以及保护

首先这个文件系统解压出来重新打包会出现问题,因为按照上一篇博客所讲的,创建 file_system 文件夹后,把文件系统的包丢进去,一旦更改后缀名称就是导致归档失败,无法使用 gunzip 进行解包。手动提取后,使用 find 打包,会导致我们启动的时候,报错我们没有挂载的权限。所以我没有重新打包。

提取出来的 vmlinux

dreamcat@ubuntu:~/Desktop/wykernel/inctf2021_kqueue$ checksec vmlinux
[*] '/home/dreamcat/Desktop/wykernel/inctf2021_kqueue/vmlinux'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0xffffffff81000000)
    RWX:      Has RWX segments

文件系统加载了一个 mod,就是 kqueue

dreamcat@ubuntu:~/Desktop/wykernel/inctf2021_kqueue/file_system/root$ checksec --file=kqueue.ko
[*] '/home/dreamcat/Desktop/wykernel/inctf2021_kqueue/file_system/root/kqueue.ko'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x0)

之开启了 canary 以及 NX 保护,(这里的 NX 就不允许我们执行用户态代码)

chall, 启动脚本

#!/bin/bash
exec qemu-system-x86_64 \
    -cpu kvm64 \
    -m 512 \
    -nographic \
    -kernel "bzImage" \
    -append "console=ttyS0 panic=-1 pti=off kaslr quiet" \
    -monitor /dev/null \
    -initrd "./rootfs.cpio" \
    -net user \
    -net nic

开启了 kaslr 地址随机化

下面就是我们对题目的逆向

# 前置知识

xlab&slub 分配

# 题目分析

题目加载了 kqueue 的模块,题目直接给出了源码

/* Generic header files */
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/mutex.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include "kqueue.h"
#pragma GCC push_options
#pragma GCC optimize ("O1")
static noinline long kqueue_ioctl(struct file *file, unsigned int cmd, unsigned long arg){
    long result;
    request_t request;
    
    mutex_lock(&operations_lock);
    if (copy_from_user((void *)&request, (void *)arg, sizeof(request_t))){
        err("[-] copy_from_user failed");
        goto ret;
    }
    switch(cmd){
        case CREATE_KQUEUE:
            result = create_kqueue(request);
            break;
        case DELETE_KQUEUE:
            result = delete_kqueue(request);
            break;
        case EDIT_KQUEUE:
            result = edit_kqueue(request);
            break;
        case SAVE:
            result = save_kqueue_entries(request);
            break;
        default:
            result = INVALID;
            break;
    }
ret: 
    mutex_unlock(&operations_lock);
    return result;
}
static noinline long create_kqueue(request_t request){
    long result = INVALID;
    if(queueCount > MAX_QUEUES)
        err("[-] Max queue count reached");
    /* You can't ask for 0 queues , how meaningless */
    if(request.max_entries<1)
        err("[-] kqueue entries should be greater than 0");
    /* Asking for too much is also not good */
    if(request.data_size>MAX_DATA_SIZE)
        err("[-] kqueue data size exceed");
    /* Initialize kqueue_entry structure */
    queue_entry *kqueue_entry;
    /* Check if multiplication of 2 64 bit integers results in overflow */
    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");
    /* Total size should not exceed a certain limit */
    if(queue_size>sizeof(queue) + 0x10000)
        err("[-] Max kqueue alloc limit reached");
    /* All checks done , now call kzalloc */
    queue *queue = validate((char *)kmalloc(queue_size,GFP_KERNEL));
    /* Main queue can also store data */
    queue->data = validate((char *)kmalloc(request.data_size,GFP_KERNEL));
    /* Fill the remaining queue structure */
    queue->data_size   = request.data_size;
    queue->max_entries = request.max_entries;
    queue->queue_size  = queue_size;
    /* Get to the place from where memory has to be handled */
    kqueue_entry = (queue_entry *)((uint64_t)(queue + (sizeof(queue)+1)/8));
    /* Allocate all kqueue entries */
    queue_entry* current_entry = kqueue_entry;
    queue_entry* prev_entry = current_entry;
    uint32_t i=1;
    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;
    }
    /* Find an appropriate slot in kqueues */
    uint32_t j = 0;
    for(j=0;j<MAX_QUEUES;j++){
        if(kqueues[j] == NULL)
            break;
    }
    if(j>MAX_QUEUES)
        err("[-] No kqueue slot left");
    /* Assign the newly created kqueue to the kqueues */
    kqueues[j] = queue;
    queueCount++;
    result = 0;
    return result;
}
static noinline long delete_kqueue(request_t request){
    /* Check for out of bounds requests */
    if(request.queue_idx>MAX_QUEUES)
        err("[-] Invalid idx");
    /* Check for existence of the request kqueue */
    queue *queue = kqueues[request.queue_idx];
    if(!queue)
        err("[-] Requested kqueue does not exist");
    
    kfree(queue);
    memset(queue,0,queue->queue_size);
    kqueues[request.queue_idx] = NULL;
    return 0;
}
static noinline long edit_kqueue(request_t request){
    /* Check the idx of the kqueue */
    if(request.queue_idx > MAX_QUEUES)
        err("[-] Invalid kqueue idx");
    /* Check if the kqueue exists at that idx */
    queue *queue = kqueues[request.queue_idx];
    if(!queue)
        err("[-] kqueue does not exist");
    /* Check the idx of the kqueue entry */
    if(request.entry_idx > queue->max_entries)
        err("[-] Invalid kqueue entry_idx");
    /* Get to the kqueue entry memory */
    queue_entry *kqueue_entry = (queue_entry *)(queue + (sizeof(queue)+1)/8);
    /* Check for the existence of the kqueue entry */
    exists = false;
    uint32_t i=1;
    for(i=1;i<queue->max_entries+1;i++){
        
        /* If kqueue entry found , do the necessary */
        if(kqueue_entry && request.data && queue->data_size){
            if(kqueue_entry->idx == request.entry_idx){
                validate(memcpy(kqueue_entry->data,request.data,queue->data_size));
                exists = true;
            }
        }
        kqueue_entry = kqueue_entry->next;
    }
    /* What if the idx is 0, it means we have to update the main kqueue's data */
    if(request.entry_idx==0 && kqueue_entry && request.data && queue->data_size){
        validate(memcpy(queue->data,request.data,queue->data_size));
        return 0;
    }
    if(!exists)
        return NOT_EXISTS;
    return 0;
} 
/* Now you have the option to safely preserve your precious kqueues */
static noinline long save_kqueue_entries(request_t request){
    /* Check for out of bounds queue_idx requests */
    if(request.queue_idx > MAX_QUEUES)
        err("[-] Invalid kqueue idx");
    /* Check if queue is already saved or not */
    if(isSaved[request.queue_idx]==true)
        err("[-] Queue already saved");
    queue *queue = validate(kqueues[request.queue_idx]);
    /* Check if number of requested entries exceed the existing entries */
    if(request.max_entries < 1 || request.max_entries > queue->max_entries)
        err("[-] Invalid entry count");
    /* Allocate memory for the kqueue to be saved */
    char *new_queue = validate((char *)kzalloc(queue->queue_size,GFP_KERNEL));
    /* Each saved entry can have its own size */
    if(request.data_size > queue->queue_size)
        err("[-] Entry size limit exceed");
    /* Copy main's queue's data */
    if(queue->data && request.data_size)
        validate(memcpy(new_queue,queue->data,request.data_size));
    else
        err("[-] Internal error");
    new_queue += queue->data_size;
    /* Get to the entries of the kqueue */
    queue_entry *kqueue_entry = (queue_entry *)(queue + (sizeof(queue)+1)/8);
    /* copy all possible kqueue entries */
    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;
    }
    /* Mark the queue as saved */
    isSaved[request.queue_idx] = true;
    return 0;
}
#pragma GCC pop_options
static int __init init_kqueue(void){
    mutex_init(&operations_lock);
    reg = misc_register(&kqueue_device);
    if(reg < 0){
        mutex_destroy(&operations_lock);
        err("[-] Failed to register kqueue");
    }
    return 0;
}
static void __exit exit_kqueue(void){
    misc_deregister(&kqueue_device);
}
module_init(init_kqueue);
module_exit(exit_kqueue);

代码比较场我们一点点分析

# kqueue_ioctl

static noinline long kqueue_ioctl(struct file *file, unsigned int cmd, unsigned long arg){
    long result;
    request_t request;
    
    mutex_lock(&operations_lock);
    if (copy_from_user((void *)&request, (void *)arg, sizeof(request_t))){
        err("[-] copy_from_user failed");
        goto ret;
    }
    switch(cmd){
        case CREATE_KQUEUE:
            result = create_kqueue(request);
            break;
        case DELETE_KQUEUE:
            result = delete_kqueue(request);
            break;
        case EDIT_KQUEUE:
            result = edit_kqueue(request);
            break;
        case SAVE:
            result = save_kqueue_entries(request);
            break;
        default:
            result = INVALID;
            break;
    }
ret: 
    mutex_unlock(&operations_lock);
    return result;
}

ioctl 函数,用于我们与设备的通信。首先会从用户态拷贝 request 的数据。一些重要结构体的定义如下

# 结构体

typedef struct{
    uint32_t max_entries;
    uint16_t data_size;
    uint16_t entry_idx;
    uint16_t queue_idx;
    char* data;
}request_t;
typedef struct{
    uint16_t data_size;
    uint64_t queue_size; /* This needs to handle larger numbers */
    uint32_t max_entries;
    uint16_t idx;
    char* data;
}queue;
typedef struct queue_entry queue_entry;
struct queue_entry{						// 队列的节点
    uint16_t idx;						// 节点的位置
    char *data;
    queue_entry *next;
};

然后就是根据我们输入的 cmd 进行操作,可以看到是我们熟悉的增删改操作。

# create_kqueue

static noinline long create_kqueue(request_t request){
    long result = INVALID;
    if(queueCount > MAX_QUEUES)												//MAX_QUEUES= 5
        err("[-] Max queue count reached");					// 限制创建的数目为 5
    /* You can't ask for 0 queues , how meaningless */
    if(request.max_entries<1)								// 我们创建的队列至少有一个节点
        err("[-] kqueue entries should be greater than 0");
    /* Asking for too much is also not good */
    if(request.data_size>MAX_DATA_SIZE)						// 限制 data 的数据大小为 0x20
        err("[-] kqueue data size exceed");
    /* Initialize kqueue_entry structure */
    queue_entry *kqueue_entry;								// 初始化节点指针
    /* Check if multiplication of 2 64 bit integers results in overflow */
    ull space = 0;
    if(__builtin_umulll_overflow(sizeof(queue_entry),(request.max_entries+1),&space) == true)
        err("[-] Integer overflow");
//Gcc 的内部函数,space= sizeof (queue_entry) * (request.max_entries+1);每一个 entry 可以理解为一个 node
    /* 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");
//Gcc 的内部函数,queue_size = sizeof (queue) + space
    /* Total size should not exceed a certain limit */
    if(queue_size>sizeof(queue) + 0x10000)
        err("[-] Max kqueue alloc limit reached");
    /* All checks done , now call kzalloc */
    queue *queue = validate((char *)kmalloc(queue_size,GFP_KERNEL));
// 创建队列,queue_size = sizeof (queue_entry) * (request.max_entries+1) + sizeof (queue);
    /* Main queue can also store data */
    queue->data = validate((char *)kmalloc(request.data_size,GFP_KERNEL));
    /* Fill the remaining queue structure */
    queue->data_size   = request.data_size;
    queue->max_entries = request.max_entries;
    queue->queue_size  = queue_size;
    /* Get to the place from where memory has to be handled */
    kqueue_entry = (queue_entry *)((uint64_t)(queue + (sizeof(queue)+1)/8));//(&queue+0x18) 指向的是第一个节点的位置
    /* Allocate all kqueue entries */
    queue_entry* current_entry = kqueue_entry;
    queue_entry* prev_entry = current_entry;
    uint32_t i=1;
    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;			//(queue_entry *)current_entry +=1,指向下一个结构体,所有的节点在 queue 空间是线性的,所以这个 queue 的空间其实就是 queue (队列头信息) + n*queue_entry,
        
        /* Populate next pointer of the previous entry */
        prev_entry->next = current_entry;					// 尾插法
        prev_entry = prev_entry->next;
    }
    /* Find an appropriate slot in kqueues */
    uint32_t j = 0;
    for(j=0;j<MAX_QUEUES;j++){							// 所有的队列形成一个数组 kqueues, 定义在头文件里
        if(kqueues[j] == NULL)
            break;
    }
//queue *kqueues[MAX_QUEUES] = {(queue *)NULL};
    if(j>MAX_QUEUES)
        err("[-] No kqueue slot left");
    /* Assign the newly created kqueue to the kqueues */
    kqueues[j] = queue;
    queueCount++;
    result = 0;
    return result;
}

整个创建新的 kqueue 的函数还是比较繁琐的,注释写道代码里

大概就是,我们创建一个队列,整个队列包含所有的节点结构体,每个结构体的 data 指向一个新的堆块。然后把这个队列的头指针放入一个全局的数组。

# delete_kqueue

static noinline long delete_kqueue(request_t request){
    /* Check for out of bounds requests */
    if(request.queue_idx>MAX_QUEUES)
        err("[-] Invalid idx");
    /* Check for existence of the request kqueue */
    queue *queue = kqueues[request.queue_idx];
    if(!queue)
        err("[-] Requested kqueue does not exist");
    
    kfree(queue);
    memset(queue,0,queue->queue_size);
    kqueues[request.queue_idx] = NULL;
    return 0;
}

这里会根据我们传入结构体的 queue_idx, 释放对应的 queue, 并且数据清空,不存在 uaf。

# edit_kqueue

static noinline long edit_kqueue(request_t request){
    /* Check the idx of the kqueue */
    if(request.queue_idx > MAX_QUEUES)
        err("[-] Invalid kqueue idx");
    /* Check if the kqueue exists at that idx */
    queue *queue = kqueues[request.queue_idx];
    if(!queue)
        err("[-] kqueue does not exist");
    /* Check the idx of the kqueue entry */
    if(request.entry_idx > queue->max_entries)
        err("[-] Invalid kqueue entry_idx");
// 根据 idx 进行定位,定位到节点,每个节点不会保留 data 的大小,
    /* Get to the kqueue entry memory */
    queue_entry *kqueue_entry = (queue_entry *)(queue + (sizeof(queue)+1)/8);// 第一个头节点
// 进行一个简单的遍历
    /* Check for the existence of the kqueue entry */
    exists = false;
    uint32_t i=1;
    for(i=1;i<queue->max_entries+1;i++){
        
        /* If kqueue entry found , do the necessary */
        if(kqueue_entry && request.data && queue->data_size){// 节点存在,data 指针不为空,
            if(kqueue_entry->idx == request.entry_idx){
                validate(memcpy(kqueue_entry->data,request.data,queue->data_size));
                exists = true;
            }
        }
        kqueue_entry = kqueue_entry->next;
    }
    /* What if the idx is 0, it means we have to update the main kqueue's data */
    if(request.entry_idx==0 && kqueue_entry && request.data && queue->data_size){
        validate(memcpy(queue->data,request.data,queue->data_size));//memcpy 返回的是 queue->data
        return 0;
    }
    if(!exists)
        return NOT_EXISTS;
    return 0;
}

这里要注意下头部也是可以保存信息的,但是目前还未看到漏洞。

# save_kqueue_entries

static noinline long save_kqueue_entries(request_t request){
    /* Check for out of bounds queue_idx requests */
    if(request.queue_idx > MAX_QUEUES)
        err("[-] Invalid kqueue idx");
    /* Check if queue is already saved or not */
    if(isSaved[request.queue_idx]==true)				//isSaved 一个 bool 类型的数组
        err("[-] Queue already saved");
    queue *queue = validate(kqueues[request.queue_idx]);	// 检查参数值是否为空的函数 validate
    /* Check if number of requested entries exceed the existing entries */
    if(request.max_entries < 1 || request.max_entries > queue->max_entries)
        err("[-] Invalid entry count");
    /* Allocate memory for the kqueue to be saved */
    char *new_queue = validate((char *)kzalloc(queue->queue_size,GFP_KERNEL));// 新开一个空间
    /* Each saved entry can have its own size */
    if(request.data_size > queue->queue_size)
        err("[-] Entry size limit exceed");
    /* Copy main's queue's data */
    if(queue->data && request.data_size)
        validate(memcpy(new_queue,queue->data,request.data_size));// 没有检查 size,
    else
        err("[-] Internal error");
    new_queue += queue->data_size;
    /* Get to the entries of the kqueue */
    queue_entry *kqueue_entry = (queue_entry *)(queue + (sizeof(queue)+1)/8);
    /* copy all possible kqueue entries */
    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;
    }
    /* Mark the queue as saved */
    isSaved[request.queue_idx] = true;
    return 0;
}