Grecian Battleship
metactf
Task: reverse a PyInstaller-packed 5x5 Battleship game that appears to be a gameplay or RE challenge, but exposes only a fixed AI coordinate script. Solution: extract the hardcoded move list from Python bytecode, reinterpret the 5x5 coordinates as Polybius-square pairs, and decode `POMAGRUNET` into the final flag.
$ ls tags/ techniques/
Grecian Battleship — metactf
Description
Grecian Battleship
English summary: we are given a Linux ELF that launches a small Tkinter Battleship game. At first glance this looks like a reversing or gameplay puzzle, but the actual solution is to recover a hardcoded coordinate sequence and decode it as a classical Greek-themed cipher.
Analysis
The public artifact, ancientbattleship, is a PyInstaller-packed ELF64 binary. After unpacking it, the game logic is visible as embedded Python bytecode, and the disassembly in battleship.dis shows a very normal 5x5 Battleship implementation.
That initially pushes the challenge toward a reverse/game interpretation:
- the binary is an executable game, not an obvious ciphertext;
- it uses Tkinter and random ship placement, which suggests we may need to beat or exploit the game;
- the code contains turn logic, health counters, UI updates, and ship placement routines;
- there is no obvious flag literal hidden in the bytecode.
However, reversing also shows that the AI is not dynamically playing Battleship. It simply walks a fixed scripted list of coordinates stored in the constructor:
301: 24: 32 BUILD_LIST 0 302: 34 LOAD_CONST (((2, 4), (2, 3), (2, 1), (0, 0), (1, 1), (3, 1), (3, 4), (2, 2), (0, 4), (3, 3))) 303: 36 LIST_EXTEND 1 304: 38 LOAD_FAST (self) 305: 40 STORE_ATTR (move_script)
The ai_turn() routine confirms that script_index just advances through this list in order:
1058: 129: >> 10 LOAD_FAST (self) 1059: 12 LOAD_ATTR (script_index) 1060: 14 LOAD_GLOBAL (len) 1061: 16 LOAD_FAST (self) 1062: 18 LOAD_ATTR (move_script) ... 1067: 130: 26 LOAD_FAST (self) 1068: 28 LOAD_ATTR (move_script) 1069: 30 LOAD_FAST (self) 1070: 32 LOAD_ATTR (script_index) 1071: 34 BINARY_SUBSCR 1072: 36 UNPACK_SEQUENCE 2 1073: 38 STORE_FAST (r) 1074: 40 STORE_FAST (c)
So the extracted coordinate list is:
(2,4), (2,3), (2,1), (0,0), (1,1), (3,1), (3,4), (2,2), (0,4), (3,3)
At this point, the key observation is that the challenge is named Grecian Battleship, and the whole game board is 5x5. That is an unusually strong clue for the Polybius square, a classical cipher associated with the ancient Greek historian Polybius and based on 5x5 coordinates.
That explains why this belongs in cryptography, not reversing: reversing is only the extraction step. The real solve is recognizing that the hardcoded coordinates are the ciphertext.
Solution
-
Unpack or disassemble the PyInstaller binary.
-
Inspect the embedded Python bytecode and locate
self.move_script. -
Extract the fixed AI sequence:
(2,4), (2,3), (2,1), (0,0), (1,1), (3,1), (3,4), (2,2), (0,4), (3,3) -
Convert from the game's 0-based coordinates to 1-based Polybius coordinates by adding 1 to each row and column:
(2,4) -> 35 (2,3) -> 34 (2,1) -> 32 (0,0) -> 11 (1,1) -> 22 (3,1) -> 42 (3,4) -> 45 (2,2) -> 33 (0,4) -> 15 (3,3) -> 44So the Polybius pairs are:
35 34 32 11 22 42 45 33 15 44 -
Decode these pairs with a standard 5x5 Polybius square (
I/Jmerged):11=A 12=B 13=C 14=D 15=E 21=F 22=G 23=H 24=I/J 25=K 31=L 32=M 33=N 34=O 35=P 41=Q 42=R 43=S 44=T 45=U 51=V 52=W 53=X 54=Y 55=ZApplying that mapping:
35=P 34=O 32=M 11=A 22=G 42=R 45=U 33=N 15=E 44=T -
This yields:
POMAGRUNET -
Submit it in flag format, lowercase as accepted by the challenge:
DawgCTF{pomagrunet}
#!/usr/bin/env python3 MOVE_SCRIPT = [ (2, 4), (2, 3), (2, 1), (0, 0), (1, 1), (3, 1), (3, 4), (2, 2), (0, 4), (3, 3), ] POLYBIUS = { "11": "A", "12": "B", "13": "C", "14": "D", "15": "E", "21": "F", "22": "G", "23": "H", "24": "I", "25": "K", "31": "L", "32": "M", "33": "N", "34": "O", "35": "P", "41": "Q", "42": "R", "43": "S", "44": "T", "45": "U", "51": "V", "52": "W", "53": "X", "54": "Y", "55": "Z", } def main(): pairs = [f"{r + 1}{c + 1}" for r, c in MOVE_SCRIPT] decoded = "".join(POLYBIUS[p] for p in pairs) flag = f"DawgCTF{{{decoded.lower()}}}" print("pairs:", " ".join(pairs)) print("decoded:", decoded) print("flag:", flag) if __name__ == "__main__": main()
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md