HTB: CubeBreaker Challenge
CubeBreaker - HackTheBox Challenge Writeup
Challenge Information
| Property | Value |
|---|---|
| Name | CubeBreaker |
| Category | Misc / Game Reversing (Unity) |
| Difficulty | Medium |
| Author | d3vn0mi |
Description
Make love, not war. Break cubes, not hearts.
The challenge ships a UnityPlayer.dll in the task description, but that file is a 0-byte stub. The real artifact is a password-protected zip (hackthebox) containing a full Windows Unity game build. There’s no network target — this is a pure static reverse-engineering / forensics challenge against the shipped game binaries.
Solution
The core insight for CubeBreaker is that the flag is not hidden anywhere obvious (textures, scenes, TextAssets) — it’s embedded as an XOR-encrypted, base64-encoded PNG inside the game’s compiled C# assembly, protected by the Beebyte Obfuscator. Solving it requires disassembling raw CIL bytecode with pure-Python tooling (no mono/dotnet/ilspy available) to recover the decryption routine, then replaying it in Python.
Step 1 — Find the real artifact
The handed UnityPlayer.dll was a decoy (0 bytes). The actual challenge bundle lived under /out/<id>.zip, protected with the password hackthebox. Extracting it revealed a standard Unity Windows build layout:
CubeBreaker/└── HackTheBox CubeBreaker_Data/ ├── Managed/ │ ├── Assembly-CSharp.dll # game logic — the interesting target │ ├── UnityEngine.*.dll │ └── ACTk.Runtime.dll # Anti-Cheat Toolkit (obfuscation clue) ├── level0, sharedassets0.assets, resources.assets, ... └── ...The presence of ACTk.Runtime.dll (Anti-Cheat Toolkit) plus a heavily obfuscated Assembly-CSharp.dll strongly suggested the flag was buried in game logic rather than raw assets.
Step 2 — Rule out the easy paths
Before committing to a full disassembly, I checked the cheap options first:
# Direct string search across all extracted filesgrep -rao 'HTB{REDACTED}]*}' . 2>/dev/null
# Look for obfuscation / anti-cheat markers and readable stringsstrings -n 5 Managed/Assembly-CSharp.dll | grep -iE 'Obscured|cube|breaker|score|flag'No hits. I then used UnityPy to enumerate and export the game’s serialized assets — textures, sprites, MonoBehaviours, and scene data — on the theory that HTB game-RE challenges sometimes draw the flag directly into a hidden win-screen texture:
import UnityPy
for fn in ["level0", "sharedassets0.assets", "resources.assets"]: env = UnityPy.load(fn) for obj in env.objects: if obj.type.name in ("Texture2D", "Sprite", "MonoBehaviour", "TextAsset"): print(obj.type.name, obj.path_id)78 textures and 3 sprites were exported and manually inspected — all were legitimate game art / post-processing LUTs, no flag. MonoBehaviour serialized data was also manually walked (no typetree was available, so length-prefixed strings had to be parsed by hand from the raw bytes) — nothing in the scene data either. This confirmed the flag lives inside the compiled assembly itself.
Step 3 — Identify the obfuscator
strings -n 4 Managed/Assembly-CSharp.dll | grep -iE 'Beebyte|Obfuscator|ConfuserEx|Koi'The assembly contained Beebyte.Obfuscator.* MonoScript type names, confirming Beebyte Obfuscator (a commercial Unity/.NET obfuscator) rather than ConfuserEx. Two structural fingerprints reinforced this:
- No
#US(User Strings) metadata stream — literal strings aren’t stored as plaintext constants. - ~10,000
$$field-N-namedFieldRVAbyte blobs — Beebyte moves string data into RVA-initialized byte arrays instead of the#USstream.
Class names for gameplay types (Cube, CubeCounter, CubeOOB, GamePlayer) survived obfuscation and were readable, which helped scope which methods were worth disassembling.
Step 4 — Disassemble the IL with pure-Python tooling
No mono, dotnet, or ilspy was available in the sandbox, and there was no apt/sudo access to install one. I installed pure-Python CIL tooling instead:
pip3 install --break-system-packages dnfile dncil UnityPyUsing dnfile to parse the assembly’s metadata tables and dncil to decode CIL instructions, I located a .ctor (constructor) that:
- Loads a 128-byte key via
RuntimeHelpers.InitializeArray(a compiler-generated FieldRVA blob). - Loads a 17,792-byte data blob, also via
InitializeArray. - Calls a method at token
0x06002920— the decryption routine.
import dnfilefrom dncil.cil.body import CilMethodBodyfrom dncil.cil.body.reader import CilMethodBodyReaderBase
pe = dnfile.dnPE("Managed/Assembly-CSharp.dll")md = pe.net.mdtables
# Locate the .ctor initializing the key + data FieldRVA blobs,# then disassemble method token 0x06002920 to recover the# decryption loop's actual operation.method = pe.net.mdtables.MethodDef.rows[0x06002920 & 0xFFFFFF - 1]# ... instruction walk confirms: data[i] ^= key[i % 128]Reading the disassembled body of 0x06002920 showed a simple repeating-key XOR loop followed by a UTF-8 decode:
// Reconstructed from IL:for (int i = 0; i < data.Length; i++) data[i] ^= key[i % key.Length]; // key.Length == 128
string result = Encoding.UTF8.GetString(data);Step 5 — Replay the decryption in Python
With the algorithm identified, I extracted the raw key and data bytes (dumped directly from the FieldRVA blobs via dnfile) and replayed the XOR + decode in plain Python:
key = bytes([...]) # 128 bytes, extracted from FieldRVAdata = bytearray([...]) # 17792 bytes, extracted from FieldRVA
for i in range(len(data)): data[i] ^= key[i % len(key)]
result = data.decode("utf-8")print(result[:60])# -> "iVBORw0KGgo..." (base64-encoded PNG header)Step 6 — Decode and read the flag
The decrypted string was a base64-encoded PNG. Decoding it produced a 1280×720 image — the game’s win screen — with the flag rendered directly in green text:
import base64
with open("hidden.png", "wb") as f: f.write(base64.b64decode(result))Opening hidden.png revealed the flag:
HTB{REDACTED}(“break the boundaries”)
Key Steps
- Extract the real artifact from
/out/<id>.zip(passwordhackthebox) — the shippedUnityPlayer.dllis a decoy stub. - Rule out asset-based hiding spots first (textures, sprites, scene MonoBehaviours) using
UnityPy. - Fingerprint the obfuscator via
strings—Beebyte.Obfuscator.*type names + missing#USstream + thousands of$$field-NFieldRVA blobs. - Use pure-Python
dnfile(metadata parsing) +dncil(CIL disassembly) to statically read IL when nomono/dotnettoolchain is available. - Locate the
.ctorloading a 128-byte key and a 17,792-byte blob viaRuntimeHelpers.InitializeArray, and disassemble the decryptor it calls. - Reconstruct the algorithm (
data[i] ^= key[i % 128]→ UTF-8 decode) and replay it in Python against the extracted bytes. - Decode the resulting base64 string as a PNG to reveal the flag rendered in the image.
Tools Used
dnfile— pure-Python .NET metadata (PE/CLR table) parserdncil— pure-Python CIL instruction decoder/disassemblerUnityPy— Unity asset bundle/scene parsing and texture exportstrings/grep— obfuscator fingerprinting and quick literal search- Python 3 (
base64, manual XOR replay)
Key Learnings
- A 0-byte handed file is a signal, not a dead end. When a challenge’s advertised artifact is empty, check for a companion archive (
/out/*.zip) — HTB game-RE challenges often ship the real build separately. - Rule out cheap hiding spots before committing to static RE. Textures, sprites, and serialized MonoBehaviour scene data are fast to check with
UnityPyand are common flag locations in other Unity challenges — eliminate them early so effort isn’t wasted disassembling a huge assembly if the flag were actually sitting in a texture. - Obfuscator fingerprinting narrows the search fast. Missing
#USstring streams plus thousands of$$field-N/FieldRVA blobs is a strong Beebyte Obfuscator signature — knowing this immediately explains why plainstrings/grepcome up empty and redirects effort toward FieldRVA byte extraction instead of chasing decompiled string tables. - Pure-Python
dnfile/dncilis a viable substitute fordnSpy/ilspyin restricted sandboxes. When no package manager/root access is available to install a full .NET decompiler, these libraries can still parse metadata tables and disassemble CIL well enough to hand-reconstruct a method’s logic (here, a straightforward repeating-key XOR). - Encrypted blobs decoding to base64 are worth decoding recursively. The final XOR-decrypted payload wasn’t the flag text itself — it was a base64 PNG. Always check whether a decrypted string is itself another encoding layer before concluding the search failed.