Description
The developer replaced the dangerous input function with fgets(), but the fix is incomplete. Download vuln and vuln.c, then find the remaining path to the flag.
Setup
Download vuln and its source code.
Read the source to see how fgets() is misused.
cat vuln.cchmod +x vulnSolution
Walk me through it- Step 1Find the fgets() bugRead vuln.c - the developer switched from gets()/scanf() to fgets() but passed the wrong size argument. The result is a buffer read that is still larger than the actual buffer, creating a stack overflow.bash
cat vuln.cbashchecksec --file=./vulnLearn more
fgets(buf, size, stdin)is the safe replacement forgets()precisely because it accepts a size limit. But the safety guarantee only holds when the size argument accurately reflects the buffer's actual allocation. A common developer mistake is passing a size that is larger than the buffer - for example, writingfgets(buf, 256, stdin)whenbufwas declared aschar buf[64]. The result is thatfgetshappily reads up to 256 bytes into a 64-byte region, overflowing 192 bytes past the end.This is a false-fix pattern: the developer knows
gets()is dangerous and switches tofgets(), but copies the wrong size constant. It appears in real codebases regularly - particularly when refactoring legacy code where the buffer size and the read size were originally the same constant name but later diverged. Static analysis tools like-D_FORTIFY_SOURCE=2and AddressSanitizer catch this at runtime;clang-tidycan catch it statically.checksecshows which mitigations the binary was compiled with: stack canaries (SSP), non-executable stack (NX), ASLR/PIE, and RELRO. If the stack canary is absent, a straightforward overflow of the return address succeeds. If a canary is present, you need to first leak its value (via an information leak) before overwriting past it. - Step 2Find the offset to the return addressp.corefile only works if core dumps are enabled (ulimit -c unlimited). On x86-64, read 8 bytes from core.rip - not 4 from core.sp. If your shell blocks core dumps, fall back to GDB pattern create / pattern offset.bash
ulimit -c unlimited # enable core dumps in this shellpythonpython3 << 'EOF' from pwn import * context.binary = "./vuln" # picks up arch/bits from the ELF p = process("./vuln") p.sendline(cyclic(200, n=8)) # 8-byte chunks for x86-64 p.wait() core = p.corefile # x86-64: instruction pointer is in core.rip (8 bytes). # x86-32: use core.eip (4 bytes) and cyclic(..., n=4). offset = cyclic_find(p64(core.rip), n=8) log.info(f"Offset to saved RIP: {offset}") EOFbash# Fallback (no core dumps): gdb-peda> pattern create 200 ; r ; pattern offset $ripLearn more
A cyclic (De Bruijn) pattern is a sequence of bytes where every subsequence of length n appears exactly once. When this pattern overwrites the saved return address and the program crashes, the value in the instruction pointer register (
$ripon x86-64,$eipon x86-32) is a unique 4- or 8-byte substring of the pattern.cyclic_find()searches the pattern for that substring and returns its offset from the start, giving you the exact number of padding bytes needed.A core dump is produced when the process crashes with an unhandled signal (SIGSEGV). Pwntools'
Corefileclass parses the core and exposes register state, memory maps, and stack contents. Alternatively, GDB'spattern create/pattern offset(from the PEDA or GEF extension) achieves the same result interactively. Runningulimit -c unlimitedbefore the program ensures core dumps are written.On x86-64, the saved return address is 8 bytes wide and must be 8-byte aligned. The stack layout from the top of a function frame is typically: local variables, then the saved base pointer (rbp, 8 bytes), then the return address (8 bytes). So if the buffer is at offset 0 on the frame, the return address starts at
sizeof(buf) + 8. The cyclic approach confirms this empirically without needing to read the assembly manually. - Step 3Redirect execution to print_flag()Find print_flag's address, then overflow into the saved return address. If the program crashes inside a movaps instruction, the stack isn't 16-byte aligned at the call - prepend a single ret gadget to fix it.bash
objdump -d vuln | grep print_flagbashROPgadget --binary vuln | grep ': ret$' # find a bare 'ret' for alignmentpythonpython3 << 'EOF' from pwn import * e = ELF("./vuln") p = remote("<HOST>", <PORT_FROM_INSTANCE>) offset = 72 # adjust from your analysis print_flag = e.sym["print_flag"] ret_gadget = 0x40101a # any address holding a single 'ret'; adjust per-binary # If print_flag crashes in movaps, the SysV ABI 16-byte alignment is off. # Prepending a 'ret' realigns the stack by 8 before entering print_flag. payload = b"A" * offset payload += p64(ret_gadget) payload += p64(print_flag) p.sendline(payload) print(p.recvall(timeout=3)) EOFLearn more
A classic ret2win exploit overwrites the saved return address with the address of a "win" function that already exists in the binary - in this case
print_flag(). The payload is simply padding bytes (to fill the buffer and any gap up to the return address) followed by the 8-byte little-endian address of the target function. When the vulnerable function executes itsretinstruction, it pops the overwritten address off the stack and jumps there.p64(addr)in pwntools packs a 64-bit integer into 8 little-endian bytes, matching the byte order expected on x86-64 systems. The equivalent for 32-bit targets isp32(addr). Getting the endianness right is critical - submitting the address in big-endian order will jump to garbage memory and crash instead.This technique is the simplest form of control-flow hijacking. More advanced variants include ret2libc (jumping to
system()/bin/shinstead of a win function), ret2plt (calling a PLT stub to invoke library functions), and ROP (Return-Oriented Programming)which chains small code snippets ("gadgets") together to execute arbitrary logic even when no win function exists. This challenge is a clean introduction to the core concept before those complications arise.See Buffer Overflow Binary Exploitation and Pwntools for CTF for the broader workflow.
Flag
picoCTF{3ch0_3sc4p3_2_...}
The developer switched to fgets() but used the wrong size - the buffer still overflows. Find the offset with a cyclic pattern, locate print_flag() with objdump, and redirect the return address to it.
How to prevent this
How to prevent this
fgets() is safe only when the size matches the actual buffer. A wrong literal here is exactly as bad as gets().
- Use
sizeof(buf), never a hardcoded number.fgets(buf, sizeof(buf), stdin)is correct under any future buffer-size change;fgets(buf, 256, stdin)rots when someone shrinksbuf. - Build with
-fstack-protector-strong. The canary catches the overflow at function epilogue regardless of which API got the size wrong. - Add
-D_FORTIFY_SOURCE=3on glibc. It replacesfgetswith a fortified version that knows the actual__bos(buffer-object-size) and aborts on out-of-bounds writes when the destination size is known at compile time.