Guessing Game 2

Published: April 2, 2026

Description

An advanced guessing game with format string and buffer overflow vulnerabilities. Stack canary, NX, Full RELRO, no PIE.

Remote

Download the binary and libc from the challenge page.

Install pwntools: pip install pwntools

pip install pwntools

Solution

  1. Step 1Predict the random number
    The 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 with time(NULL) (seconds since the Unix epoch) means the entire output sequence is known to anyone who knows the current time. Python's ctypes lets you load the same libc.so.6 the server uses and invoke rand() 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.

  2. Step 2Leak the stack canary via format string
    The 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 %p specifier tells printf() to read the next argument off the stack and print it as a pointer. The %n$p syntax 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 %p reads raw stack memory as integers, not strings.

  3. Step 3Overflow the buffer with the leaked canary
    The 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 like 0x00007f1234560078 must 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.

  4. Step 4Leak puts GOT and compute libc base
    Use 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 for puts.

    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 puts from its leaked address gives the libc base, from which you can reach system(), /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.

  5. Step 5Execute ret2libc for a shell
    With 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/sh is already embedded in libc itself (glibc uses it internally for popen()), so you only need its address.

    The pop rdi; ret gadget is critical on x86-64: the first function argument is passed in the rdi register per the System V ABI, not on the stack. The gadget pops your /bin/sh address from the stack into rdi, then ret advances to the system() call. The extra ret before everything aligns the stack to a 16-byte boundary -- required by some libc versions that use SSE instructions in system().

    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.

More Binary Exploitation