M0lecon CTF Teaser 2024 ducts Writeup
This CTF was the kickoff of Shellphish Academy.
Now Shellphish Academy is a lot like DEFCON Academy (which @Zardus explains with such grace in this DEFCON talk). So for this CTF, we had a good number of blue-belt hackers from pwn.college play with Shellphish.
It was a fun experience, and I got to meet a bunch of super awesome hackers in the process too.
This challenge was pretty fun as well. So lets get into it.
Step 1: Reversing
The binary forks off a process to act as the backend
process. This backend
process reads input from a pipe and then performs either of two operations based on the type of input.
If the input is a message, it is stored into a linked list of messages. Nothing else happens when the backend
receives a message except for writing the content of the message to /dev/null
which is probably not relevant to the solution at all.
The other part of backend
is the command handling. And this command handler is the interesting part. There’s three commands that can be sent
- Flush messages
- Print messages
- Redact message
The first two are pretty self-explanatory. The only extra detail is that print_messages
also prints out some pointers which will be useful for a leak.
The redact_message
uses an index specified in the command to walk the linked-list and identify the correct message. It then uses an 8-byte value specified in the command to overwrite the content of the message.
This is the structure of the command and message that I identified.
The backend
process is only half of the whole binary. The other half is the “frontend” which is a fork-server.
The binary spins up a new fork to handle incoming connections to a specific port. This process then uses the talk
function to read messages from the user.
These messages are then sent over the pipe to the backend
after which the process exits.
Step 2: The Bug
I spent a few minutes trying to see if there was a buffer overflow in the talk
function. Initially it seemed like the input reading loop only terminated on newlines. I soon found that after the buffer limit was reached, the program would go into an infinite loop. No dice.
The other alternative was a race-condition since we could spawn multiple threads at the same time. However, the frontend was sending data to the backend
using a write
function call.
To the best of my limited kernel knowledge, the write
function is atomic. And after consulting with @kylebot and @zolutal, I was assured that this was the case.
But the size of the buffer that the talk
function used was absolutely massive (0x27100)! And after some trial and error, I found that inputs of the size 0x20000 are not written atomically. Which meant that we could cause some mischief.
I wrote up a script that created two connections to the port and sent two giant buffers at the same time. And I saw some output that confirmed that the two giant buffers were getting spliced together. Which meant that we could possibly put a fake command into a message content and have the backend
perform our command.
The backend
function would read 4 bytes from the pipe and check if the value read was 0 or 1. If the value read was neither, it would just read the next 4 bytes. This meant that even if the message was not split exactly at the fake chunk, it would be fine since the bytes before the fake chunk would simply be read and discarded.
I decided to put my fake chunks in the last page of the giant message to give it the highest probability of triggering the race-condition.
Step 3: The Exploit
So with the race-condition, I was able to get a bunch of fake commands executed by the backend
. This gave me PIE leak and a LIBC leak that worked 6/10 times.
But for the actual exploit, I confirmed with @x3ero0 that the redact_message
was our target. Recall that with the redact_message
function, we could overwrite 8 bytes of the content of a specific message in the linked-list with a value that we control. Combined with the fact that we can craft arbitrary messages, we basically have memory leaks and an 8-byte arbitrary overwrite.
In order to do this, I created a fake message that contained an arbitrary next
pointer. The next question was to identify a target to overwrite and a value to overwrite it with.
The binary only had partial RELRO and we could overwrite some of the GOT table pointers. However, the redact_message
would write the value 1 into the message type before overwriting the content. So we had to choose an address X
such that X-0x50
was writable and X
was a GOT function that would be called with controlled input.
I found that fwrite
was a good candidate that met these requirements. It was only called when a message was received and only used to write the content of the message into /dev/null
.
Eventually I got all my offsets proper and got the shell locally. And since ASU network was interfering with my connection to the challenge server, I had @ElChals run the script and get the flag.
The final exploit is available here: Exploit
Enjoy Reading This Article?
Here are some more articles you might like to read next: