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.

struct {
int type;
int len;
message *next;
char name[0x40];
char content[];
} message;

struct {
int type;
int cmd;
long idx;
char content[8];
} command;

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:

  • M0lecon CTF 2023 NoRegVM Writeup
  • OCTF 2022 EZVM Writeup