Real Number Generator
Author: 0xIDA
Platform: FlagYard · Category: crypto · Difficulty: Hard
Solvers
Summary
48-bit seed feeds a Decimal LCG-like map state = (e·state) mod π, then sqrt(|sin+cos|) packed as IEEE-754 doubles for a keystream. Known plaintext The flag is: recovers the first stream float; inverting the exact rational form of float(e)/float(π) yields a modular equation that pins the seed uniquely. Flag is unwrapped with shake_256(str(seed)).
Challenge
class RealNumberGenerator:
def random(self):
self.state = Decimal(e) * Decimal(self.state) % Decimal(pi)
return sqrt(abs(sin(state) + cos(state))) # math.* → float
def next_bytes(self):
yield from struct.pack('d', self.random()) # 8 bytes per call
seed = secrets.randbits(48)
msg = b'The flag is: ' + xor(flag, shake_256(str(seed)).digest(len(flag)))
enc = xor(msg, rng.next_bytes())
Attack
1. Known plaintext → first double
stream[:13] = enc[:13] XOR b'The flag is: '
F0 = struct.unpack('d', stream[:8])
2. Invert sqrt(|sin+cos|)
sin s + cos s = √2 sin(s+π/4) = ±F0² gives two real roots in [0,π):
s ∈ {0.5369…, 1.0338…} (and a few adjacent IEEE floats that still pack to F0).
3. Exact modular form of the state update
Decimal(math.e) / Decimal(math.pi) are exact float dyadics:
e = e_num/e_den, π = pi_num/pi_den
(e·seed) mod π = (A·seed mod B) / DEN
A = e_num·pi_den, B = e_den·pi_num, DEN = e_den·pi_den
gcd(A,B) = 2^48. With Ag=A/G, Bg=B/G (~53-bit modulus):
(Ag·seed) mod Bg ≈ T·DEN/G
seed ≡ (target)·Ag⁻¹ (mod Bg)
Because Bg > 2^48, at most one seed per target residue; a ±4 window around each good T is enough.
4. Verify & decrypt
Match 13 stream bytes, then:
msg = enc XOR stream
flag = msg[13:] XOR shake_256(str(seed)).digest(...)
Result
SEED 22933217634325
FLAG FlagY{r34lly_n0t_g00d_RNG_9aedfc5b67}
Flag
FlagY{r34lly_n0t_g00d_RNG_9aedfc5b67}
Offline from handout; submitted successfully to Flagyard.
Key Takeaways
- “Real” RNGs that mix IEEE floats with
Decimalare still finite-state and invertible when the seed is small. - Known-plaintext on the first
struct.pack('d', …)block reduces the problem to recovering state preimages under a smooth function. - Representing float constants via
as_integer_ratio()turns% πinto a pure integer modular condition; largegcdwith2^48collapses the search. - Always confirm the full known-plaintext prefix before SHAKE unwrap.
Tools
- Python 3, numpy (
nextafter), pycryptodome not required - Solver:
/root/flagyard/real-number-generator/solve.py