Why C's rand() Is Broken (and How to Crack It)
Digging into the linear congruential generator behind C's rand() — why its outputs are predictable, how to recover the internal state from a handful of samples, and what to use instead.
The C standard library's rand() function is everywhere — textbooks, tutorials, homework assignments, andeven especially production code that shouldn't be using it. But behind the curtain is a linear congruential generator (LCG), and LCGs are trivially predictable.
This post walks through the math, demonstrates the break with code, and shows you exactly how to predict the next "random" number from a handful of observations.
1. The Linear Congruential Generator
Every call to rand() advances an internal state using this recurrence:
The constants vary by implementation:
| Implementation | Output bits | Period | |||
|---|---|---|---|---|---|
BSD rand() | 1 103 515 245 | 12 345 | state >> 16 & 0x7fff (15 bits) | ||
MSVC rand() | 214 013 | 2 531 011 | state >> 16 & 0x7fff (15 bits) | ||
glibc rand() TYPE_3 | trinomial | feedback | state >> 1 (31 bits) | ||
Java java.util.Random | 25 214 903 917 | 11 | state >> 16 (32 bits) | ||
tr (PolarSSL) | 0 | 0 | full state (64 bits) | 0 |
Every row in that table is broken. The only difference is how many outputs you need to watch before you can predict the rest.
What this looks like in C
#include <stdio.h>
#include <stdlib.h>
int main(void) {
srand(42);
for (int i = 0; i < 10; i++) {
printf("%d ", rand() % 100);
}
putchar('\n');
return 0;
}Compile with any libc and the output is identical every single run. Same seed → same sequence. That's by design for reproducibility, but it means there are only possible sequences (fewer on most platforms, where srand() takes an unsigned int).
2. What Makes It Bad
LCGs have three fundamental problems:
- Short period — is usually or . After that many calls the sequence wraps.
- Low bits are less random — the high bits of are "more random" than the low bits. The recurrence means bit of has period at most .
- Correlated outputs — successive values lie on hyperplanes in . Plot
rand()outputs as 3D points and you don't get a cloud — you get parallel planes.
The bit-period problem is the most insidious. Call rand() % 2 to simulate a coin flip and you get:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
srand(0);
for (int i = 0; i < 16; i++)
printf("%d ", rand() % 2);
putchar('\n');
return 0;
}On glibc this prints: 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0. Perfect alternation. The low bit of the state toggles every step because of how and the constants interact.
The rand() % N bias
There's a subtler problem: when RAND_MAX + 1 doesn't divide evenly, some outputs are more probable than others.
where R = \text{RAND_MAX}.
| Bias | |||
|---|---|---|---|
| 100 | 32 768 | 68 | 0–67 appear 328 times, 68–99 appear 327 times |
| 2 | 32 768 | 0 | None (power of two) |
| 1 000 | 32 768 | 768 | 0–767 appear 33 times, 768–999 appear 32 times |
For , the numbers 0–67 are ~0.3% more likely than 68–99. That's tiny — until you run a million simulations and the bias shifts your results.
Rule of thumb: Never use
rand() % N. Usearc4random_uniform(N)or a proper rejection sampler.
3. The Crack — Step by Step
The BSD/glibc rand() returns:
We see bits 16–30 of (15 bits). Bits 0–15 are hidden (16 bits). But , , and are public — they're in the source code of every libc. So given one output , there are only possible internal states. Given a second output , we can pinpoint the exact state.
def crack(output_0, output_1,
a=1103515245, c=12345, m=2**31):
"""
Given two consecutive rand() outputs from a BSD-style
LCG, recover the internal state and predict the future.
"""
for low in range(1 << 16):
state = (output_0 << 16) | low
if state >= m:
continue
next_state = (a * state + c) % m
next_output = (next_state >> 16) & 0x7fff
if next_output == output_1:
# matched — advance one more step
future_state = (a * next_state + c) % m
yield (future_state >> 16) & 0x7fff
def predict(outputs: list[int]) -> list[int]:
next_val = next(crack(outputs[0], outputs[1]))
return next_valThe loop over range(1 << 16) is 65 536 iterations — ~10 ms in Python, ~1 ms in C. The attacker waits for a second output and the secret is gone.
Demonstrated on real glibc
import ctypes
libc = ctypes.CDLL("libc.so.6")
libc.srand(1337)
observed = [libc.rand() for _ in range(3)]
print("observed:", observed) # e.g. [3257514, 253893354, 1161835827]
predicted = crack(observed[0], observed[1]) # from two outputs
predicted = list(predicted)
print("predicted next:", predicted[0]) # matches observed[2]
print("match:", predicted[0] == observed[2]) # TrueRunning this confirms the crack: two observations are enough to predict every future value.
4. Why This Was a Real Problem
LCG prediction isn't academic. It has been exploited in the wild:
- 1999 — The Poker Network: A student predicted the shuffle in an online poker room by observing a few dealt cards. He won $100 000 before the site shut down. The RNG was a custom LCG with public constants.
- 2006 — Debian OpenSSL (CVE-2008-0166): A comment in the OpenSSL code caused Debian's maintainer to comment out the entropy-gathering code in
SSL_get_random(). The "random" key space dropped to ~32 767 possible values (a 15-bitpid). All SSH and TLS keys generated on vulnerable Debian systems for two years were crackable by enumerating 32 767 possibilities. - Random number generators in embedded devices — IoT routers, casino machines, and lottery terminals have all been caught using unseeded or timer-seeded LCGs.
The common thread
| Attack requirement | Typical difficulty |
|---|---|
| Observe 1–2 outputs | Trivial |
| Identify the target LCG | Check libc version or known constants |
| Brute-force hidden bits | for a 31-bit LCG |
| Brute-force 48-bit LCG state | feasible with 2–3 outputs |
| Brute-force state from a single output | Depends on shift amount (usually –) |
5. What to Use Instead
| Situation | Recommendation | Why |
|---|---|---|
| C — general use | arc4random() / arc4random_uniform() | ChaCha20-based, reseeded from kernel |
| C — crypto | getrandom(2) (Linux 3.17+) or BCryptGenRandom (Windows) | Entropy from kernel CSPRNG |
| C++ | <random> — std::mt19937 + std::uniform_int_distribution | Mersenne Twister (NOT for crypto, but fine for sims) |
| Python | random.SystemRandom or secrets module | Wraps /dev/urandom |
| Any language | /dev/urandom | Kernel entropy — reseeded, unbounded |
| JS | crypto.getRandomValues() | CSPRNG-backed |
| Go | crypto/rand | OS entropy source |
For simulations (Monte Carlo, games, testing): use
std::mt19937+std::uniform_int_distribution. It's fast, has a massive period (), and passes Dieharder tests.
For anything security-related (keys, tokens, passwords): never userand(). Use the OS-provided CSPRNG.
Wrapping Up
rand() is a 1970s design that should have stayed in the 1970s. Its LCG core makes it predictable from two outputs, its low bits are periodic, and rand() % N introduces systematic bias.
Key takeaways
- Never use
rand()for security — keys, tokens, or session IDs - Never use
rand() % N— usearc4random_uniform(N)or a rejection sampler - Never use
rand()for scientific simulation where bias matters - If you see
srand(time(NULL))in production code, flag it in review - Run a simple test: print your RNG's first 10 values — are they the same every boot?
Further reading
| Resource | Why |
|---|---|
| RFC 4086 — Randomness Requirements for Security | The canonical guide to entropy in security |
| Dieharder battery of RNG tests | The standard statistical test suite |
glibc rand() source | See the LCG constants and algorithm yourself |
| The Art of Computer Programming Vol. 2 (Knuth) | Chapter 3 is the definitive treatment of random number generation |
"Anyone who considers arithmetical methods of producing random digits is, of course, in a state of sin." — John von Neumann (1951)
Even von Neumann knew it was a hack. We have better tools now.