← Back to Writeups
CUSTOMMediumMisc

Random Password

XESXOR9/2/20263 min read
#misc#custom#medium#deterministic_simulation#forward_dp_search#prng_consumption_tracking#prng_state_prediction

Random Password

Platform: tamuctf | Category: Misc | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-03-21 | Status: Solved Techniques: deterministic_simulation, forward_dp_search, prng_consumption_tracking, prng_state_prediction

Summary

Task: Password verification using deterministic PRNG with fixed seed, fake sleep consuming random values. Solution: Forward DP tracking reachable PRNG indices to find bit string matching target final value.

Random Password — TAMUctf 2026

Description

Password verification by sleepy counting

The challenge provides a password verification server that uses a deterministic PRNG. The goal is to find a 64-character hex password that passes verification.

  • Files: server.py, solver-template.py
  • Flag format: gigem{...}

Analysis

Server Code (Key Parts)

python
from functools import partial
import random

random.seed(121728)  # Fixed seed - deterministic!

def random_sleep(timeout: float) -> None:
    time_elapsed = 0
    while time_elapsed < timeout:
        time_elapsed += random.random()  # Consumes PRNG values
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

handle_zero = partial(random_sleep, 5)   # ~10 calls on average
handle_one = partial(random_sleep, 17)   # ~34 calls on average

def verify(password: str, correct_value: float) -> bool:
    if len(password) != 64: return False
    bit_str = bin(int(password, 16))[2:].zfill(256)
    for bit in bit_str:
        match bit:
            case '0': handle_zero()
            case '1': handle_one()
    return random.random() == correct_value  # Must match exactly!

Key Insight

The "sleepy counting" hint is crucial: random_sleep() does NOT call time.sleep(). Instead, it consumes random numbers from a deterministic PRNG (seeded with 121728) by summing random.random() calls until the sum exceeds a threshold (5 for bit '0', 17 for bit '1'). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Since the seed is fixed, all random values are fully predictable. The challenge becomes: find a 256-bit string such that after processing all bits, the next random.random() equals exactly 0.9992610559813815.

Problem Structure

  1. PRNG seed is fixed: random.seed(121728) makes all values deterministic
  2. Password: 64 hex chars = 256 bits
  3. Each bit: Consumes a variable number of PRNG calls (depends on actual random values)
  4. Target: After 256 bits, random.random() == 0.9992610559813815
  5. State space: The PRNG index after processing determines the final value ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution Approach: Forward Dynamic Programming

Since two different bit prefixes reaching the same PRNG index are equivalent for all future choices, we can use forward DP:

  1. Pre-generate all random values we might need (~20000)
  2. Find where the target value 0.9992610559813815 appears in the sequence
  3. Track reachable PRNG indices at each bit position
  4. For each state, try both bit choices (0 and 1) and track resulting indices
  5. After 256 bits, check if any final state matches the target index ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

python
import random

random.seed(121728)
MAX_VALS = 20000
values = [random.random() for _ in range(MAX_VALS)]

def simulate_sleep(timeout, start_idx):
    """Returns the number of random values consumed"""
    time_elapsed = 0
    idx = start_idx
    while time_elapsed < timeout:
        time_elapsed += values[idx]
        idx += 1
    return idx - start_idx

target = 0.9992610559813815

# Find the target value's position in the pre-generated array
target_indices = [i for i in range(MAX_VALS) if values[i] == target]
print(f"Target indices: {target_indices}")  # [5719]
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Forward BFS/DP: track reachable PRNG indices at each bit position
current_states = {0: ""}  # index -> bit_string_prefix

for bit_pos in range(256):
    next_states = {}
    for idx, bits in current_states.items():
        # Try bit = 0 (timeout = 5)
        consumed_0 = simulate_sleep(5, idx)
        new_idx_0 = idx + consumed_0
        if new_idx_0 not in next_states:
            next_states[new_idx_0] = bits + "0"
        
        # Try bit = 1 (timeout = 17)
        consumed_1 = simulate_sleep(17, idx)
        new_idx_1 = idx + consumed_1
        if new_idx_1 not in next_states:
            next_states[new_idx_1] = bits + "1"
    
    current_states = next_states
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Check which final states match a target index
for target_idx in target_indices:
    if target_idx in current_states:
        bit_str = current_states[target_idx]
        password = hex(int(bit_str, 2))[2:].zfill(64)
        print(f"Password (hex): {password}")

Results

  • Target value 0.9992610559813815 found at PRNG index 5719
  • Forward DP explored ~4500 reachable states per bit position (manageable)
  • Found password: 00000000000000000000000000000000ffffffffefffffffffffffffffffff7f

Submission

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
from pwn import *
io = remote("streams.tamuctf.com", 443, ssl=True, sni="random-password")
io.recvuntil(b"Enter the password in hex: ")
io.sendline(b"00000000000000000000000000000000ffffffffefffffffffffffffffffff7f")
response = io.recvall(timeout=10)
# Here's the flag gigem{REDACTED}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍