Challenge Overview
Category: Crypto
Flag: Alpaca{how_much_sugar_do_you_need}
We’re given a Python script that implements a Linear Congruential Generator (LCG) to produce pseudo-random numbers. The flag is encrypted using AES-CBC, where the key is derived from the LCG parameters (p, a, b, s). Our goal is to recover these secret parameters from the 4 consecutive outputs provided.
Given Files
chall.py - The challenge script:
class LCG: def __init__(self): self.p = getPrime(64) self.a = getRandomRange(1, self.p) self.b = getRandomRange(1, self.p) self.s = getRandomRange(1, self.p)
def serve_coffee(self): self.s = (self.a * self.s + self.b) % self.p return self.soutput.txt - The challenge output:
#1 order: 4052328936969804578#2 order: 8676271689691567645#3 order: 2647032430467963079#4 order: 6612596210231769351encrypted flag: 0c5355c5bb76b2a86aa7cf53279fb2350883865f2ca7423ff47512278a59a8db1ed85e82e0d84c2fec52e29d0b3aefd97d791f11edf18efdf1febc07ae860b8bSolution
Understanding the LCG
An LCG generates numbers using the recurrence relation:
Where:
p= modulus (64-bit prime)a= multiplierb= increments= current state
We have 4 consecutive outputs: s₁, s₂, s₃, s₄
Step 1: Recovering the Modulus p
The key insight is that consecutive differences have a multiplicative relationship:
Since:
This means:
Cross-multiplying:
Therefore:
Computing this value gives us a multiple of p:
t1 = s2 - s1 # 4623942752721763067t2 = s3 - s2 # -6029239259223604566t3 = s4 - s3 # 3965563779763806272
zero_val = t2 * t2 - t3 * t1# = 18015186145068426177274734295463492132Factoring this value:
18015186145068426177274734295463492132 = 2² × 9197623 × 37447334611 × 13076220846716751461The 64-bit prime factor is our modulus:
Step 2: Recovering the Multiplier a
Using the relationship :
a = (t2 * pow(t1, -1, p)) % p# a = 6534081916410610007Step 3: Recovering the Increment b
From :
b = (s2 - a * s1) % p# b = 12854780772490122855Step 4: Recovering the Initial State s₀
From :
s0 = ((s1 - b) * pow(a, -1, p)) % p# s0 = 12938961673315310206Step 5: Decrypting the Flag
With all parameters recovered, we can derive the AES key and decrypt:
params = (p, a, b, s0)# = (13076220846716751461, 6534081916410610007, 12854780772490122855, 12938961673315310206)
key = hashlib.sha256(str(params).encode()).digest()cipher = AES.new(key, AES.MODE_CBC, iv=enc_flag[:16])flag = unpad(cipher.decrypt(enc_flag[16:]), 16)Recovered Parameters
| Parameter | Value |
|---|---|
p | 13076220846716751461 |
a | 6534081916410610007 |
b | 12854780772490122855 |
s₀ | 12938961673315310206 |
Full Solver
import hashlibfrom Crypto.Cipher import AESfrom Crypto.Util.Padding import unpadimport sympy
def decrypt_flag(ct, params): key = hashlib.sha256(str(params).encode()).digest() cipher = AES.new(key, AES.MODE_CBC, iv=ct[:16]) return unpad(cipher.decrypt(ct[16:]), 16)
# Given outputss1 = 4052328936969804578s2 = 8676271689691567645s3 = 2647032430467963079s4 = 6612596210231769351enc_flag = bytes.fromhex("0c5355c5bb76b2a86aa7cf53279fb2350883865f2ca7423ff47512278a59a8db1ed85e82e0d84c2fec52e29d0b3aefd97d791f11edf18efdf1febc07ae860b8b")
# Step 1: Recover pt1, t2, t3 = s2 - s1, s3 - s2, s4 - s3zero_val = t2 * t2 - t3 * t1
factors = sympy.factorint(abs(zero_val))for factor in factors: if 63 <= factor.bit_length() <= 64 and sympy.isprime(factor): p = factor break
# Step 2-4: Recover a, b, s0a = (t2 * pow(t1, -1, p)) % pb = (s2 - a * s1) % ps0 = ((s1 - b) * pow(a, -1, p)) % p
# Step 5: Decryptflag = decrypt_flag(enc_flag, (p, a, b, s0))print(f"Flag: {flag.decode()}")Flag
Alpaca{how_much_sugar_do_you_need}