Description
An advanced guessing game with format string and buffer overflow vulnerabilities. Stack canary, NX, Full RELRO, no PIE.
Setup
Download the binary and libc from the challenge page.
Install pwntools: pip install pwntools
Solution
- Step 1Predict the random numberThe number is derived from rand() seeded by a value related to the program's runtime state. Mirror the rand() call locally with ctypes to obtain the correct guess before connecting.python3 -c " import ctypes, time libc = ctypes.CDLL('libc.so.6') libc.srand(int(time.time())) print(libc.rand()) "
Learn more
C's
rand()is a linear congruential generator -- a completely deterministic function of its seed. Seeding withtime(NULL)(seconds since the Unix epoch) means the entire output sequence is known to anyone who knows the current time. Python'sctypeslets you load the samelibc.so.6the server uses and invokerand()with an identical seed, producing byte-for-byte identical output.This is a more advanced variant of Guessing Game 1 -- the binary adds stack canaries, NX, and Full RELRO, so the PRNG prediction alone only gets you past the first gate. The real challenge is the two-stage binary exploitation that follows. Understanding that the PRNG is a prerequisite -- not the final step -- is the key insight that separates this challenge from its predecessor.
- Step 2Leak the stack canary via format stringThe binary passes user input directly to printf() with no format string. Use %<n>$p specifiers to walk the stack and find the canary value -- it typically appears at a fixed offset from the format string argument on the stack.python3 -c "print('%7$p %8$p %9$p %10$p %11$p %12$p')" | nc <host> <port>
Learn more
A format string vulnerability occurs when user-controlled data is passed as the first argument to
printf()(or similar) instead of being passed as a data argument. The%pspecifier tellsprintf()to read the next argument off the stack and print it as a pointer. The%n$psyntax targets the n-th argument directly, letting you read any stack slot without consuming earlier ones.A stack canary is a random value placed between the local variables and the saved return address at function entry. Before returning, the compiler emits a check: if the canary has changed, the program calls
__stack_chk_fail()and terminates. Leaking the canary via a format string bug lets you include the original value in your overflow payload, bypassing the check entirely.Canary values on Linux are 8 bytes with the least-significant byte always set to
0x00-- this null byte prevents the canary from being leaked by string functions that stop at null terminators. The format string approach sidesteps this because%preads raw stack memory as integers, not strings. - Step 3Overflow the buffer with the leaked canaryThe input buffer is 512 bytes. Build the payload: 512 bytes of padding + 8 bytes for the canary alignment + the leaked 8-byte canary + 8 bytes to restore the saved frame pointer + the ROP chain.python3 -c " from pwn import * payload = b'A'*512 + b'B'*8 + p64(leaked_canary) + b'C'*8 + rop_chain "
Learn more
Stack layout (low address to high) after the buffer: the 512-byte buffer, an 8-byte alignment pad, the 8-byte canary, the saved rbp (frame pointer), and finally the saved rip (return address). Your payload must fill all slots in order to reach and overwrite rip without disturbing the canary value.
p64()packs a 64-bit integer into 8 bytes in little-endian order -- the byte order x86-64 uses. Getting this packing wrong is one of the most common bugs in exploit development: an address like0x00007f1234560078must be sent as exactly those 8 bytes in reversed byte order.The fact that you need a precise leaked canary means this exploit is inherently a two-connection attack: one connection to leak the canary value, then a second connection (or a continuation of the same) to send the overflow. This pattern is called a two-stage exploit and is common in CTF challenges that combine information-leak and code-execution primitives.
- Step 4Leak puts GOT and compute libc baseUse a ROP gadget to call puts(puts@got) -- this prints the runtime address of puts in libc. Subtract puts' known offset to get the libc base address.python3 -c " from pwn import * libc_base = leaked_puts - libc.sym['puts'] system = libc_base + libc.sym['system'] bin_sh = libc_base + next(libc.search(b'/bin/sh')) "
Learn more
ASLR (Address Space Layout Randomization) randomizes where libc is mapped in memory each run, so hardcoded libc addresses in a ROP chain are wrong most of the time. The standard bypass is a GOT leak: the Global Offset Table holds the actual runtime addresses of imported functions. Calling
puts(puts@got)via ROP causes the program to print the 8-byte address stored in the GOT entry forputs.Because the relative offsets between symbols inside a given libc build are fixed, knowing any one symbol's runtime address lets you compute all others. Subtracting the known static offset of
putsfrom its leaked address gives the libc base, from which you can reachsystem(),/bin/sh, or any other symbol in that libc.The challenge provides a specific libc binary for exactly this reason -- different libc builds have different internal offsets. The Full RELRO protection prevents overwriting GOT entries but does not prevent reading them, which is all this leak step requires.
- Step 5Execute ret2libc for a shellWith libc base known, call system('/bin/sh') via a ROP chain: ret gadget (for stack alignment) + pop rdi; ret + address of '/bin/sh' + address of system().python3 exploit.py
Learn more
ret2libc is the canonical technique for exploiting a buffer overflow when NX is enabled. Instead of injecting shellcode, you call
system()-- a function already present in libc -- with"/bin/sh"as its argument. The string/bin/shis already embedded in libc itself (glibc uses it internally forpopen()), so you only need its address.The
pop rdi; retgadget is critical on x86-64: the first function argument is passed in therdiregister per the System V ABI, not on the stack. The gadget pops your/bin/shaddress from the stack intordi, thenretadvances to thesystem()call. The extraretbefore everything aligns the stack to a 16-byte boundary -- required by some libc versions that use SSE instructions insystem().This complete exploit chain -- PRNG prediction, canary leak, GOT leak, ret2libc -- demonstrates why mitigations must be layered: each one alone is bypassable, but each also raises the bar substantially. Real-world hardened binaries combine all of these plus PIE,
FORTIFY_SOURCE, SafeStack, and shadow stacks to make chaining significantly harder.
Flag
picoCTF{...}
Two-stage exploit: the format string leaks the canary (bypassing stack protection), then the overflow redirects execution to system() via a ROP chain using a leaked libc base.