Admin Poker
Admin Poker
Platform: CUSTOM | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: hidden_state_leak_exploitation, poker_hand_evaluation, protocol_reverse_engineering, session_reuse
Summary
Walkthrough of the CUSTOM Reversing Admin Poker. Task: a FreeBSD-themed poker ELF looks like a local game, but it is only a TCP client for a remote service. Solution: reverse the protocol, use GAME_STATE leaking both bots' hole cards and all five bo
Description
I was invited by some hardcore sysadmins to a poker match. They promised me a flag if I could get 500 chips. The problem is, they don't even use Linux... they're too trueЪ for that, they only use FreeBSD
The provided adminpoker.elf is not the actual poker engine. It is a network client for a remote service, so the real task is to reverse the protocol and find a reliable way to accumulate chips.
Analysis
The ELF is only a thin client
Static analysis shows the binary opens a TCP connection to 109.233.56.90:11546 and communicates with newline-delimited text commands. The important protocol messages are:
CLIENT_HELLO->SERVER_HELLOSTART_GAME->STARTEDGET_GAME_STATE->GAME_STATE ...BET <n>during betting promptsGET_FLAGonce the balance is high enough
This immediately changes the mindset for the challenge: memory corruption or local patching will not help, because the server is authoritative.
Protocol structure
After the handshake, the client starts a hand and waits for WHATS_YOUR_BET. At that point it can ask for the current state with GET_GAME_STATE.
The returned line has this shape:
text read-only • copy allowedGAME_STATE <stage> <11 cards> <7 floats>
The 11 cards are enough to encode:
- our 2 hole cards,
- bot 1 hole cards,
- bot 2 hole cards,
- all 5 board cards.
The floats contain balance and other round values such as bets and bank size.
The real bug: full hidden-state leak
The crucial mistake is that GAME_STATE leaks all hidden information immediately, even at the start of the hand:
- both bots' private cards are already present,
- all five community cards are already present,
- the leak happens before flop, turn, and river are officially revealed.
So instead of playing imperfect-information poker, we can compute the final showdown result as soon as the first betting prompt appears.
Dead ends
Several ideas were unnecessary or wrong:
- calling
GET_FLAGbefore reaching 500 chips, - sending malformed or abusive bet values as a direct bypass,
- focusing on PRNG behavior as if the local ELF controlled the deck.
The binary contains local game-related code, but the server-supplied GAME_STATE overwrites everything that matters. The exploit path is a protocol information leak leading to a deterministic winning strategy.
Persistent session advantage
Another useful detail is that the connection stays alive across hands. After PRIZES_GIVEN, sending START_GAME again starts a new hand while keeping the accumulated balance. That makes it possible to grind safely until the threshold is reached.
Solution
- Connect to
109.233.56.90:11546. - Send
CLIENT_HELLOand wait forSERVER_HELLO. - For each hand, send
START_GAMEand wait forSTARTEDandWHATS_YOUR_BET. - Request
GET_GAME_STATE. - Parse the 11 leaked cards.
- Compute the best 5-card poker hand out of each player's 7 cards.
- If our final hand is strictly better than both bots, bet the whole current balance; otherwise bet
0. - Repeat until the balance exceeds
500, then sendGET_FLAG.
The practical optimization used in the final solver was to send the same command three times at once:
text read-only • copy allowedBET <balance> BET <balance> BET <balance>
That answers all betting rounds in one shot for winning hands.
python read-only • copy allowed#!/usr/bin/env python3 import itertools import re import socket import sys HOST = "109.233.56.90" PORT = 11546 def recv_until(sock, marker): data = "" while marker not in data: chunk = sock.recv(4096).decode("latin1", "replace") if not chunk: raise EOFError(f"connection closed while waiting for {marker!r}") data += chunk return data def score5(cards): vals = sorted((r for r, _ in cards), reverse=True) counts = {v: vals.count(v) for v in set(vals)} groups = sorted(counts.items(), key=lambda kv: (kv[1], kv[0]), reverse=True) flush = len({s for _, s in cards}) == 1 uniq = sorted(set(vals), reverse=True) straight = False shigh = None if len(uniq) == 5: if uniq[0] - uniq[4] == 4: straight = True shigh = uniq[0] elif uniq == [14, 5, 4, 3, 2]: straight = True shigh = 5 if straight and flush: return (8, shigh) if groups[0][1] == 4: four = groups[0][0] kicker = max(v for v in vals if v != four) return (7, four, kicker) if groups[0][1] == 3 and groups[1][1] == 2: return (6, groups[0][0], groups[1][0]) if flush: return (5, *vals) if straight: return (4, shigh) if groups[0][1] == 3: trips = groups[0][0] kick = sorted((v for v in vals if v != trips), reverse=True) return (3, trips, *kick) if groups[0][1] == 2 and groups[1][1] == 2: pairs = sorted([g[0] for g in groups if g[1] == 2], reverse=True) kicker = max(v for v in vals if v not in pairs) return (2, pairs[0], pairs[1], kicker) if groups[0][1] == 2: pair = groups[0][0] kick = sorted((v for v in vals if v != pair), reverse=True) return (1, pair, *kick) return (0, *vals) def best7(cards): return max(score5(c) for c in itertools.combinations(cards, 5)) def parse_state(text): m = re.search( r"GAME_STATE\s+(-?\d+)((?:\s+\d+\s+[cdhs]){11})\s+([0-9.-]+)\s+([0-9.-]+)\s+([0-9.-]+)\s+([0-9.-]+)\s+([0-9.-]+)\s+([0-9.-]+)\s+([0-9.-]+)", text, ) if not m: raise ValueError(f"no GAME_STATE in: {text!r}") cp = m.group(2).split() cards = [(int(cp[i]), cp[i + 1]) for i in range(0, len(cp), 2)] floats = list(map(float, m.groups()[2:])) return int(m.group(1)), cards, floats def main(): sock = socket.create_connection((HOST, PORT), timeout=10) sock.sendall(b"CLIENT_HELLO\n") print(recv_until(sock, "SERVER_HELLO\n").strip()) hand = 0 while True: sock.sendall(b"START_GAME\n") recv_until(sock, "STARTED\n") recv_until(sock, "WHATS_YOUR_BET\n") sock.sendall(b"GET_GAME_STATE\n") _, cards, floats = parse_state(recv_until(sock, "GAME_STATE")) balance = floats[0] my = best7([cards[0], cards[1], *cards[6:11]]) bot1 = best7([cards[2], cards[3], *cards[6:11]]) bot2 = best7([cards[4], cards[5], *cards[6:11]]) winning = my > bot1 and my > bot2 bet = int(balance) if winning else 0 sock.sendall((f"BET {bet}\n" * 3).encode()) recv_until(sock, "PRIZES_GIVEN\n") sock.sendall(b"GET_GAME_STATE\n") _, _, floats = parse_state(recv_until(sock, "GAME_STATE")) balance = floats[0] hand += 1 print(f"hand={hand} win={winning} bet={bet} balance={balance}") if balance > 500: sock.sendall(b"GET_FLAG\n") print(recv_until(sock, "}")) return if __name__ == "__main__": try: main() except Exception as e: print(f"ERROR: {e}") sys.exit(1)
Key Takeaways / Lessons
- Mastering hidden_state_leak_exploitation, poker_hand_evaluation, protocol_reverse_engineering, session_reuse was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-06-08 • CUSTOM • Admin Poker • Difficulty: Medium