cryptofreeeasy

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/
pyinstaller_unpackingpython_bytecode_reversingclue_driven_cipher_identificationpolybius_square_decodingcoordinate_translation

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

  1. Unpack or disassemble the PyInstaller binary.

  2. Inspect the embedded Python bytecode and locate self.move_script.

  3. 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)
  4. 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) -> 44

    So the Polybius pairs are:

    35 34 32 11 22 42 45 33 15 44
  5. Decode these pairs with a standard 5x5 Polybius square (I/J merged):

    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=Z

    Applying that mapping:

    35=P 34=O 32=M 11=A 22=G 42=R 45=U 33=N 15=E 44=T
  6. This yields:

    POMAGRUNET
  7. 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