Trail of Bits CTF 1/5 Easy32
Trail of Bits released a number of CTF challeleges on Github.
This post is about the easy32
binary exploitation challenge.
The easy32
challenge is found in the ctf/exploits/binary1_workshop/easy/
directory. It contains a binary (easy32
), source code (EasyServer.c
) and a makefile.
From reading the source we can quickly see that it will create a socket and listen on port 12346 on all interfaces. When a client connects they are presented with a menu with 3 choices. All 3 choices lead to similar functions which each read 40 bytes from the socket into a 32 byte stack buffer. The objective of this challenge seems to be to exploit this buffer overflow to alter other variables on the stack which will cause the code to enter a conditional block it would not otherwise execute and set a global variable. When all 3 global varibales are set the key is written out to the socket and the challenge is complete.
Here is the beginning of the pokemans
function.
Before each 32 byte stack buffer is a 4 byte integer. Because of the way the stack grows downwards towards lower addresses, writing to byte 33 of of the buffer (buf[32]
or buf + 32
) will write into the 4 byte variable before it on the stack.
+-----------------------------+
| sym.pokemans stack frame |
+-----------------------------+
| 0x9000 (ebp) | ... |
| ... | ... |
| 0x8FF7 | pikachy + 3 |
| 0x8FF6 | pikachy + 2 |
| 0x8FF5 | pikachy + 1 |
| 0x8FF4 | pikachy + 0 |
| 0x8FF3 | buf + 31 |
| 0x8FF2 | buf + 30 |
| 0x8FF1 | buf + 29 |
| 0x8FF0 | buf + 28 |
| 0x8FEF | buf + 27 |
| 0x8FEE | buf + 26 |
| 0x8FED | buf + 25 |
| 0x8FEC | buf + 24 |
| 0x8FEB | buf + 23 |
| 0x8FEA | buf + 22 |
| 0x8FE9 | buf + 21 |
| 0x8FE8 | buf + 20 |
| 0x8FE7 | buf + 19 |
| 0x8FE6 | buf + 18 |
| 0x8FE5 | buf + 17 |
| 0x8FE4 | buf + 16 |
| 0x8FE3 | buf + 15 |
| 0x8FE2 | buf + 14 |
| 0x8FE1 | buf + 13 |
| 0x8FE0 | buf + 12 |
| 0x8FDF | buf + 11 |
| 0x8FDE | buf + 10 |
| 0x8FDD | buf + 09 |
| 0x8FDC | buf + 08 |
| 0x8FDB | buf + 07 |
| 0x8FDA | buf + 06 |
| 0x8FD9 | buf + 05 |
| 0x8FD8 | buf + 04 |
| 0x8FD7 | buf + 03 |
| 0x8FD6 | buf + 02 |
| 0x8FD5 | buf + 01 |
| 0x8FD4 (esp) | buf + 00 |
+-----------------------------+
The conditional block we want to enter checks the value of these variables against a hardcoded value.
We simply need to ensure that bytes 33-36 of the data our client sends to the server through the socket correspond to the hardcoded values (0xfa75beef
for pokemans
).
Sounds good in theory, but if things don’t work out right away we may quickly end up trying different variations of the input and generally floundering towards a solution which is not a productive strategy for more complex projects. Even though this project is trivial, lets use a debugger to help us understand what happens at runtime and guide us towards a solution in a decisive manner. Specifically we want r2 to breakpoint before the conditional block and print out the value of the local variable we are trying to overflow (pikachy
). That way we can get immediate feedback on how our client input affects the server.
I’m going to write this script in Ruby using the r2pipe
gem.
First we need to load the binary into r2 and then reopen it in debug mode (doo
)
The easy32 server works by listening for a new connection and then spawning a new thread. The new thread is the one which will read data from the client so we need to set our first breakpoint at a point on the main thread after the child thread has been created. The waitpid
function is a good candidate.
Next we need to find the pid of the child thread and attach the debugger to it. This may not be the most elegant code for achieving this.
Now we need to set a breakpoint on one of the compare instructions. We’ll choose number 1, the pokemans
function. It would be simpler to manually disassemble the function with r2 and then copy and paste the correct address into our script. However, this is a good opportunity to demonstrate how to use the r2 search functionality.
Finally we can print out the value of our (hopefully) overflowed local variable which has been stored in eax
.
Here is an example session with netcat.
† nc 127.0.0.1 12346
Do you want to be a?
1.) Pokemon Master
2.) Elite Hacker
3.) The Batman
1
So you want to be the best there ever was?
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabcde
Well then go away
And the output of out debugger script.
† ruby easy32_dbg.rb
[+] Waiting for connection on port 12346
[+] Attaching to child 3870
[+] Setting breakpoint on compare instruction at 0x80489d4
[+] Continuing child 3870
[+] Hit breakpoint! eax = 0x65646362
[+] Continuing child 3870
We can see that Endieness causes our input 0x62,0x63,0x64,0x65 to become 0x65646362 when interpreted as a 4 byte integer. From reading the server source code we can see that:
- the
pokemans
function wants0xfa75beef
- the
batmenss
function wants0x12345678
- the
hekers
function wants0xcafebabe
To write our exploit script we will drive an external netcat process using Ruby’s built in expect
functionality (note that mixing expect
and r2
pipe in the same script won’t work because they will fight for control over the target process’s IO.
From here we can get data out of netcat by calling read
or expect
on the master
object and give data to netcat by calling write
on the write
object.
And so on for the other 2 functions. Here is example output:
† ruby easy32_exp.rb
[+] Waiting for menu...
...ok
[+] selecting 1.) Pokemon Master
[+] Waiting for menu...
...ok
[+] selecting 2.) Elite Hacker
[+] Waiting for menu...
...ok
[+] selecting 3.) The Batman
[+] Trying to read key from socket...
Congratulations on your memory coruptions.
It only gets harder from here.
[+] kthxbai
Here is the full exploit script.
Here is the full r2 debugger script.