Guessing Game 1

Published: April 2, 2026

Description

Guess the correct number, then exploit a buffer overflow in the winner function to get a shell.

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 binary seeds srand() with time(NULL) and calls rand() % 100 + 1. Because the seed is the current Unix timestamp, you can call the exact same sequence locally using Python's ctypes to mirror libc's rand() -- as long as you run it within the same second as the server.
    python3 -c " import ctypes, time libc = ctypes.CDLL('libc.so.6') libc.srand(int(time.time())) print(libc.rand() % 100 + 1) "
    Learn more

    Pseudo-random number generators (PRNGs) are not truly random -- they are deterministic algorithms that produce a sequence of numbers from an initial seed. C's rand() paired with srand(time(NULL)) is a classic example of a predictable PRNG because time(NULL) returns the number of seconds since the Unix epoch, a value that anyone can observe or predict.

    Python's ctypes library lets you call native C library functions directly. By loading libc.so.6 and calling the same srand()/rand() sequence with the same seed, you mirror the server's exact computation. The race is against the clock -- both calls must land in the same one-second window so the seed matches.

    In real-world security contexts, time-seeded PRNGs have caused serious vulnerabilities in session token generation, password reset links, and lottery software. Cryptographically secure PRNGs (/dev/urandom, arc4random, getrandom()) are seeded from entropy sources an attacker cannot observe, making prediction infeasible.

  2. Step 2Identify the buffer overflow
    After a correct guess, the program calls a winner() function that reads input into a 100-byte stack buffer with no bounds checking. The return address sits 120 bytes from the start of the buffer. NX is enabled, so the stack is not executable -- a ROP chain is required.
    checksec --file=./guessing_game_1
    python3 -c "from pwn import *; print(cyclic(200))" | ./guessing_game_1
    Learn more

    A stack buffer overflow occurs when a function writes more data into a stack-allocated buffer than it was sized to hold. The excess bytes overwrite adjacent stack memory, including the saved return address -- the value the CPU pops into the instruction pointer when the function returns. By controlling that value, an attacker redirects execution anywhere they choose.

    checksec (part of pwntools) reads ELF metadata and reports which mitigations are compiled in. NX (No-Execute), also called DEP on Windows, marks the stack and heap as non-executable at the hardware level, so injecting raw shellcode no longer works -- the CPU raises a fault before executing a single injected instruction.

    cyclic() generates a De Bruijn sequence -- a pattern where every N-byte substring is unique. When the program crashes, reading the value at the instruction pointer tells you exactly how many bytes of padding precede the return address, removing the need to count offsets by hand.

  3. Step 3Build a ROP chain to make the stack executable
    Use ROPgadget or pwntools ROP to locate _dl_make_stack_executable and the necessary gadgets. Call it to flip the NX bit, then return to shellcode on the stack.
    ROPgadget --binary ./guessing_game_1 --rop
    python3 -m pwn rop ./guessing_game_1
    Learn more

    Return-Oriented Programming (ROP) bypasses NX by repurposing existing executable code inside the binary and its libraries. A gadget is a short sequence of instructions ending in a ret instruction. By chaining gadgets -- each ret pops the next gadget address off the stack -- an attacker builds arbitrary computation without injecting a single new byte of code.

    _dl_make_stack_executable is a glibc function that calls mprotect() to add the executable bit back to the stack region. Calling it via ROP is an uncommon but valid technique for older binaries that link against glibc without full RELRO -- it effectively reverses NX for the stack, allowing a return to shellcode.

    Modern defenses against ROP include Control Flow Integrity (CFI), which validates that indirect branches only target expected addresses, and shadow stacks (Intel CET), which maintain a separate hardware-protected copy of return addresses. These make ROP significantly harder but not yet universally deployed.

  4. Step 4Send the full exploit with pwntools
    Combine the PRNG prediction with the ROP exploit in a pwntools script. Predict the number, send it, then send 120 bytes of padding followed by the ROP chain and shellcode.
    python3 exploit.py
    Learn more

    pwntools is the standard Python library for binary exploitation CTF challenges. It provides abstractions for network connections (remote()), process spawning (process()), ELF parsing, packing/unpacking integers to bytes (p64()/u64()), and an automated ROP chain builder that queries gadget databases for you.

    A complete exploit script ties together every prior step: predict the seed, open a connection, send the predicted number, wait for the overflow prompt, then send the crafted payload. The ability to automate this in a single script is essential because the PRNG prediction and the exploit transmission must both happen within the same second of wall-clock time.

    The broader exploit development workflow -- identify vulnerability, measure offsets, bypass mitigations, chain into a shell -- is the same pattern used in professional penetration testing and vulnerability research, just without the time-pressure CTF wrapper around it.

Flag

picoCTF{...}

PRNG seeded with time() is predictable -- synchronize your local rand() with the server's by using the same seed at the same second, then predict all outputs exactly.

More Binary Exploitation