Description
Guess the correct number, then exploit a buffer overflow in the winner function to get a shell.
Setup
Download the binary and libc from the challenge page.
Install pwntools: pip install pwntools
Solution
- Step 1Predict the random numberThe 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 withsrand(time(NULL))is a classic example of a predictable PRNG becausetime(NULL)returns the number of seconds since the Unix epoch, a value that anyone can observe or predict.Python's
ctypeslibrary lets you call native C library functions directly. By loadinglibc.so.6and calling the samesrand()/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. - Step 2Identify the buffer overflowAfter 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_1python3 -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. - Step 3Build a ROP chain to make the stack executableUse 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 --roppython3 -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
retinstruction. By chaining gadgets -- eachretpops the next gadget address off the stack -- an attacker builds arbitrary computation without injecting a single new byte of code._dl_make_stack_executableis a glibc function that callsmprotect()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.
- Step 4Send the full exploit with pwntoolsCombine 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.