Photo by Victor Ballesteros on Unsplash

Hacking Blind in 2021

Posted on January 31, 2021

Disclaimer: This post is currently a draft focusing on high-level overview of the challenge. I’ll add more details later on.

This is a writeup for the Pinata challenge I prepared for JustCTF 2020 (moved to 2021 ;)). The challenge was based on the excellent Hacking Blind paper from 2014. My goal was to evaluate how it in works in 2021.

The challenge obviously does not include the binary and requires using the techniques from the paper also known as the blind ROP. Blind does not mean random guessing, there are some estimations to make however the challenge can be solved in a systematic way, incrementally learning more about the binary.

The target is an nginx compiled with a custom module written in C. The module has a stack buffer overflow vulnerability where the buffer can be controlled by an attacker. Worker’s request handler sets an alarm to timeout after 1 second to prevent infinite loops which would make the challenge extremely hard to solve.

Vulnerability discovery

The module performs basic authentication, after vising the page, it will prompt for a username and password.

Trying out some longer credentials will cause the worker process to crash due to overwriting the stack canary. The socket will be closed without sending any data. The proxy will return 502 in this case.

At this point it’s good to prepare a function which will reproduce the crash and will allow us to precisely control the payload:

from pwn import *
from base64 import b64encode

HOST = b"..."
PORT = 80

def request(payload):
    with remote(HOST, PORT) as s:
        req  = b"GET / HTTP/1.1\r\n"
        req += (b"Host: %b\r\n" % HOST)
        req += (b"Authorization: Basic %s\r\n" % b64encode(payload))
        req += b"\r\n"
        print(req)

        s.send(req)

        try:
            resp = s.recv(1024, timeout=5)
        except Exception as e:
            print(e)
            return None

        print(resp)

    if  b"502" in resp or resp == '':
        return None # crashed
    else:
        return resp

The exact buffer size can be determined manually by using the auth form though it’s more practical to automate that. We can start with a single byte and add one by one until we get the crash:

MAX_BUFFER_SIZE = 1024

def detect_buffer_size():
    for i in range(1, MAX_BUFFER_SIZE):
        if not request(b"A" * i):
            return i - 1
    sys.exit("couldn't detect buffer size")

The buffer holding decoded base64 data from the Authorization header is only 16 bytes. There is also another word on the stack after the buffer, that’s why it appears as buffer is larger (24 bytes). This doesn’t really influence the attack in any meaningful way. Here we can begin the stack reading phase which signals that the BROP environment exists.

Stack reading phase

The only part of the challenge which is randomized is the stack reading phase. In case of Pinata it’s stack canary and return address (7 + 5 bytes). Rest of the offsets can be hardcoded relative to the return address while incrementally exploring the binary for interesting places. I call it return address but stack reading can in fact return other addresses from the binary, in our case it’s also a stop gadget. The vulnerable function is compiled in a way that the word after canary doesn’t matter and can be all zero. This is convenient as the value is not requred for the task and it’s less requests to complete this step. So we get 3 words, first - canary, second - doesn’t matter, can be all zero, third - an address from the binary (stop gadget).

Stop Gadget

The challenge was specifically designed to encounter a “perfect” stop gadget very early (when going byte by byte ascendingly) while reading the return address form the stack. I call it perfect, because it’s a write straight to the socket without any additinal conditions/constraints. It’s not the original return address but that doesn’t matter. Good stop gadgets are hard to find and require a lot of trials, I wanted to spare this part.

Obviously, we don’t know that right away and might look for other addresses but it’s a strong sign and it’s worth to verify by trying to find some ROP gadgets by using it.

BROP gadget

You can use the stop gadget to find the BROP gadget. It’s good to collect a few to avoid false positives. Later, we can loop through candidates and based on the behavior we can hardcode it’s offset for the rest of the challenge. BROP gadget is used to control rdi and rsi registers (first two arguments).

Finding PLT

While finding the PLT is well described in the paper, there is a slight difference. The method of verifying that slow path does not crash at offsets +6 from a PLT entry does not work as Full RELRO is turned on, so we can’t rely on that. I guess it was not that common at the time of writing the paper. The pattern with just checking if a few subsequent PLT entries do not crash works well here, 3 should do the job. The binary is fairly large so skipping PLT entries is a must but it requires just one good hit. On the way, we will see that some addresses are executing (this is a side effect of verifying PLT address), we can use this information to estimate binary size and start from this offset next time as PLT is towards the beginning of the binary. Once we land on a promising address we can explore more from there, PLT is quite characteristic as subsequent entries do not crash and there is no other place like that in the program.

We could also grab here an nginx binary to do some estimations about the binary size and number of PLT entries which won’t be accurrate but can at give some perspective.

Finding strcmp

After we know an estimated address of PLT we can start to look for strcmp, by iterating over entries using the pattern is described in the paper. The trick with using the PLT slow path won’t work due to Full RELRO hence the iteration. There are a few candidates similarly to the BROP gadget so it’s good to collect them all and later pick the winner. The address of strcmp is required to control rdx (third parameter to write).

Finding write

To find write, we also iterate on PLT entries like with strcmp. The fd number is 3. It’s good to try all the strcmp candidates here. Eventually a leak from the binary will happen.

Dumping the binary

It’s good to adjust arguments to strcmp to maximize rdx which will give us more leaked bytes per request.

Finalizing the attack

Now we can perform a regular ROP attack by searching gadgets in the dumped binary and take control over the server. For example, load the /flag.txt contents into memory and write it to the socket or launch a reverse shell.