Web
Easy
150 points

BP88 Level 1

Recuite 2025 - HCMUS
6 tháng 10, 2025
PRNG
Static Seed
Predictable Random
Dice Game
Recuite 2025 - HCMUS
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

  1. Never use static seeds for random number generation
  2. Do not use random module for security-related applications
  3. Use secrets module for cryptographic randomness
  4. Do not reset seed to the same value every round
  5. 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