HTB: CubeBreaker Challenge

CubeBreaker - HackTheBox Challenge Writeup

Challenge Information

PropertyValue
NameCubeBreaker
CategoryMisc / Game Reversing (Unity)
DifficultyMedium
Authord3vn0mi

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:

Terminal window
# Direct string search across all extracted files
grep -rao 'HTB{REDACTED}]*}' . 2>/dev/null
# Look for obfuscation / anti-cheat markers and readable strings
strings -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

Terminal window
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-named FieldRVA byte blobs — Beebyte moves string data into RVA-initialized byte arrays instead of the #US stream.

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:

Terminal window
pip3 install --break-system-packages dnfile dncil UnityPy

Using dnfile to parse the assembly’s metadata tables and dncil to decode CIL instructions, I located a .ctor (constructor) that:

  1. Loads a 128-byte key via RuntimeHelpers.InitializeArray (a compiler-generated FieldRVA blob).
  2. Loads a 17,792-byte data blob, also via InitializeArray.
  3. Calls a method at token 0x06002920 — the decryption routine.
import dnfile
from dncil.cil.body import CilMethodBody
from 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 FieldRVA
data = 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

  1. Extract the real artifact from /out/<id>.zip (password hackthebox) — the shipped UnityPlayer.dll is a decoy stub.
  2. Rule out asset-based hiding spots first (textures, sprites, scene MonoBehaviours) using UnityPy.
  3. Fingerprint the obfuscator via strings — Beebyte.Obfuscator.* type names + missing #US stream + thousands of $$field-N FieldRVA blobs.
  4. Use pure-Python dnfile (metadata parsing) + dncil (CIL disassembly) to statically read IL when no mono/dotnet toolchain is available.
  5. Locate the .ctor loading a 128-byte key and a 17,792-byte blob via RuntimeHelpers.InitializeArray, and disassemble the decryptor it calls.
  6. Reconstruct the algorithm (data[i] ^= key[i % 128] → UTF-8 decode) and replay it in Python against the extracted bytes.
  7. 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) parser
  • dncil — pure-Python CIL instruction decoder/disassembler
  • UnityPy — Unity asset bundle/scene parsing and texture export
  • strings / 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 UnityPy and 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 #US string streams plus thousands of $$field-N/FieldRVA blobs is a strong Beebyte Obfuscator signature — knowing this immediately explains why plain strings/grep come up empty and redirects effort toward FieldRVA byte extraction instead of chasing decompiled string tables.
  • Pure-Python dnfile/dncil is a viable substitute for dnSpy/ilspy in 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.