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

0x00nier

writeups/

xp/

whoami/

IrisCTF 2023 - ret2libm Write-up

Preliminary Analysis

Greetings everyone. Welcome to another write-up in our series covering the challenges from the Iris CTF 2023 event. Today, we will be discussing the ret2libm challenge, a unique challenge that involves using a buffer overflow attack to execute arbitrary code. The challenge prompt reads —

Unlike the first challenge we covered, I had to use Docker to make my exploit work remotely for this challenge. As the hint suggests, the server may have been acting up, but using Docker allowed me to test and debug my exploit in a matching environment. Credit goes to @playoff-rondo for reminding me to check the Dockerfile for clues on how to solve the challenge.

With that said, let’s take a look at what we get when we unpack the provided zip file —

Upon unpacking the provided zip file, we find the source code for the challenge and the compiled binary file chal. We are also given the libc and libm shared object files, which we can use to test our exploit against. In order to use these shared object files with the “chal” binary, we will need to do a couple of things. But before we get into that, let’s first take a look at the source code and run the binary to see how it behaves:

You might be wondering what the setvbuf function does in this code. The calls to setvbuf here turn off buffering for the standard input and output streams. This means that data is not stored and is sent directly to I/O. In some cases, this can help avoid implementation errors when doing socket I/O, but I am not completely sure.

In the next few lines, we see a classic buffer overflow vulnerability in the yours variable. We know that the gets function is vulnerable to this type of attack, and we can exploit that here. Additionally, we see that the address of the fabs function is printed out. This will be important to our approach, as we will see.

Let’s run the binary to see what happens:

The binary works as expected. It prints out an address as well and takes input for the yours variable.

Let’s run checksec on this.

When we run the binary, we see that several security features are enabled. Specifically, the binary has Full RELRO, which means that we cannot write to the Global Offset Table (GOT) or the Procedure Linkage Table (PLT) or any relocation sections. Additionally, the NX flag is enabled, which means that we cannot execute any code on the stack. This makes it difficult to use a traditional buffer overflow attack. The binary also has PIE enabled, which makes it even harder to exploit.

Despite these challenges, our goal is to try to overflow the buffer and gain control of the Instruction Pointer (RIP). Let’s see how we can do this.

In this step, we are setting a breakpoint at the gets function call in order to examine the state of the stack right after we fill it up with input.

To test our buffer overflow exploit, we are using a small Python 2 one-liner to send 16 'A' characters followed by a 64-bit representation of the value 0xdeadbeef (which is just '\xde\xad\xbe\xef' repeated twice). This input is intended to overflow the buffer and overwrite the return address of the function with the value 0xdeadbeef.

From the debugger output, we can see that the value of RBP+8 has been overwritten with our 64-bit representation of 0xdeadbeef. In a typical function call, this location in the stack is reserved for the return pointer after the function has finished executing and the stack frame has been destroyed. This means that our buffer overflow exploit was successful and we have control over the return address of the function.

As a result of our buffer overflow exploit, we have gained control of RIP. However, this is only the first step in our journey. In order to fully exploit the vulnerability, we still need to link the provided libc and libm shared object files to the binary itself. This will allow us to execute arbitrary code using functions from those libraries.

From the output, we can see that the chal binary is currently using the libc and libm shared object files from my Kali system. The linker is also using these system libraries. In order to link the binary to the libc and libm shared object files provided in the challenge, we will use the pwninit and patchelf tools. This will allow us to test our exploit against the correct versions of the libraries.

The first step is to create a symbolic link from libm.so.6 to the provided libm shared object file using the command shown above. The pwninit tool appears to handle the linking of libm automatically when you do this.

Once the symbolic link is created, we can simply run pwninit on the chal binary as usual to link it to the correct version of libc and libm.

Now that we’ve linked the chal binary to the correct shared object files, we can start working on the exploit. It’s possible that pwninit also handles the linking of any other shared libraries in the same directory when it creates the loader (ld-2.27.so). But I can’t say for sure.

Now that we have successfully linked the chal binary to the correct shared object files, we can begin working on the exploit.

Approach

Since we have control over RIP, we can execute a ret2libm attack (which is the name of this challenge). In case you’re not familiar with it, this is similar to a ret2libc attack, but with an extra step involved (technically we don’t return to libm). Essentially, we will use a libm function to jump to the base address of libc. From there, we can use a traditional ret2libc attack to execute arbitrary code.

To achieve this, we will use the Pwntools script provided in the previous message. If you want to learn more about the boilerplate script I used for this CTF, including the part that handles the proof-of-work requirement, check out my first writeup (linked below).

IrisCTF-2023 Babyseek

I shall only focus on the relevant details here.

The good news is that the binary itself gives us the address of the fabs function, which is located in the libm shared object file. We can use this address to calculate the base address of libm, and then jump to the base of libc from there.

We will use GDB and Pwntools to assist us in these steps. Let’s start by using GDB to see how we can reach the base of libm using the fabs address.

Steps to Exploit

As we step through the program in gdb-pwndbg, we see the following —

We can see that the address printed is that of the function fabsf64 in libm. We can retrieve this address at runtime using the pwntools function io.recvline() and process it accordingly.

To find the base address of libm, we can subtract the offset of fabsf64 from the address of fabsf64 that we retrieved at runtime. In this case, we would subtract 0x31cf0 to get the base address of libm.

How do we get to libc from the base of libm?

To get to libc from the base of libm, we can use the ldd command on chal_patched to see the difference between the addresses. This difference is always 0x400000. By subtracting 0x400000 from the base of libm, we can get the base of libc. From there, we can perform a classic ret2libc exploitation and call system on a pointer to the string /bin/sh. To get the necessary offsets, we can use the readelf command as shown below.

As the typical ROP chain for ret2libc for 64-bit binaries go, we overflow the buffer, do a nop;ret to avoid the movaps crash, pop the pointer to /bin/sh into RDI and call system and exit successively.

Our exploit works locally but not remotely. To find out why, we check the difference between libm and libc addresses on the remote endpoint using a docker container. We find that the difference is not 0x400000, as we expected, but rather 0x3f1000. In our exploit, we must adjust our calculations accordingly to account for this difference.

First, we build an image from the Docker file.

We run a container using the ret2libm image and mount the directory where our files are located. Then, we use the ldd command on the binary to see the difference in addresses between libm and libc. Using python, we can see that the difference is not 0x400000, but rather 0x3f1000. This means that in our exploit, we will have to adjust our calculations accordingly.

In this code snippet, we save the address of fabsf6r into a variable called ret. We subtract the offset of fabsf64 from libm and the offset of libc from libm and store the result in the same variable ret. Then we calculate the offsets for system, the pointer to /bin/sh, the POP RDI; RET gadget, the NOP; RET gadget, and exit. To get the address of /bin/sh, use the below command —

strings -a -t x libc.so.6 | grep "/bin/sh"

For the libc functions, you use readelf as usual. For the gadgets used, use the below commands.

ropper -f libc.so.6 --search "nop; ret"
ropper -f libc.so.6 --search "pop rdi"

With all the offsets found, we just pack it all in pwn.flat and use io.sendline() to send it to the remote endpoint.

As we can see, our exploit works well and we are able to get a working shell.

Thanks for reading :)))