Rokos Replicant IrisCTF 2023 - babyseek Write-up | 0x00nier

0x00nier

writeups/

xp/

whoami/

IrisCTF 2023 - babyseek Write-up

Preliminary Analysis

Greetings everyone. I recently had the opportunity to participate in IrisCTF 2023 and was able to complete a total of 7 challenges. One of these challenges was called “babyseek,” and the prompt for it read as follows:

According to the prompt for the “babyseek” challenge, we are able to “seek around” a specific file. However, when we try to access this file, it allegedly leads to nowhere since we are at /dev/null. To complete the challenge, we were provided with both a remote endpoint and a zip file to work with. Upon unzipping the zip file, we discovered the following:

The “babyseek” challenge provided us with a binary file called “chal” as well as its accompanying source code. A Dockerfile was also included, which allowed us to test any exploits we developed. While working on this challenge, I found that I didn’t actually need to use the Dockerfile. However, it did come in handy later on when I was working on the “ret2libm” challenge, which I plan to write about in a separate post.

Now, let’s take a closer look at the source code itself.

According to the source code, this program first prints out the memory address of the win function. It then prompts the user for an integer input, which is used as a positional offset from the start of the /dev/null file. The program also prints out the current position of the null character. After that, it uses the fwrite function to write the value of the super_special pointer (which points to the address of the win function) to the /dev/null stream at the specified position.

We run the binary and the output is as expected.

On running checksec on the binary, we see that NX and PIE is enabled. There’s no RELRO either, so that means we can overwrite GOT entries very easily.

My Approach

To exploit this, I decided to input an offset that would overwrite the call to the exit function in the main function with the address of the win function. This would cause the program to execute the win function and print the contents of the /flag file when it finishes running, instead of exiting as intended.

To do this, we were given the addresses of both the win and null functions at their respective starting file offsets. Our goal was to use these addresses to somehow reach the address of the exit function.

To assist with this process, I used the pwntools library to attach gdb-pwndbg to a process running the chal binary. I will explain in more detail how I did this after I describe the steps I took to locate the address of the exit function.

As demonstrated in the attached screenshot, by using the flag GDB, it is possible to launch a debugging session with pwndbg that breaks at the scanf function (I will provide instructions on how to do this later).

For now, let’s turn our attention to the new terminal window that has been opened with GDB.

If you take a close look at the value stored in the RDX register (I stepped 3 instructions after the scanf function), you will notice that it represents our current position in the file stream of the null function. It’s important to keep this in mind as we continue.

By running the got command in pwndbg, it is possible to view all of the libc functions along with their corresponding addresses. The addresses that are colored white are of particular interest to us, as they are located in the got.plt section of the binary.

One way I attempted to reach the address of the exit function was to simply subtract its address from the value stored in the RDX register (which holds the address of the current file pointer). This would give me the offset to subtract from the current file pointer in order to reach the desired address. In other words:

curr + (curr-exit())

I initially tried hardcoding this offset, but it turns out that it changes with every run of the program.

As you can see, the distance between the current file pointer and the win function also changes with every runtime of the program.

However, this occurrence gave me an idea. If we subtract the address of the current file pointer from the address of the win function, we can calculate an offset that leads us from the null function to the win function.

As demonstrated above, by calculating the offset that leads us to the win function, we are able to reach it consistently in both runs of the program. And when we subtract the address of the win function from the address of the exit function in the GOT section, the resulting offset remains unchanged (8767 in this case).

Based on this information, we can construct a formula that will always allow us to reach the exit function. It is as follows:

exit = curr + [(win-curr)+8767] (or 0x223f)

The input we provide to the program will simply be [(win-curr)+8767] (or 0x223f), and this will allow us to overwrite the address of the exit function with the address of the win function.

With that said, let’s move on to constructing our pwntools script. I will also explain how I dealt with the Proof of Work (PoW) solver within the same script.

 

Step-by-Step Construction

This script is able to run the “chal” binary locally, with GDB enabled, and it is also able to execute the exploit remotely. However, the PoW solver isn’t required when running the program locally, so I had to create a separate Python file that excludes that code in order to test the exploit more efficiently.

In this section of the script, we import several basic libraries. The most important of these is pwn. If you are using a different terminal emulator (such as alacrity or terminator), you may also need to include the pwn.context.terminal line to ensure compatibility. Since terminator runs using Python 3, I had to hardcode this value. For more information, you can refer to the pwn library documentation.

We also define the start function, which is capable of taking various arguments such as GDB and REMOTE. When called without any arguments, the start function launches a local process. In this function, the gdbscript variable needs to be formatted in a specific way, as we will see below.

As you can see in the code snippet above, the gdbscript variable should be defined in this specific format in order to use the desired script. In this case, the init-pwndbg command enables the use of the pwndbg debugger instead of the default gdb debugger. This command was made possible through an installation of the necessary tools from this GitHub repository.

In this section of the script, we set the log level to debug, which is the highest possible level. This provides us with a significant amount of output on the terminal that can be helpful in identifying and troubleshooting issues with I/O. If you prefer to turn off this level of logging, you can use the “critical” log level instead.

The next line sets the current context’s binary to the chal file using the pwn.ELF function and saves the resulting ELF object into the elf variable. This can be useful in a variety of situations. For example, if the binary had PIE disabled, we could use the convenient functions provided by pwntools to directly retrieve the addresses of functions and linked libraries. However, in this case, the binary has PIE enabled, so this approach is not possible.

This code snippet automates the PoW solving here. We receive 3 lines of input using io.recvline().

We get a solver prompt in the next line that we need to execute. See below -

We make a script called solver.sh that runs solversc.sh.

The solver.sh script saves the output of solversc.sh into the solved file. In the pwntools script, we open this file and use the io.sendline() function to send the solution to the “Solution?” prompt.

After sending the solution, we receive output indicating whether the input is correct or not. To handle this output, we use another io.recvline() function. Once this step is complete, the actual program begins.

If you recall, when we ran the chal binary locally, the first two lines of output contained the addresses of the win function and the current file pointer.

To retrieve this information in the pwntools script, we use the io.recvline() function to receive the output from the program. We then decode the binary output using the decode() function and split the resulting string into separate pieces using the split() function. To obtain the exact address we need, we select the last element of the split string using the index [-1].

We repeat these steps to obtain both the address of the win function and the address of the current file pointer.

Once we have these values, we use the formula we constructed earlier to calculate the address we need to seek to in order to overwrite the exit function with the win function. We then use io.recvline() to receive the flag (since the win() function runs cat /flag).

Finally, we use the io.interactive() function to establish an interactive session with remote input/output (this function is particularly useful if we had run the /bin/sh command).

To use this script, we save it as a Python file and run it to obtain the flag, as shown in the example below.

That’s all for this writeup. I might release another writeup for the ret2libm challenge as well and others that I completed.

Thank you for reading :)))