Web
Easy
150 points
BP88 Level 1
Recuite 2025 - HCMUS
6 tháng 10, 2025
PRNG
Static Seed
Predictable Random
Dice Game

Web
BP88 - Write-up
Challenge Information
- Category: Web
- Difficulty: Easy
- Vulnerability: Predictable PRNG (Static Seed)
- Game: Tai Xiu (Sic Bo / Dice Game)
Overview
BP88 is a web-based dice gambling game with 3 difficulty levels. Level 1 uses a static seed for Python's random module, making the dice results completely predictable.
Game Mechanics
Sic Bo Rules
- Roll 3 dice (each 1-6)
- Total ≥ 11: Tai (High/Big)
- Total ≤ 10: Xiu (Low/Small)
- Player bets on Tai or Xiu
Win Condition
- Level 1: Reach 50 coins to unlock Level 2
Vulnerability: PRNG with Static Seed
Source Code Analysis
def prepare_seed(level):
""" Create seed for `random` module """
if level == 1:
random.seed(13371337) # ⚠️ Fixed seed!
elif level == 2:
random.seed(round(time.time()))
else:
random.seed()
def roll_dice():
dice = [random.randint(1, 6) for _ in range(3)]
return dice
The Problem:
- Level 1 always uses the same seed (13371337)
- Random number sequence is completely deterministic
- Outcome of every dice roll can be predicted
Exploitation
Step 1: Reverse Engineer PRNG
Python's random module with a fixed seed will always produce the same sequence:
import random
def predict_result(seed):
random.seed(seed)
dice = [random.randint(1, 6) for _ in range(3)]
total = sum(dice)
result = 'tai' if total >= 11 else 'xiu'
return result, dice, total
# Seed of Level 1
seed = 13371337
result, dice, total = predict_result(seed)
print(f"Result: {result}, Dice: {dice}, Total: {total}")
Step 2: Prediction Test
import random
random.seed(13371337)
# First roll
print([random.randint(1,6) for _ in range(3)]) # [5, 2, 5] = 12 = Tai
# Reset seed for each game
random.seed(13371337)
print([random.randint(1,6) for _ in range(3)]) # [5, 2, 5] = 12 = Tai (identical!)
Key Point: Each game round resets the seed to 13371337, so every round has identical results!
Mitigation
❌ Vulnerable Code
def prepare_seed(level):
if level == 1:
random.seed(13371337) # Fixed Seed
✅ Secure Code
import secrets
def roll_dice():
# Use cryptographically secure random
dice = [secrets.randbelow(6) + 1 for _ in range(3)]
return dice
# Or use os.urandom
import os
def roll_dice_secure():
dice = [int.from_bytes(os.urandom(1), 'big') % 6 + 1 for _ in range(3)]
return dice
Better Approach: Server-Side Secrets
import secrets
import time
def generate_provably_fair_seed():
# Combine multiple entropy sources
timestamp = str(time.time_ns())
random_bytes = secrets.token_hex(32)
server_secret = os.environ.get('SERVER_SECRET')
# Hash everything together
import hashlib
seed_material = f"{timestamp}{random_bytes}{server_secret}"
seed = int(hashlib.sha256(seed_material.encode()).hexdigest(), 16)
return seed % (2**32)
Key Takeaways
- Never use static seeds for random number generation
- Do not use
randommodule for security-related applications - Use
secretsmodule for cryptographic randomness - Do not reset seed to the same value every round
- Provably fair systems need commitment schemes
Level 2
The seed is now random but is also displayed on the web, so it can be easily retrieved and used for prediction.
Level 3
At this point, the seed has been hidden but it still exists somewhere on the web, so you can open browser tools and input JS commands to display it.
Even if not in gameData, seed might be in other events:
// Intercept ALL Socket.IO messages
socket.onAny((eventName, ...args) => {
console.log('Event:', eventName, args);
// Deep search for values looking like seeds
JSON.stringify(args).match(/\d{10}/g)?.forEach(num => {
console.log('🔍 Potential Seed:', num);
});
});
Flag
Flag 1:
HCMUS{st4t1c_s33d_1s_pr3d1ct4bl3}
Flag 2:
HCMUS{t1m3st4mp_s33d_3xp0s3d}
Flag 3:
HCMUS{h1dd3n_s33d_st1ll_pr3d1ct4bl3}
References
150
Points
Easy
Difficulty
Web
Category