miscfreeeasy

Lucky Dice

hackthebox

$ ls tags/ techniques/
tcp_input_prebufferingtiming_race_conditiontcp_nodelay

Lucky Dice — HackTheBox

Description

"How fast can you keep score?"

A TCP service at 154.57.164.67:31990 runs a dice game with 100 rounds. Each round, 8-13 players roll dice (increasing count per round). You must identify the winner (highest sum, ties broken by last player) within 0.3 seconds.

Analysis

Examining the challenge source code reveals the game mechanics:

Game Structure

  • 100 rounds total
  • player_nr = random.randint(8, 13) — random number of players (8-13)
  • Each round rnd has dice_nr = rnd * 2 + 2 dice per player (increasing each round)
  • Each die: random.randint(1, 6)
  • Winner = player with highest dice sum; ties broken by highest player number ("the player who rolled the last dice wins")
  • Winner is determined by: sorted(dice_sum.items(), key=lambda x:x[1])[-1][0][1].split('_')[1]
  • Server has time.sleep(0.1) per player line and time.sleep(0.05) before "Who wins"

Critical Timing Code

start = time.time() answer = input('> ') if time.time() - start > timeout: # timeout = 0.3 print("Mate... your are too slow!") return False

The key insight is that the 0.3s timer starts when time.time() is called BEFORE input(). If our answer is already in the TCP receive buffer when input() executes, it returns instantly, making time.time() - start ≈ 0.

The Exploit: TCP Input Pre-buffering

  1. Parse dice values from "Player N: d1 d2 d3..." lines as they arrive
  2. Compute winner immediately after receiving "Who wins this round?" line
  3. Send answer via TCP BEFORE the server prints player options and "> " prompt
  4. The answer travels through the network and arrives in the TCP buffer
  5. When server eventually calls input('> '), the answer is already waiting → instant read → timer ≈ 0
  6. TCP_NODELAY socket option (disable Nagle's algorithm) helps reduce send latency

Solution

#!/usr/bin/env python3 """ Solver for Lucky Dice challenge. Strategy: send answer right after "Who wins this round?" line, before the server prints options and "> " prompt. The answer will be buffered in TCP and read instantly by input(). Retry until we get lucky with network timing for all 100 rounds. """ from pwn import * import re import socket import sys def solve(host, port): r = remote(host, port) r.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # Answer "Are you ready?" r.recvuntil(b"> ") r.sendline(b"1") # Wait for "Go!" r.recvuntil(b"Go!\n") for round_num in range(100): # Read all data until "Who wins this round?" data = r.recvuntil(b"Who wins this round?\n", timeout=30).decode() # Parse player lines players = {} for m in re.finditer(r"Player (\d+): ([\d ]+)", data): pnum = int(m.group(1)) dsum = sum(int(x) for x in m.group(2).split()) players[pnum] = dsum # Winner: highest sum; ties → highest player number (last roller) max_sum = max(players.values()) winner = max(p for p, s in players.items() if s == max_sum) # Send answer NOW — before server prints options and "> " prompt r.send(str(winner).encode() + b"\n") # Read through response resp = b"" while b"Correct" not in resp and b"slow" not in resp and b"off" not in resp: resp += r.recvuntil(b"\n", timeout=15) if b"Correct" not in resp: log.warning(f"Round {round_num + 1}: FAILED — {resp.decode().strip()[:80]}") r.close() return False if round_num % 20 == 0 or round_num == 99: log.info(f"Round {round_num + 1}/100: OK (winner={winner})") # Collect flag log.success("All 100 rounds passed!") flag_data = r.recvall(timeout=10).decode() print(flag_data) r.close() return True if __name__ == "__main__": host = sys.argv[1] if len(sys.argv) > 1 else "localhost" port = int(sys.argv[2]) if len(sys.argv) > 2 else 11337 context.log_level = "info" for attempt in range(50): log.info(f"=== Attempt {attempt + 1} ===") try: if solve(host, port): break except Exception as e: log.warning(f"Error: {e}")

Local Testing

  • No Dockerfile provided, so created local test environment using socat: socat TCP-LISTEN:11337,reuseaddr,fork EXEC:"python3 challenge.py"
  • Created flag.txt with test flag in the challenge directory
  • Locally the solver passes all 100 rounds 100% of the time (no network latency)

Remote Execution

  • Server ping: ~115-130ms round-trip
  • With pre-buffering, the approach is slightly probabilistic due to network jitter
  • Sometimes the answer packet arrives after input() is called → timeout
  • On the successful run, all 100 rounds passed on the first attempt
  • Multiple retries (up to 50) were coded as fallback

What Didn't Work (Dead Ends)

ApproachWhy It Failed
Wait for "> " prompt then send answerNetwork RTT (~230ms) exceeds 0.3s timeout
Read player option lines then sendStill too slow — reading lines adds latency
TCP_NODELAY aloneHelps marginally but doesn't solve the fundamental latency issue
Brute-force PRNG seed (0-100000, timestamps)Seed is from os.urandom(), space too large
z3 MT19937 state recovery from 3-bit observationsrandint(1,6) only reveals top 3 bits of each 32-bit MT word; 3 bits per word is insufficient for unique state recovery
z3 with 0-rejection assumptionz3 finds SOME satisfying state but it doesn't match the real state — predictions are wrong (0/5 accuracy across 5 test seeds)
z3 with rejection modeling (acc_count variables, Array indexing)Correct formulation but z3 still finds non-unique solutions due to insufficient bit information (3/32 bits known per word)
Iterative MT recovery (z3 solve → simulate rejections → re-solve)Doesn't converge — z3 finds different valid states each iteration, state changes 600+/624 words per iteration
randcrack libraryRequires 624 consecutive full 32-bit outputs; we only get 3 bits per output

Flag

HTB{r0LL1ng-1n-t43_D33P-b0t_n3T-cRe4t10n}

Key Indicators

Use this technique when:

  • TCP service with tight timeout on input() — timer starts at input() call, not prompt display
  • Python's input() reads from stdin buffer — answer can be sent before prompt appears
  • time.time() measured around input() — buffer trick makes elapsed time ≈ 0
  • Game logic is deterministic given displayed values — parse and compute immediately
  • Challenge hints at speed/automation ("How fast can you keep score?")

$ cat /etc/motd

Liked this one?

Pro unlocks every writeup, every flag, and API access. $9/mo.

$ cat pricing.md

$ grep --similar

Similar writeups