HTB: SokobanHTB Challenge
SokobanHTB - HackTheBox Challenge Writeup
Challenge Information
| Field | Value |
|---|---|
| Name | SokobanHTB |
| Category | Reversing / Misc |
| Difficulty | Easy |
| Author | d3vn0mi |
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:
- Recover the real executable from the distributed archive (the staged copy was a 0-byte placeholder).
- Locate and decode the hardcoded 7×8 level grid stored as SIMD loads in
.rdata. - Identify the tile legend by tracing which texture each
switchcase renders (rather than guessing). - 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.
- Recover the TEA key, which is derived at runtime from the pixel coordinates of the box/target sprites rather than being a static constant.
- 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:
# The staged file was empty — the real payload was still in the original zipunzip -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 / OpenGL2. 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]:
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 cellsCapstone/pefile were used to pull the raw bytes and reshape them:
import pefile, structfrom 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 operandslevel = 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:
| Value | Tile |
|---|---|
1 | Wall |
2 | X target |
3 | Box |
4 | Player |
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: 0123456y0: .####..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:
objdump -d --start-address=0x140003f00 --stop-address=0x140004320 \ -M intel SokobanHTB.exeThe routine’s constants confirmed standard TEA decryption:
delta = 0x9E3779B9sum (init) = 0xC6EF3720 # = 32 * delta, i.e. sum after 32 roundsadd r9d, 0x61C88647 # 0x61C88647 == -delta mod 2^32 -> sum -= delta each round5. 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 + 320screen_y = row * 64 + 90Reading 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 = 0xFFFFFFFFDELTA = 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 coordinateskey = [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 extractioncapstone— x86-64 disassembly for locating and tracing key routinesobjdump(-d -M intel) — bulk static disassembly ofmain, the level-load code, and the TEA routineunzip— 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) and0xC6EF3720(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.