CTF Misc - Games, VMs & Constraint Solving (Part 2)
Table of Contents
- Cookie Checkpoint Game Brute-Forcing (BYPASS CTF 2025)
- Flask Session Cookie Game State Leakage (BYPASS CTF 2025)
- WebSocket Game Manipulation + Cryptic Hint Decoding (BYPASS CTF 2025)
- Server Time-Only Validation Bypass (BYPASS CTF 2025)
- De Bruijn Sequence for Substring Coverage (BearCatCTF 2026)
- Brainfuck Interpreter Instrumentation (BearCatCTF 2026)
- WASM Linear Memory Manipulation (BearCatCTF 2026)
- References
Cookie Checkpoint Game Brute-Forcing (BYPASS CTF 2025)
Pattern (Signal from the Deck): Server-side game where selecting tiles increases score. Incorrect choice resets the game. Score tracked via session cookies.
Technique: Save cookies before each guess, restore on failure to avoid resetting progress.
import requests
URL = "https://target.example.com"
def solve():
s = requests.Session()
s.post(f"{URL}/api/new")
while True:
data = s.get(f"{URL}/api/signal").json()
if data.get('done'):
break
checkpoint = s.cookies.get_dict()
for tile_id in range(1, 10):
r = s.post(f"{URL}/api/click", json={'clicked': tile_id})
res = r.json()
if res.get('correct'):
if res.get('done'):
print(f"FLAG: {res.get('flag')}")
return
break
else:
s.cookies.clear()
s.cookies.update(checkpoint)Key insight: Session cookies act as save states. Preserving and restoring cookies on failure enables deterministic brute-forcing without game reset penalties.
Flask Session Cookie Game State Leakage (BYPASS CTF 2025)
Pattern (Hungry, Not Stupid): Flask game stores correct answers in signed session cookies. Use flask-unsign -d to decode the cookie and reveal server-side game state without playing.
# Decode Flask session cookie (no secret needed for reading)
flask-unsign -d -c '<cookie_value>'Example decoded state:
{
"all_food_pos": [{"x": 16, "y": 12}, {"x": 16, "y": 28}, {"x": 9, "y": 24}],
"correct_food_pos": {"x": 16, "y": 28},
"level": 0
}Key insight: Flask session cookies are signed but not encrypted by default. flask-unsign -d decodes them without the secret key, exposing server-side game state including correct answers.
Detection: Base64-looking session cookies with periods (.) separating segments. Flask uses itsdangerous signing format.
WebSocket Game Manipulation + Cryptic Hint Decoding (BYPASS CTF 2025)
Pattern (Maze of the Unseen): Browser-based maze game with invisible walls. Checkpoints verified server-side via WebSocket. Cryptic hint encodes target coordinates.
Technique:
- Open browser console, inspect WebSocket messages and
playerobject - Decode cryptic hints (e.g., "mosquito were not available" → MQTT → port 1883)
- Teleport directly to target coordinates via console
function teleport(x, y) {
player.x = x;
player.y = y;
verifyProgress(Math.round(player.x), Math.round(player.y));
console.log(`Teleported to x:${player.x}, y:${player.y}`);
}
// "mosquito" → MQTT (port 1883), "not available" → 404
teleport(1883, 404);Common cryptic hint mappings:
- "mosquito" → MQTT (Mosquitto broker, port 1883)
- "not found" / "not available" → HTTP 404
- Port numbers, protocol defaults, or ASCII values as coordinates
Key insight: Browser-based games expose their state in the JS console. Modify player.x/player.y or equivalent properties directly, then call the progress verification function.
Server Time-Only Validation Bypass (BYPASS CTF 2025)
Pattern (Level Devil): Side-scrolling game requiring traversal of a map. Server validates that enough time has elapsed (map_length / speed) but doesn't verify actual movement.
import requests
import time
TARGET = "https://target.example.com"
s = requests.Session()
r = s.post(f"{TARGET}/api/start")
session_id = r.json().get('session_id')
# Wait for required traversal time (e.g., 4800px / 240px/s = 20s + margin)
time.sleep(25)
s.post(f"{TARGET}/api/collect_flag", json={'session_id': session_id})
r = s.post(f"{TARGET}/api/win", json={'session_id': session_id})
print(r.json().get('flag'))Key insight: When servers validate only elapsed time (not player position, inputs, or movement), start a session, sleep for the required duration, then submit the win request. Always check if the game API has start/win endpoints that can be called directly.
De Bruijn Sequence for Substring Coverage (BearCatCTF 2026)
Pattern (Brown's Revenge): Server generates random n-bit binary code each round. Input must contain the code as a substring. Pass 20+ rounds with a single fixed input under a character limit.
def de_bruijn(k, n):
"""Generate de Bruijn sequence B(k, n): cyclic sequence containing
every k-ary string of length n exactly once as a substring."""
a = [0] * k * n
sequence = []
def db(t, p):
if t > n:
if n % p == 0:
sequence.extend(a[1:p+1])
else:
a[t] = a[t - p]
db(t + 1, p)
for j in range(a[t - p] + 1, k):
a[t] = j
db(t + 1, t)
db(1, 1)
return sequence
# For 12-bit binary codes: B(2, 12) has length 4096
seq = ''.join(map(str, de_bruijn(2, 12)))
payload = seq + seq[:11] # Linearize: 4096 + 11 = 4107 chars
# Every possible 12-bit code appears as a substringKey insight: De Bruijn sequence B(k, n) contains all k^n possible n-length strings over alphabet k as substrings, with cyclic length k^n. To linearize (non-cyclic), append the first n-1 characters. Total length = k^n + n - 1. Send the same string every round — it contains every possible code.
Detection: Must find arbitrary n-bit pattern as substring of limited-length input. Character budget matches de Bruijn length (k^n + n - 1).
Brainfuck Interpreter Instrumentation (BearCatCTF 2026)
Pattern (Ghost Ship): Large Brainfuck program (10K+ instructions) validates a flag character-by-character. Full reverse engineering is impractical.
Per-character brute-force via instrumentation:
- Instrument a Brainfuck interpreter to track tape cell values
- Identify a "wrong count" cell that increments per incorrect character
- For each position, try all printable ASCII — pick the character that doesn't increment the wrong counter
def run_bf_instrumented(code, input_bytes, max_steps=500000):
tape = [0] * 30000
dp, ip, inp_idx = 0, 0, 0
for _ in range(max_steps):
if ip >= len(code): break
c = code[ip]
if c == '+': tape[dp] = (tape[dp] + 1) % 256
elif c == '-': tape[dp] = (tape[dp] - 1) % 256
elif c == '>': dp += 1
elif c == '<': dp -= 1
elif c == '.': pass # output
elif c == ',':
tape[dp] = input_bytes[inp_idx] if inp_idx < len(input_bytes) else 0
inp_idx += 1
elif c == '[' and tape[dp] == 0:
# skip to matching ]
...
elif c == ']' and tape[dp] != 0:
# jump back to matching [
...
ip += 1
return tape
# Brute-force: ~40 positions × 95 chars = 3800 runs
flag = []
for pos in range(40):
for c in range(32, 127):
candidate = flag + [c] + [ord('A')] * (39 - pos)
tape = run_bf_instrumented(code, candidate)
if tape[WRONG_COUNT_CELL] == 0: # No errors up to this position
flag.append(c)
breakKey insight: Brainfuck programs that validate input character-by-character can be brute-forced without understanding the program logic. Instrument the interpreter to observe tape state, find the cell that tracks validation progress, and optimize per-character search. ~3800 runs completes in minutes.
WASM Linear Memory Manipulation (BearCatCTF 2026)
Pattern (Dubious Doubloon): Browser game compiled to WebAssembly with win conditions requiring luck (e.g., 15 consecutive coin flips). WASM linear memory is flat and unprotected.
Direct memory patching in Node.js:
const { readFileSync } = require('fs');
const wasmBuffer = readFileSync('game.wasm');
const { instance } = await WebAssembly.instantiate(wasmBuffer, imports);
const mem = new DataView(instance.exports.memory.buffer);
// Patch game variables at known offsets
mem.setInt32(0x102918, 14, true); // streak counter = 14 (need 15)
mem.setInt32(0x102898, 100, true); // win chance = 100%
// One more flip → guaranteed win → flag decoded
const result = instance.exports.flipCoin();Key insight: Unlike WAT patching (modifying the binary), memory manipulation patches runtime state after loading. All WASM variables live in flat linear memory at fixed offsets. Use wasm-objdump -x game.wasm or search for known constants to find variable offsets. No need to understand the full game logic — just set the state to "about to win".
Detection: WASM game requiring statistically impossible sequences (streaks, perfect scores). Game logic is in .wasm file loadable in Node.js.
References
- BYPASS CTF 2025 "Signal from the Deck": Cookie checkpoint game brute-forcing
- BYPASS CTF 2025 "Hungry, Not Stupid": Flask cookie game state leakage
- BYPASS CTF 2025 "Maze of the Unseen": WebSocket teleportation + cryptic hints
- BYPASS CTF 2025 "Level Devil": Server time-only validation bypass
- BearCatCTF 2026 "Brown's Revenge": De Bruijn sequence substring coverage
- BearCatCTF 2026 "Ghost Ship": Brainfuck instrumentation brute-force
- BearCatCTF 2026 "Dubious Doubloon": WASM linear memory state patching
See also: games-and-vms.md for WASM patching, Roblox reversing, PyInstaller, Z3, K8s RBAC, floating-point exploitation, custom assembly sandbox escape, and multi-phase crypto games.