TKOTF CTF - Titato Writeup
Titato - stripped 64-bit ELF binary.
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):
| # | Direction | Type | Payload |
|---|---|---|---|
| 1 | S→C | PT_HELLO (0x01) | 8-byte nonce |
| 2 | C→S | PT_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):
| Type | Direction | Payload |
|---|---|---|
PT_SESS_OK (0x03) | S→C | 4-byte session id |
PT_BOARD (0x04) | S→C | 9-byte board + 8-byte turn token |
PT_MOVE (0x05) | C→S | 1-byte cell + 8-byte turn token |
PT_RESULT (0x06) | S→C | 1-byte result + message |
PT_FLAG (0x07) | S→C | flag string |
PT_ERROR (0x08) | S→C | error 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_aused twice: P(0) legit (ascur_tok), then P(2) stale (asprv_tok)tok_cused twice: P(6) legit (ascur_tok), then P(8) stale (asprv_tok)tok_eused 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("Diskussion
0 kommentarer
Logga in för att kommentera, svara och nämna skribenten eller andra läsare med @.
- Laddar kommentarer...
Utforska fler inlägg i bloggarkivet, eller gå med och skriv egna.
