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#
Thông Tin Challenge#
- Danh mục: Web
- Độ khó: Dễ
- Lỗ hổng: PRNG Dự Đoán Được (Static Seed)
- Game: Tài Xỉu (trò chơi xúc xắc)
Tổng Quan#
BP88 là một web-based dice gambling game với 3 levels độ khó. Level 1 sử dụng static seed cho Python's random module, khiến kết quả xúc xắc hoàn toàn có thể dự đoán được.
Cơ Chế Trò Chơi#
Luật Tài Xỉu#
- Tung 3 xúc xắc (mỗi xúc xắc 1-6)
- Tổng ≥ 11: Tài (High)
- Tổng ≤ 10: Xỉu (Low)
- Người chơi đặt cược Tài hoặc Xỉu
Điều Kiện Thắng#
- Level 1: Đạt 50 coins để mở khóa Level 2
Lỗ Hổng: PRNG với Static Seed#
Phân Tích Source Code#
pythondef prepare_seed(level):
""" Tạo seed cho module `random` """
if level == 1:
random.seed(13371337) # ⚠️ Seed cố định!
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
Vấn đề:
- Level 1 luôn sử dụng seed giống nhau (13371337)
- Chuỗi số ngẫu nhiên là hoàn toàn xác định
- Có thể dự đoán outcome của mọi lượt tung xúc xắc
Khai Thác#
Bước 1: Reverse Engineer PRNG#
Python's random module với seed cố định sẽ luôn tạo ra cùng một chuỗi:
pythonimport 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 của Level 1
seed = 13371337
result, dice, total = predict_result(seed)
print(f"Kết quả: {result}, Xúc xắc: {dice}, Tổng: {total}")
Bước 2: Test Dự Đoán#
pythonimport random
random.seed(13371337)
# Lượt tung đầu tiên
print([random.randint(1,6) for _ in range(3)]) # [5, 2, 5] = 12 = Tài
# Reset seed cho mỗi game
random.seed(13371337)
print([random.randint(1,6) for _ in range(3)]) # [5, 2, 5] = 12 = Tài (giống nhau!)
Điểm mấu chốt: Mỗi round game reset seed về 13371337, nên mọi round đều có kết quả giống hệt nhau!
Cách Phòng Chống#
❌ Code Lỗ Hổng#
pythondef prepare_seed(level):
if level == 1:
random.seed(13371337) # Seed cố định
✅ Code An Toàn#
pythonimport secrets
def roll_dice():
# Dùng random bảo mật mã hóa
dice = [secrets.randbelow(6) + 1 for _ in range(3)]
return dice
# Hoặc dùng os.urandom
import os
def roll_dice_secure():
dice = [int.from_bytes(os.urandom(1), 'big') % 6 + 1 for _ in range(3)]
return dice
Cách Tiếp Cận Tốt Hơn: Server-Side Secrets#
pythonimport secrets
import time
def generate_provably_fair_seed():
# Kết hợp nhiều nguồn entropy
timestamp = str(time.time_ns())
random_bytes = secrets.token_hex(32)
server_secret = os.environ.get('SERVER_SECRET')
# Hash tất cả lại với nhau
import hashlib
seed_material = f"{timestamp}{random_bytes}{server_secret}"
seed = int(hashlib.sha256(seed_material.encode()).hexdigest(), 16)
return seed % (2**32)
Bài Học Rút Ra#
- Không bao giờ dùng static seeds cho việc tạo số ngẫu nhiên
- Đừng dùng
randommodule cho ứng dụng liên quan bảo mật - Dùng
secretsmodule cho cryptographic randomness - Đừng reset seed về cùng một giá trị mỗi round
- Hệ thống provably fair cần commitment schemes
Level 2#
Thì seed lúc này được random nhưng seed cũng hiển thị ở trên web nên có thể dễ dàng lấy và sử dụng để dự đoán
Level 3#
Lúc này seed đã được giấu đi nhưng nó vẫn năm đâu đó ở trên web thì có thể mở browers tool rồi nhập lệnh JS để hiển thị nó
Ngay cả không có trong gameData, seed có thể trong events khác:
javascript// Chặn TẤT CẢ tin nhắn Socket.IO
socket.onAny((eventName, ...args) => {
console.log('Event:', eventName, args);
// Tìm kiếm sâu các giá trị giống seed
JSON.stringify(args).match(/\d{10}/g)?.forEach(num => {
console.log('🔍 Seed tiềm năng:', num);
});
});
Flag#
Flag 1:
HCMUS{st4t1c_s33d_1s_pr3d1ct4bl3}
Flag 2:
HCMUS{t1m3st4mp_s33d_3xp0s3d}
Flag 3:
HCMUS{h1dd3n_s33d_st1ll_pr3d1ct4bl3}
Tài Liệu Tham Khảo#
150
Points
Easy
Difficulty
Web
Category