BtSCTF Roulette - Pez1181/CTF GitHub Wiki
🎖️ Mission Brief: Operation Lucky 13 🎖️
Classification: Top Secret (and slightly absurd)
Commander: Cadet Noobinator
Objective: Land the cursed number 13 for 37 consecutive rounds on the “provably fair” roulette game and exfiltrate the flag (redacted to the punBTS{N0_W4Y_Y0U_W1N_13_37}
).
⚔️ Phase 1: Recon & Intel Gathering
-
Service Topology
- The challenge provides an
sc
stub (snicat) to tunnel TLS/SNI. - Download & run:
wget -O sc
https://github.com/CTFd/snicat/releases/latest/download/sc_Linux_x86_64
chmod +x sc
./sc -b 4000 roulette.chal.bts.wh.edu.pl
This binds 127.0.0.1:4000 → real roulette service.
- The challenge provides an
-
Provable-Fair Illusion
- Server prints a commitment:
Server seed hash (verify later):<64-hex-chars>
- Fatal Flaw: entropy = 17 bits (
secrets.randbits(17)
).
- Server prints a commitment:
💣 Phase 2: Breaking the Commitment
-
Capture the printed
server_seed_hash
. -
Brute-force all 131,072 possibilities:
for k in range(1<<17):
cand = sha256_hex(bytes(k))
if sha256_hex(cand.encode()) == server_seed_hash:
server_seed = cand
break -
Result: recover the exact 64-hex
server_seed
in milliseconds! ⚡
🧙♂️ Phase 3: Crafting the Winning Client Seed
-
Game outcome:
game_hash = sha256(f"{server_seed}:{client_seed}".encode()).hexdigest()
roulette_number = int(game_hash, 16) % 37 -
Goal: force
roulette_number == 13
. -
Method: try
client_seed = "0"
,"1"
, ... until hit 13:for i in itertools.count():
gh = sha256_hex(f"{server_seed}:{i}".encode())
if int(gh, 16) % 37 == 13:
client_seed = str(i)
break -
Avg tries: ~37 🎯
🤖 Phase 4: Automation & 37-Strike Run
-
Precompute Map
- Build
seed_map: server_seed_hash → server_seed
for all 2¹⁷ seeds once.
- Build
-
Handle I/O Quirks
- Use raw
recv()
orrecv_until()
because “Place your bet” has no newline. - Sync on the newline-terminated
"Server seed hash"
each round.
- Use raw
-
High-Level Workflow
seed_map = build_17bit_map() ← one-time work
sock = connect("127.0.0.1", 4000)
wins = 0
while True:
srv_hash = recv_until(sock, b"Server seed hash")
server_seed = seed_map[srv_hash]
client_seed = find_seed_for_13(server_seed)
send(sock, client_seed + "\n")
recv_until(sock, b"Place your bet")
send(sock, b"13\n")
buf = recv_until_any(sock, [b"Server seed hash", b"How? How is it possible"])
if b"How? How is it possible" in buf:
flag = extract_flag(buf)
print(flag)
break
wins += 1
log(f"Win #{wins}") -
Beginner Tips
- Precompute the 17-bit map—avoid repeated brute-forces.
- Beware prompts without
\n
. - Use stderr for logs so flag pops cleanly to stdout.
🏆 Phase 5: Exfiltration & Debrief
-
On Win #37, the server exclaims:
How? How is it possible? What was the chance?!
Anyway, here’s your flag, congratulations… BTS{N0_W4Y_Y0U_W1N_13_37}
Lessons Learned
- Entropy matters: 17 bits ≪ 128 bits → trivially broken.
- Handle interactive I/O quirks carefully.
- Automation + protocol insight = swift victory! 🚩
Operation Lucky 13 complete! 🎖️