Grecian Battleship
Grecian Battleship
Platform: CUSTOM | Category: Crypto | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: clue_driven_cipher_identification, coordinate_translation, polybius_square_decoding, pyinstaller_unpacking, python_bytecode_reversing
Summary
Walkthrough of the CUSTOM Crypto Grecian Battleship. 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 Pyth
title: "Grecian Battleship" id: 20260410_metactf_grecian_battleship category: crypto difficulty: easy event: metactf tags: ["pyinstaller", "elf", "coordinate_cipher", "polybius_square", "dawgctf", "tkinter", "bytecode_reversing", "fixed_move_script"] techniques: ["clue_driven_cipher_identification", "coordinate_translation", "polybius_square_decoding", "pyinstaller_unpacking", "python_bytecode_reversing"] tools: ["python3", "pyinstxtractor", "pydisasm"] indicators: ["PyInstaller-packed Tkinter game with no embedded flag string", "5x5 board paired with coordinates", "challenge title uses 'Grecian", "hardcoded move list in bytecode", "extracted coordinates become valid Polybius pairs after 0-based to 1-based conversion"]
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:
text read-only • copy allowed301: 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:
text read-only • copy allowed1058: 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:
text read-only • copy allowed(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:
text read-only • copy allowed(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:
text read-only • copy allowed(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:
text read-only • copy allowed35 34 32 11 22 42 45 33 15 44 -
Decode these pairs with a standard 5x5 Polybius square (
I/Jmerged):text read-only • copy allowed11=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:
text read-only • copy allowed35=P 34=O 32=M 11=A 22=G 42=R 45=U 33=N 15=E 44=T -
This yields:
text read-only • copy allowedPOMAGRUNET -
Submit it in flag format, lowercase as accepted by the challenge:
text read-only • copy allowedDawgCTF{REDACTED}
python read-only • copy allowed#!/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()
Key Takeaways / Lessons
- Mastering clue_driven_cipher_identification, coordinate_translation, polybius_square_decoding, pyinstaller_unpacking, python_bytecode_reversing was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-04-10 • CUSTOM • Grecian Battleship • Difficulty: Easy