Challenge Overview
A dice-based gambling game where you start with $800 and need to reach $10,000 to buy the flag. The game allows you to:
- Bet on dice rolls (0-99)
- Set a “greed” threshold (2-98)
- Win if the roll ≤ greed, with multiplier = 99/greed
- Play multiple games in sequence
Key Files
chall.py– The challenge server codesolve.py– The exploit scriptserverseed– Random seed file (unknown to us)
The Vulnerability
Provably Fair System Gone Wrong
The game implements a “provably fair” dice system where:
- Server generates a random 32-bit seed using Python’s
random.getrandbits(32) - Dice roll is deterministic:
HMAC-SHA256(server_seed, client_seed-nonce) - After each game, the server reveals the server_seed used
The Critical Flaw
Python’s random module uses the Mersenne Twister (MT19937) PRNG, which has a well-known property:
- Given 624 consecutive outputs of
getrandbits(32), you can completely reconstruct the internal state - Once you have the state, you can predict all future outputs
Since the server reveals each server seed after every game, we can:
- Collect 624 server seeds
- Reverse-engineer the MT19937 state
- Predict all future server seeds
- Calculate future dice rolls
- Only bet on guaranteed wins
The Mathematics
Dice Roll Generation
nonce_client_msg = f"{client_seed}-{nonce}".encode()
sig = hmac.new(str(server_seed).encode(), nonce_client_msg, hashlib.sha256).hexdigest()
# Extract lucky number from first 5 hex chars < 1,000,000
index = 0
lucky = int(sig[index*5:index*5+5], 16)
while lucky >= 1e6:
index += 1
lucky = int(sig[index * 5:index * 5 + 5], 16)
roll = round((lucky % 1e4) * 1e-2) # Result: 0-99
Win Probability & Multiplier
- Win condition:
roll ≤ greed - Win probability:
(greed + 1) / 100 - Multiplier:
99 / greed
Examples:
greed=10: 11% win chance, 9.9x multipliergreed=20: 21% win chance, 4.95x multipliergreed=50: 51% win chance, 1.98x multiplier
Exploitation Strategy
Phase 1: State Recovery (624 games)
from randcrack import RandCrack
# Play 624 games with minimum wager to collect server seeds
min_wager = 800 / 800 # $1 per game
collected_seeds = play_games(wager=1.0, games=624, greed=98)
# Clone the MT19937 state
rc = RandCrack()
for seed in collected_seeds:
rc.submit(seed)
# Now we can predict all future seeds!
future_seeds = [rc.predict_getrandbits(32) for _ in range(5000)]
Cost: ~$624 in wagers, but we’ll win some back with greed=98 (~98% win rate)
Phase 2: Prediction Generation
# Predict all future dice rolls
def predict_roll(server_seed, client_seed, nonce):
nonce_client_msg = f"{client_seed}-{nonce}".encode()
sig = hmac.new(str(server_seed).encode(), nonce_client_msg, hashlib.sha256).hexdigest()
# ... (same logic as server)
return roll
all_predictions = [
predict_roll(future_seeds[i], "1337awesome", nonce+i)
for i in range(len(future_seeds))
]
Phase 3: Exploitation Loop
Find sequences where ALL games are guaranteed wins:
def find_guaranteed_wins(start_nonce):
# Try different greed levels (lower = higher profit)
for greed in [10, 15, 20, 25, 30, 35, 40]:
for length in range(3, 50):
rolls = predictions[start_nonce:start_nonce+length]
# Check if ALL rolls ≤ greed (guaranteed wins)
if all(roll <= greed for roll in rolls):
return {
"length": length,
"greed": greed,
"multiplier": 99/greed,
"rolls": rolls
}
Aggressive Betting Strategy:
- Lower greed = higher multiplier = safer to bet more
- Bet 30-50% of balance per game when greed ≤ 30
- Compound winnings exponentially
Example sequence:
Nonce 624-633: [8, 12, 18, 9, 15, 20, 10, 14, 7, 19]
All ≤ 20, so use greed=20 (4.95x multiplier)
Bet $100/game → Win $495/game → Profit $395/game
Total profit: $3,950 from $1,000 wagered
Running the Exploit
# Install dependencies
pip3 install pwntools randcrack
# Run the exploit
python3 solve.py
Exploit Flow
- Connect to challenge server
- Collect 624 server seeds (~$100 balance remaining)
- Crack MT19937 state using RandCrack
- Predict future rolls for 5000+ games
- Find guaranteed win sequences
- Bet aggressively on those sequences
- Compound winnings until $10,000+
- Buy flag
Expected Output
[*] Connecting to challenge...
[*] Collecting 624 server seeds to crack Mersenne Twister...
[+] Collected 624 server seeds
[+] Current balance: $127.34
[*] Cracking Mersenne Twister state...
[+] Successfully cloned random state!
[*] Generating future server seed predictions...
[+] Generated 5624 total server seed predictions
[*] Starting exploitation...
[*] Bet 1 (nonce 624-631):
Balance: $127.34
Sequence: 8 games @ greed=20 (all rolls ≤ 20)
Wager: $7.14/game → Profit: $224.73
[+] New balance: $352.07
[*] Bet 2 (nonce 632-640):
Balance: $352.07
Sequence: 9 games @ greed=15 (all rolls ≤ 15)
Wager: $19.56/game → Profit: $940.44
[+] New balance: $1,292.51
... (continues compounding) ...
[+] SUCCESS! Balance: $10,247.83
[*] Buying flag...
[+] FLAG: flag{pr0v4bly_un41r_g4mbl1ng_ftw}
Key Takeaways
Why This Vulnerability Exists
- Cryptographically Weak PRNG: MT19937 is not cryptographically secure
- State Recovery: 624 outputs fully determine future behavior
- Information Leak: Server reveals the sensitive seed after each game
- Deterministic: HMAC makes rolls predictable once seed is known
Proper Implementation
For a secure provably fair system:
# Use cryptographically secure random
import secrets
server_seed = secrets.token_bytes(32) # Not predictable!
# Don't reveal seeds until game series ends
# Or use commitment scheme (hash first, reveal later)
Real-World Impact
This vulnerability pattern appears in:
- Gambling sites using weak PRNGs
- Blockchain games with predictable randomness
- CTF challenges testing crypto knowledge
- Any system leaking MT19937 outputs
Tools Used
- pwntools: Remote connection and interaction
- randcrack: MT19937 state recovery library
- Python’s random: Understanding the vulnerable PRNG
- hashlib/hmac: Replicating dice roll calculation
References
Flag: Successfully captured after reaching $10,000+ balance through MT19937 state exploitation.