HTB: SokobanHTB Challenge

SokobanHTB - HackTheBox Challenge Writeup

Challenge Information

FieldValue
NameSokobanHTB
CategoryReversing / Misc
DifficultyEasy
Authord3vn0mi

Description

Sokoban is a great logic game, just push the boxes on X marks and win the flag! Oh wait, someone placed the box outside the walls…

The challenge ships a statically-linked Windows PE (SFML/OpenGL, no dynamic dependencies) implementing a small Sokoban game. The hint in the description (“someone placed the box outside the walls”) signals that the flag isn’t obtained by actually playing the game to a win state — it’s obtained by reverse engineering the win-condition logic and the level data baked into the binary, entirely statically.

Solution

The binary never needs to be executed. The full solve path is:

  1. Recover the real executable from the distributed archive (the staged copy was a 0-byte placeholder).
  2. Locate and decode the hardcoded 7×8 level grid stored as SIMD loads in .rdata.
  3. Identify the tile legend by tracing which texture each switch case renders (rather than guessing).
  4. Locate the win-check that fires once every box sits on an X target, and the flag-decryption routine it triggers — a fully unrolled TEA cipher.
  5. Recover the TEA key, which is derived at runtime from the pixel coordinates of the box/target sprites rather than being a static constant.
  6. Reimplement TEA decryption in Python with the derived key to recover the flag offline.

Key Steps

1. Recovering the real binary

The extracted/staged executable was 0 bytes, but the original distribution archive was still present alongside it. Unzipping with the standard HTB archive password produced the genuine 1,030,656-byte PE32+ binary:

Terminal window
# The staged file was empty — the real payload was still in the original zip
unzip -P hackthebox /out/<session-uuid>.zip -d work/
file work/gamepwn_sokobanhtb/out/build/x64-release/SokobanHTB/SokobanHTB.exe
# PE32+ executable (GUI) x86-64, for MS Windows
# statically linked against SFML / OpenGL

2. Locating main and decoding the level grid

main sits at 0x1400045f0. Disassembly showed the 56 int32 level cells loaded not as scalar moves but as 14 movdqa (128-bit SIMD) loads straight out of .rdata into a stack buffer at [rbp+0xd0 .. rbp+0x1af]:

Terminal window
objdump -d --start-address=0x1400045f0 --stop-address=0x140004900 \
-M intel SokobanHTB.exe > main.asm
grep -n "movdqa" main.asm # confirms 14 x 128-bit loads = 56 x int32 cells

Capstone/pefile were used to pull the raw bytes and reshape them:

import pefile, struct
from capstone import *
from capstone.x86 import *
pe = pefile.PE("SokobanHTB.exe")
rdata = pe.get_memory_mapped_image() # rebased view
# 56 x int32 level data, discovered from the movdqa source operands
level = struct.unpack_from("<56i", rdata, offset)

The render loop’s bounding checks (ebx < 7 inner / esi < 8 outer, pointer advancing linearly through the buffer) pinned down the shape: 7 columns × 8 rows, row-major.

3. Determining the tile legend from the renderer, not by guessing

Each grid value drives a switch that selects a sprite texture to draw. Tracing which texture ID corresponds to which value gave an unambiguous legend:

ValueTile
1Wall
2X target
3Box
4Player

Note 2 (target) and 3 (box) are the reverse of the intuitive guess — assuming the opposite mapping made the decoded map render as nonsense, which is what flagged the mistake.

Decoded map:

x: 0123456
y0: .####..
y1: ##PX#..
y2: #.B.#..
y3: #.B.###
y4: #.B#..#
y5: #X...X#
y6: ##..###
y7: .####..

4. Finding the win-check and flag decryption

On a winning state, a 40-byte ciphertext blob is written dword-by-dword onto the stack at 0x140005751, then handed to a fully unrolled TEA decryption routine at 0x140003f00:

Terminal window
objdump -d --start-address=0x140003f00 --stop-address=0x140004320 \
-M intel SokobanHTB.exe

The routine’s constants confirmed standard TEA decryption:

delta = 0x9E3779B9
sum (init) = 0xC6EF3720 # = 32 * delta, i.e. sum after 32 rounds
add r9d, 0x61C88647 # 0x61C88647 == -delta mod 2^32 -> sum -= delta each round

5. Deriving the runtime TEA key

The key was not a compiled-in constant — it’s computed from where the box sprites end up on screen (0x1400056da):

k = [ (int)box0.y, (int)box0.x, (int)box1.x, (int)box2.x ]

Since a solved board has every box sitting exactly on an X target, the key values collapse to the three X-mark cell coordinates. Screen position for cell (col, row) is:

screen_x = col * 64 + 320
screen_y = row * 64 + 90

Reading the X marks off the decoded grid (3,1), (1,5), (5,5) gives the concrete pixel coordinates fed into the key.

6. Offline TEA decryption

import struct
M = 0xFFFFFFFF
DELTA = 0x9E3779B9
def tea_decrypt(v0, v1, key, rounds=32):
s = (DELTA * rounds) & M # 0xC6EF3720 for 32 rounds
for _ in range(rounds):
v1 = (v1 - (((v0 << 4) + key[2]) ^ (v0 + s) ^ ((v0 >> 5) + key[3]))) & M
v0 = (v0 - (((v1 << 4) + key[0]) ^ (v1 + s) ^ ((v1 >> 5) + key[1]))) & M
s = (s - DELTA) & M
return v0, v1
# key derived from the three X-mark screen coordinates
key = [k0, k1, k2, k3]
blob = b"...40 bytes of ciphertext recovered from the win-state stack write..."
out = b""
for i in range(0, len(blob), 8):
v0, v1 = struct.unpack_from("<II", blob, i)
d0, d1 = tea_decrypt(v0, v1, key)
out += struct.pack("<II", d0, d1)
print(out.rstrip(b"\x00"))

Running this against the recovered ciphertext and derived key produced the flag:

HTB{REDACTED}

Tools Used

  • pefile — PE parsing, section/RVA resolution, memory-mapped image extraction
  • capstone — x86-64 disassembly for locating and tracing key routines
  • objdump (-d -M intel) — bulk static disassembly of main, the level-load code, and the TEA routine
  • unzip — recovering the real binary from the original password-protected archive
  • Python 3 — level-grid unpacking, key derivation, and TEA re-implementation

Key Learnings

  • Don’t trust a 0-byte staged artifact — check for the original distribution archive before assuming a challenge is broken; the real payload was sitting right next to it.
  • Derive tile semantics from renderer behavior, not intuition. The box/target texture IDs were swapped relative to the “obvious” guess; verifying against which sprite each switch case actually draws avoided a dead end.
  • A “win screen” flag isn’t always static. The TEA key here was derived from in-game state (box/target screen coordinates) rather than hardcoded, meaning the challenge could be solved fully statically only by also reverse engineering the key-derivation logic — not just the cipher.
  • Standard TEA constants are recognizable at a glance: 0x9E3779B9 (delta) and 0xC6EF3720 (32 * delta) are strong fingerprints for identifying TEA/XTEA in disassembly even without symbol names.
  • The challenge never had to be run. All information needed — grid layout, tile legend, cipher, and key derivation — was recoverable through pure static analysis of the binary.