← Writing

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:

Xn+1=(aXn+c)modmX_{n+1} = (a \cdot X_n + c) \bmod m

The constants (a,c,m)(a, c, m) vary by implementation:

ImplementationaaccmmOutput bitsPeriod
BSD rand()1 103 515 24512 3452312^{31}state >> 16 & 0x7fff (15 bits)2312^{31}
MSVC rand()214 0132 531 0112312^{31}state >> 16 & 0x7fff (15 bits)2312^{31}
glibc rand() TYPE_3trinomialfeedback2312^{31}state >> 1 (31 bits)2312^{31}
Java java.util.Random25 214 903 917112482^{48}state >> 16 (32 bits)2482^{48}
tr (PolarSSL)002642^{64}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

lcg-demo.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 2322^{32} possible sequences (fewer on most platforms, where srand() takes an unsigned int).

2. What Makes It Bad

LCGs have three fundamental problems:

  1. Short periodmm is usually 2312^{31} or 2482^{48}. After that many calls the sequence wraps.
  2. Low bits are less random — the high bits of XnX_n are "more random" than the low bits. The recurrence Xn+1=(aXn+c)modmX_{n+1} = (aX_n + c) \bmod m means bit kk of XnX_n has period at most 2k+12^{k+1}.
  3. Correlated outputs — successive values lie on hyperplanes in Rk\mathbb{R}^k. 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:

coin-flip.c
#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 mm and the constants interact.

The rand() % N bias

There's a subtler problem: when RAND_MAX + 1 doesn't divide NN evenly, some outputs are more probable than others.

P(k)={RN+1k<RmodNRNkRmodNP(k) = \begin{cases} \Big\lfloor \dfrac{R}{N} \Big\rfloor + 1 & k < R \bmod N \\ \Big\lfloor \dfrac{R}{N} \Big\rfloor & k \ge R \bmod N \end{cases}

where R = \text{RAND_MAX}.

NNR+1R + 1R+1modNR + 1 \bmod NBias
10032 768680–67 appear 328 times, 68–99 appear 327 times
232 7680None (power of two)
1 00032 7687680–767 appear 33 times, 768–999 appear 32 times

For N=100N = 100, 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. Use arc4random_uniform(N) or a proper rejection sampler.

3. The Crack — Step by Step

The BSD/glibc rand() returns:

output=(Xn16)  &  0x7fff\text{output} = (X_n \gg 16) \;\&\; \texttt{0x7fff}

We see bits 16–30 of XnX_n (15 bits). Bits 0–15 are hidden (16 bits). But aa, cc, and mm are public — they're in the source code of every libc. So given one output O0O_0, there are only 216=655362^{16} = 65\,536 possible internal states. Given a second output O1O_1, we can pinpoint the exact state.

crack-rand.py
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_val

The 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

demo.py
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]) # True

Running 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-bit pid). 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 requirementTypical difficulty
Observe 1–2 outputsTrivial
Identify the target LCGCheck libc version or known constants
Brute-force hidden bits2162^{16} for a 31-bit LCG
Brute-force 48-bit LCG state2322^{32} feasible with 2–3 outputs
Brute-force state from a single outputDepends on shift amount (usually 2162^{16}2202^{20})

5. What to Use Instead

SituationRecommendationWhy
C — general usearc4random() / arc4random_uniform()ChaCha20-based, reseeded from kernel
C — cryptogetrandom(2) (Linux 3.17+) or BCryptGenRandom (Windows)Entropy from kernel CSPRNG
C++<random>std::mt19937 + std::uniform_int_distributionMersenne Twister (NOT for crypto, but fine for sims)
Pythonrandom.SystemRandom or secrets moduleWraps /dev/urandom
Any language/dev/urandomKernel entropy — reseeded, unbounded
JScrypto.getRandomValues()CSPRNG-backed
Gocrypto/randOS entropy source

For simulations (Monte Carlo, games, testing): use std::mt19937 + std::uniform_int_distribution. It's fast, has a massive period (2199372^{19937}), and passes Dieharder tests.
For anything security-related (keys, tokens, passwords): never use rand(). 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 — use arc4random_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

ResourceWhy
RFC 4086 — Randomness Requirements for SecurityThe canonical guide to entropy in security
Dieharder battery of RNG testsThe standard statistical test suite
glibc rand() sourceSee 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.