← Back to Writeups
CUSTOMMediumReversing

Admin Poker

XESXOR9/2/20265 min read
#reversing#custom#medium#hidden_state_leak_exploitation#poker_hand_evaluation#protocol_reverse_engineering#session_reuse

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_HELLO
  • START_GAME -> STARTED
  • GET_GAME_STATE -> GAME_STATE ...
  • BET <n> during betting prompts
  • GET_FLAG once 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
GAME_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_FLAG before 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

  1. Connect to 109.233.56.90:11546.
  2. Send CLIENT_HELLO and wait for SERVER_HELLO.
  3. For each hand, send START_GAME and wait for STARTED and WHATS_YOUR_BET.
  4. Request GET_GAME_STATE.
  5. Parse the 11 leaked cards.
  6. Compute the best 5-card poker hand out of each player's 7 cards.
  7. If our final hand is strictly better than both bots, bet the whole current balance; otherwise bet 0.
  8. Repeat until the balance exceeds 500, then send GET_FLAG. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The practical optimization used in the final solver was to send the same command three times at once:

text
BET <balance>
BET <balance>
BET <balance>

That answers all betting rounds in one shot for winning hands.

python
#!/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