The problem gives us a bunch of consecutive leaked tokens generated by the function below. The goal is to find the next token in line. We can see the token generation is an LCG, meaning we can easily use the leaked tokens to recover the internal state (seed, state_1, state_2) of the generator.
seed = random.getrandbits(64 * 8)
mask = (1 << 64) - 1
state_1 = random.getrandbits(64)
state_2 = (state_1 + random.getrandbits(64)) & mask
def generate_token():
global seed, state_1, state_2, mask
result = seed & mask
seed >>= 64
inc = ((result * state_1 + state_2) ^ seed) & mask
seed |= inc << (7 * 64)
return result
Functionally, the seed is a queue of 8 64-bit integers (stored concatenated together with the low 64-bits being the top of the queue). Note that this means that we can already recover the seed (because the leaked tokens map 1 to 1 to these 64-bit integers).
Let state_1 be m
(multiplier), and state_2 be c
(constant increment). Let s_n
be the nth integer in the queue. On call to generate tokens, we calculate s_{n+8} = ((s_n * m + c) ^ s_{n+1}) & mask
, then add s_{n+8}
to the back of the queue (top 8 bytes of seed), and returning s_n
.
From this equation, we can calculate c
if we know m
. Note that and-ing by (1<<64)-1 is the same as modding by (1<<64).
s_{n+8} = (s_n * m + c) ^ s_{n+1} % (mask+1)
c = (s_{n+8} ^ s_{n+1}) - (s_n * m) % (mask+1)
Then, we just need to calculate m
. We can do so by constructing a system of equations to cancel out c
.
s_{n+8} = (s_n * m + c) ^ s_{n+1} % (mask+1)
s_{n+9} = (s_{n+1} * m + c) ^ s_{n+2} % (mask+1)
(s_{n+9} ^ s_{n+2}) - (s_{n+8} ^ s_{n+1}) = (s_{n+1} * m + c) - (s_n * m + c) % (mask+1)
= (s_{n+1} - s_n) * m % (mask+1)
m = (s_{n+9} ^ s_{n+2}) - (s_{n+8} ^ s_{n+1}) / (s_{n+1} - s_n) % (mask+1)
Division is done by calculating the multiplictive inverse in the above finite field.
def crack_unknown_increment(states, modulus, multiplier):
increment = ((states[8]^states[1]) - states[0]*multiplier) % modulus
return modulus, multiplier, increment
def crack_unknown_multiplier(states, modulus):
multiplier = (((states[9]^states[2]) - (states[8]^states[1])) * gmpy2.invert(states[1] - states[0], modulus)) % modulus
return crack_unknown_increment(states, modulus, multiplier)
Then, we can simply set the LCG PRNG state to the values we calculated and generate the new token.