Writeup Reverse Engineering May 1, 2026

TKOTF CTF - Titato Writeup

Titato - stripped 64-bit ELF binary.

itik May 1, 2026 3 min lesing 751 6

TKOTF CTF - Titato Writeup

Flag: RN10{50_1t5_n0t_th4t_1mp0ss1bl3?}


Overview

You get a stripped 64-bit ELF binary. Running it shows a tic-tac-toe game against an AI that never loses; the best you can do normally is draw.

The flag only comes from the server. Patching the binary does nothing. You need to actually beat the AI over the network.


Step 1: The Binary Connects to a Remote Server

strings titato | grep -E "^[0-9]"
# 129.212.225.109

All game logic runs on 129.212.225.109:31337. The binary is just a client.


Step 2: Reverse the Protocol (Ghidra)

Load titato into Ghidra, find main, and trace the send/recv functions.

Wire format (every packet):

[0xC0][0xDE] [type:1] [seq:2 LE] [payload_len:2 LE] [encoded_payload] [tag:4 LE]

Payload encoding (rolling XOR; decode is identical):

out[i] = in[i] ^ key[(seq + i) % 8] ^ ((ptype * 7 + i) & 0xFF)

Tag (keyed CRC32, cannot be forged):

tag = CRC32(key[8] || seq_LE[2] || ptype[1] || raw_payload)

Handshake (first two packets use zero-key \x00*8):

#DirectionTypePayload
1S→CPT_HELLO (0x01)8-byte nonce
2C→SPT_PROOF (0x02)proof: rotl3(b) ^ 0x37 for each nonce byte

After the proof, derive the session key:

rk  = CRC32(nonce + proof + b"ttt_master_seed")
key = bytes([(rk >> (8 * (i % 4))) & 0xFF for i in range(8)])

Game packets (all encoded with session key):

TypeDirectionPayload
PT_SESS_OK (0x03)S→C4-byte session id
PT_BOARD (0x04)S→C9-byte board + 8-byte turn token
PT_MOVE (0x05)C→S1-byte cell + 8-byte turn token
PT_RESULT (0x06)S→C1-byte result + message
PT_FLAG (0x07)S→Cflag string
PT_ERROR (0x08)S→Cerror string

Step 3: Find the Vulnerability

Every PT_BOARD packet contains a fresh turn token. Each PT_MOVE must include the current token or it gets rejected.

The intended design: each token is valid for exactly one move, then invalidated.

The bug: the server keeps both the current token (cur_tok) and the previous one (prv_tok). It accepts either:

def token_ok(self, tok):
    if tok == self.cur_tok:  return True
    if tok == self.prv_tok:  return True   # BUG: old token never cleared
    return False

When you send a legitimate move with token_N, the server rotates:

cur_tok = token_N  →  becomes prv_tok
cur_tok = new token_N+1

If you immediately send a second move using the same token_N, it arrives while token_N is still prv_tok, so it gets accepted again.

The key effect: when the server processes a stale-token move, it applies your cell but skips the AI's response (it thinks the AI already played for that turn). You get a free move: AI does not counter.


Step 4: Exploit

The trick: send a legitimate move and a stale move back-to-back using the same token. The legitimate move gets a normal AI response. The stale move is free; AI skips.

Winning sequence (5 moves, AI only responds to 2):

P(0): legit  →  AI plays 4 (center)
P(2): STALE  →  AI skips       board: X{0,2}   O{4}
P(6): legit  →  AI plays 1 (blocks 0-1-2 fork threat)
P(8): STALE  →  AI skips       board: X{0,2,6,8}  O{1,4}
P(5): legit  →  2-5-8  WIN

After the two free moves, X sits on all four corners of one side (0,2,6,8) while O only has 1,4. X threatens both 2-5-8 and 6-7-8; AI can only block one.

Token usage:

  • tok_a used twice: P(0) legit (as cur_tok), then P(2) stale (as prv_tok)
  • tok_c used twice: P(6) legit (as cur_tok), then P(8) stale (as prv_tok)
  • tok_e used once: P(5) legit → win

Step 5: Solve Script

#!/usr/bin/env python3
import socket, struct
from zlib import crc32 as _crc32

HOST, PORT = "129.212.225.109", 31337
MAGIC = b"\xC0\xDE"
PT_HELLO, PT_PROOF, PT_SESS_OK = 0x01, 0x02, 0x03
PT_BOARD, PT_MOVE, PT_RESULT, PT_FLAG, PT_ERROR = 0x04, 0x05, 0x06, 0x07, 0x08

def crc32(d): return _crc32(d) & 0xFFFFFFFF

def encode(data, key, seq, ptype):
    return bytes([data[i] ^ key[(seq+i)%8] ^ ((ptype*7+i)&0xFF) for i in range(len(data))])

decode = encode

def make_tag(key, seq, ptype, raw):
    return crc32(key + struct.pack("

Diskusjon

0 kommentarer

Logg inn for å kommentere, svare og nevne skribenten eller andre lesere med @.

  1. Laster kommentarer...

Utforsk flere innlegg i bloggarkivet, eller bli med og skriv dine egne.