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