← Back to Writeups
HTBEasyCryptography

Grecian Battleship

D3v0o0Nu119/2/20265 min read
#cryptography#htb#easy#clue_driven_cipher_identificat#coordinate_translation#polybius_square_decoding#pyinstaller_unpacking#python_bytecode_reversing

Grecian Battleship

Platform: Metactf | Category: Cryptography | Type: Challenge | Difficulty: Easy | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-10 | Status: Solved Techniques: clue_driven_cipher_identification, coordinate_translation, polybius_square_decoding, pyinstaller_unpacking, python_bytecode_reversing

Summary

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.

Recon

Port scan

bash
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: metactf | ID: 20260410_metactf_grecian_battleship
  • Tags: pyinstaller, elf, coordinate_cipher, polybius_square, dawgctf, tkinter, bytecode_reversing, fixed_move_script
  • 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
  • Source: 20260410_metactf_grecian_battleship.md

Foothold

Vulnerability / Misconfiguration

  1. clue_driven_cipher_identification
  2. coordinate_translation
  3. polybius_square_decoding
  4. pyinstaller_unpacking
  5. python_bytecode_reversing
bash
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

bash
sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
bash
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • clue_driven_cipher_identification
  • coordinate_translation
  • polybius_square_decoding
  • pyinstaller_unpacking
  • python_bytecode_reversing
  • Tags: pyinstaller, elf, coordinate_cipher, polybius_square, dawgctf, tkinter, bytecode_reversing, fixed_move_script

Original Writeup

<details><summary>Click to expand original content</summary>

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
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:

text
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:

text
(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:

    text
    (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:

    text
    (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:

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

    text
    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:

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

    text
    POMAGRUNET
  7. Submit it in flag format, lowercase as accepted by the challenge:

    text
    DawgCTF{REDACTED}
python
#!/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()
</details>

signed by XESXOR