Lesson 9.2: Recovering MT19937 State and Breaking LCG
Lesson 9.1 said MT19937 is unsafe because “with enough output you can predict the rest”. This lesson does exactly that, from start to finish. We invert the MT19937 temper step so that 624 consecutive outputs rebuild the full state, then clone the generator and predict every future number. After that we move to the LCG, a simpler generator, and recover all three secret parameters a, c and m from a sequence of outputs. All of it is working code, and none of it uses the victim’s seed.
Untemper turns 624 outputs back into the state, and the LCG parameters fall out of differences between outputs.
| Part 9 (Randomness) | Time: about 45 minutes | Difficulty: hard |
Prerequisites: Lesson 9.1 (PRNG, MT19937, seed). Lesson 1.1 (modular inverse, for the LCG part). Bit operations (shift, xor, mask).
Tools: Python 3 (random, math, functools from the standard library).
Goals
By the end of this lesson you understand the MT19937 temper step and why it is reversible. You can write an untemper function that inverts all four steps and rebuild the state from 624 outputs. You can clone the generator and predict the next number. You can recover the a, c and m of an LCG from its outputs. You can recognize challenge setups that give you enough output to recover the state.
1. Theory
How MT19937 produces a number: the temper step
MT19937 keeps 624 words of 32 bits as its state. To produce an output it takes one state word x and passes it through a chain of four operations called temper, which mixes the bits so the output looks more even:
1
2
3
4
5
6
y = x
y = y ^ (y >> 11)
y = y ^ ((y << 7) & 0x9D2C5680)
y = y ^ ((y << 15) & 0xEFC60000)
y = y ^ (y >> 18)
output = y
The key point for an attacker is that each of these operations is reversible. Every step has the form y = y ^ (y shifted and masked), and it can be inverted by repeating the same expression enough times. So from output we can compute back to x, the original state word. This inversion is called untemper.
Why y = y ^ (y >> s) is invertible
Take the right shift y = y ^ (y >> s). The top bits are not affected by a right shift of themselves, so we can recover them first and then use them to recover the lower bits step by step. A compact way in code is to iterate r = y ^ (r >> s) a few times until it converges. For 32 bits, 32 // s + 1 iterations is always enough. A masked left shift works the same way in the other direction: r = y ^ ((r << s) & mask).
The order of inversion must be the reverse of the temper order. Temper does >>11, <<7, <<15, >>18, so untemper does >>18, <<15, <<7, >>11.
From untemper to clone
With 624 consecutive outputs, we untemper each one and get 624 state words. We load them into a fresh MT19937 as its state and set the read index to 624, so that the next call triggers the twist step that refreshes the array, exactly as the original did after using all 624 words. From then on the clone runs in lockstep with the victim, and every future output matches. We do not need the seed, only enough output.
In Python the state of a random.Random is read and written with getstate/setstate, in the format (3, tuple of 624 state words + (index,), None).
LCG: simpler, and also broken
An LCG (linear congruential generator) produces numbers with the formula:
1
X_{n+1} = (a * X_n + c) mod m
where a (multiplier), c (increment) and m (modulus) are the parameters and X_0 is the seed. LCGs appear everywhere (glibc rand(), java.util.Random, and others). If the output is returned directly (no high bits cut off), we can recover all three parameters from a sequence of outputs, even knowing nothing in advance:
- Find
m. Lett_i = s_{i+1} - s_i. Then every expressionu_i = t_{i+1} * t_{i-1} - t_i^2is a multiple ofm(a short proof: expand with the LCG formula and every term is divisible bym). Take thegcdof a fewu_ito getm(it may be off by a small factor, but with enough samples it usually gives exactlym). - Find
a. Froms_2 - s_1 = a (s_1 - s_0) mod mwe geta = (s_2 - s_1) * (s_1 - s_0)^{-1} mod m, using the modular inverse from Lesson 1.1. - Find
c. Substitute:c = (s_1 - a * s_0) mod m.
With all three, we can predict every following number.
2. Demo
The script below has two parts. The MT19937 part takes 624 outputs from a generator (system seed, which we never touch), untempers them, clones the generator, then predicts the next five numbers and compares. The LCG part takes a sequence from an LCG with secret parameters, recovers a, c and m, and predicts the next number.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# mt_lcg.py: recover MT19937 from 624 outputs then predict, and break an LCG
import random
from math import gcd
from functools import reduce
# ---------- MT19937 ----------
def undo_rshift(y, s):
r = y
for _ in range(32 // s + 1):
r = y ^ (r >> s)
return r & 0xFFFFFFFF
def undo_lshift(y, s, mask):
r = y
for _ in range(32 // s + 1):
r = y ^ ((r << s) & mask)
return r & 0xFFFFFFFF
def untemper(y): # invert the 4 temper steps of MT19937
y = undo_rshift(y, 18)
y = undo_lshift(y, 15, 0xEFC60000)
y = undo_lshift(y, 7, 0x9D2C5680)
y = undo_rshift(y, 11)
return y
victim = random.Random() # system seed, which we do NOT know
outs = [victim.getrandbits(32) for _ in range(624)] # observe 624 outputs
state = tuple(untemper(o) for o in outs)
clone = random.Random()
clone.setstate((3, state + (624,), None)) # load the state back
pred = [clone.getrandbits(32) for _ in range(5)]
real = [victim.getrandbits(32) for _ in range(5)]
print("MT19937 predicted:", pred)
print("MT19937 real :", real)
print("match?", pred == real)
# ---------- LCG: X_{n+1} = (a*X_n + c) mod m ----------
def lcg(seed, a, c, m, n):
x = seed; out = []
for _ in range(n):
x = (a * x + c) % m; out.append(x)
return out
A, C, M, SEED = 1103515245, 12345, 2**31, 987654321 # secret
seq = lcg(SEED, A, C, M, 12) # we only have this sequence
diffs = [seq[i+1] - seq[i] for i in range(len(seq)-1)]
mults = [diffs[i+2]*diffs[i] - diffs[i+1]**2 for i in range(len(diffs)-2)]
m = reduce(gcd, [abs(x) for x in mults]) # m = gcd of the multiples of m
a = ((seq[2]-seq[1]) * pow(seq[1]-seq[0], -1, m)) % m # a from the ratio of differences
c = (seq[1] - a*seq[0]) % m # c follows
print()
print("LCG recover (a,c,m):", (a, c, m))
print("LCG real (a,c,m):", (A, C, M), "| match?", (a,c,m)==(A,C,M))
print("predicted next number:", (a*seq[-1]+c) % m, "| real:", lcg(SEED,A,C,M,13)[-1])
Running it gives this output (the MT part changes on every run because of the system seed, the LCG part is fixed because its parameters are fixed):
1
2
3
4
5
6
7
MT19937 predicted: [3669146054, 19416189, 2308367118, 2047324767, 3202878535]
MT19937 real : [3669146054, 19416189, 2308367118, 2047324767, 3202878535]
match? True
LCG recover (a,c,m): (1103515245, 12345, 2147483648)
LCG real (a,c,m): (1103515245, 12345, 2147483648) | match? True
predicted next number: 840622714 | real: 840622714
Reading the output: in the MT19937 part, the five predicted numbers match the five numbers the victim really produced, although we never knew the seed. 624 outputs are enough to rebuild the state. In the LCG part, we recover all three parameters correctly, a = 1103515245, c = 12345 and m = 2147483648 (these are glibc-style parameters), from only twelve numbers, and then predict the next number 840622714 correctly. The full lab with step-by-step comments and strict checks is in the Lab section below.
3. Lab
- Task: A service issues “lucky codes” using consecutive
random.getrandbits(32)calls. It shows you some codes (at least 624) through a public API, and the next code is the winning code. Predict the winning code. Variant: the service uses an LCG and only shows a few outputs. Recover the parameters and predict. - Files: a self-contained
solve.py(untemper,clone_mt,recover_lcg) andtranscript.txtwith real output. - Hints, step by step:
- Write
undo_rshiftandundo_lshiftfirst and test each one: tempering and then untempering must give back the original number. - Combine them into
untemper, and remember to use the reverse order. Untemper 624 outputs into 624 state words. - Load the state with
setstateand set the index to 624. Predict and compare with the real output in a test environment. - For the LCG: if the
gcdgives a multiple ofminstead ofm, collect more output so thegcdconverges. Be careful whenmis not a power of 2.
- Write
- Done when: you predict the next MT19937 number from 624 outputs, and recover the LCG parameters from a sequence of outputs.
4. Key takeaways
- The MT19937 temper has four steps, all reversible, so it can be untempered.
- 624 consecutive outputs are enough to rebuild the full state and clone the generator.
- Invert
y = y ^ (y >> s)by repeating the expression32 // s + 1times. - Untemper must run in the reverse order of temper.
- An LCG leaks
a,candmfrom its outputs:mthrough the gcd of multiples,athrough a ratio of differences,cby substitution. - Defense: use a CSPRNG. Never let someone collect enough raw PRNG output and then trust the next output.
5. Common pitfalls
- Inverting the temper steps in the wrong order. It must be fully reversed, with
>>18first and>>11last. A wrong order produces a garbage state. - Forgetting the
& 0xFFFFFFFFmask after each step, which lets numbers grow past 32 bits. Always cut to 32 bits. - Not iterating enough times when inverting a shift.
32 // s + 1is safe. Fewer iterations leave the low bits unconverged. - For an LCG, assuming the output is the state. Many LCGs drop the low or high bits before returning (for example
java.util.Randomreturns high bits). Then the simple gcd method fails and you need a lattice technique, which is harder. - A
gcdthat gives a multiple ofminstead ofm. Collect a few more outputs so the gcd converges, or try dividing out small factors. - Having fewer than 624 outputs for MT19937. Each missing word is a missing piece of the state, so you cannot clone. You need 624 consecutive outputs with no bits cut off.
- Outputs that are not
getrandbits(32)but have been processed (for example by modulo, or turned into floats). Then each output is no longer a whole state word, and recovery has to be done differently.
6. Further reading
- Cryptopals Set 3, Challenge 23 (Clone an MT19937 RNG from its output), the standard exercise for the MT part.
- Cryptopals Set 3, Challenge 22 (Crack an MT19937 seed), to practice brute-forcing a time-based seed.
- “Reconstructing truncated integer variables satisfying linear congruences” (Frieze et al.), for the case of an LCG with truncated bits, using lattices.
- Lesson 10.2 (Z3 and SAT/SMT), if you want to solve more complex PRNG constraints with a solver.
