RSA vs RNG (rsa-vs-rng)
RSA vs RNG (rsa-vs-rng)
Platform: CUSTOM | Category: Crypto | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: gap_search, hensel_lifting, lcg_related_primes, quadratic_equation_mod_2k, rsa_decrypt
Summary
Walkthrough of the CUSTOM Crypto RSA vs RNG (rsa-vs-rng). Task: RSA where primes P,Q are produced by an LCG mod 2^512 (known A,B); get_prime skips composites so Q = A^k·P + C_k (mod 2^512) for a small unknown gap k. Solution: substitute into N=P·Q to get a q
Description
Source generates RSA primes from a linear congruential generator and encrypts the flag. Given
N,E, and the ciphertext (flag.enc), recover the flag.
python read-only • copy allowedMOD = 2**512 A = 2287734286...471189 # known LCG multiplier B = 4179258870...545010537 # known LCG increment class PRNG: def get_num(self): self.state = (A*self.state + B) % MOD; return self.state def get_prime(self): p = self.get_num() while not isPrime(p): p = self.get_num() # skip composites return p P = rng.get_prime(); Q = rng.get_prime(); N = P*Q; E = 0x10001
Analysis
The seed is secret, but A, B, MOD = 2^512 are known. Q is generated after P by some
number of LCG steps; because get_prime skips composites, Q is k steps after P for a
small unknown gap k (one step per candidate tried). The k-fold LCG iterate is affine:
Q = A^k · P + C_k (mod 2^512), C_k = B·(A^{k-1} + … + A + 1)
Substituting into N = P·Q:
N = A^k · P^2 + C_k · P (mod 2^512)
⟹ A^k · P^2 + C_k · P − N ≡ 0 (mod 2^512)
This is a quadratic in P modulo a power of two, one for each candidate gap k. Quadratics
mod 2^512 are solved cheaply by bit-by-bit Hensel lifting: fix P mod 2, mod 4, …, mod
2^512, keeping every partial root that still satisfies the congruence at the current precision.
For the correct k one of the recovered roots is the true P; verify by checking P | N over
the integers.
Solution
python read-only • copy alloweddef solve_quadratic_mod2(a, b, c, bits): # solve a x^2 + b x + c = 0 mod 2^bits sols = [0] for i in range(bits): m = 1 << (i+1); new = [] for x in sols: for bit in (0,1): xx = x | (bit << i) if (a*xx*xx + b*xx + c) % m == 0: new.append(xx) sols = list(set(new)) if not sols: return [] return sols Apow = 1; Csum = 0 for k in range(1, 6000): Apow = (Apow*A) % MOD # A^k Csum = (Csum*A + 1) % MOD # A^{k-1}+...+1 Ck = (B*Csum) % MOD # C_k for P in solve_quadratic_mod2(Apow, Ck, (-N) % MOD, 512): if P and N % P == 0: Q = N // P if isPrime(P) and isPrime(Q): d = inverse(E, (P-1)*(Q-1)) pt = pow(int.from_bytes(ct,'big'), d, N) print("k =", k, long_to_bytes(pt)); raise SystemExit
The correct gap is k = 180 (180 LCG outputs were composite and skipped between P and Q).
Once P and Q are known it is textbook RSA decryption.
Key Takeaways / Lessons
- Mastering gap_search, hensel_lifting, lcg_related_primes, quadratic_equation_mod_2k, rsa_decrypt was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-06-24 • CUSTOM • RSA vs RNG (rsa-vs-rng) • Difficulty: Medium