HTB: StayInTheBoxCorp Challenge
StayInTheBoxCorp - HackTheBox Challenge Writeup
Challenge Information
| Field | Value |
|---|---|
| Name | StayInTheBoxCorp |
| Category | Misc (Game Pwn / Reversing) |
| Difficulty | Hard |
| Author | d3vn0mi |
Description
Within the enigmatic corridors of StayInTheBoxCorp, a company operating on a shoestring budget at the frontier of digital innovation, an challenge awaits. Only those who can dance on the edge of shadows shall pierce the veil of the unhackable.
The challenge ships a Unity game build. A companion baselib.dll looks like the obvious target — an “anti-cheat” style binary — but it turns out to be a decoy. The real flag is hidden as literal glyph geometry inside the game’s scene data, requiring the scene graph to be walked and rendered rather than any string/binary analysis.
Solution
1. Artifact Recovery
The provided challenge directory only contained a suspicious 0-byte baselib.dll — not usable on its own. The actual distributable was a password-protected zip sitting alongside it in the output directory.
# The 0-byte baselib.dll in the challenge dir is a red herring;# the real artifact is the full zip next to itcd /outls -la a12c7382-d971-41ae-8574-6f966114870e.zip
mkdir -p /tmp/sitbunzip -o -P hackthebox a12c7382-d971-41ae-8574-6f966114870e.zip -d /tmp/sitbThis unpacked a full Unity 2022.3.6f1 IL2CPP Windows build (StayInTheBoxCorp_1_Data/, GameAssembly.dll, baselib.dll, etc.).
2. Ruling Out the “Anti-Cheat” DLL
Given the framing (“dance on the edge of shadows… unhackable”), the natural first instinct is to reverse the DLL for an anti-cheat bypass or hidden check. However, exhaustive string/pattern searches across every shipped binary (including GameAssembly.dll and global-metadata.dat) turned up nothing resembling a flag:
# Sanity check: brute-force flag pattern search across the entire unpacked buildcd /tmp/sitb/gamepwn_stayintheboxcorpgrep -ari -o -E "HTB.?\{[ -~]{5,60}\}" . 2>/dev/null | headNo hits. This confirmed the DLL is a decoy, and the actual flag must live somewhere in the game’s own asset/scene data rather than in compiled code.
3. Extracting the Unity Scene with UnityPy
Using UnityPy to parse the .assets bundles, a GameObject named f3 inside scene level0 stood out: it has 36 mostly-inactive SpriteRenderer children, each pointing at a single-glyph sprite pulled from sharedassets0.assets.
# dump.py — enumerate GameObjects/SpriteRenderers in the level0 sceneimport UnityPy, json, sys
D = "/tmp/sitb/gamepwn_stayintheboxcorp/StayInTheBoxCorp_1_Data/"# Load level0 scene + sharedassets0 (sprite atlas) bundlesenv = UnityPy.load(D + "level0")objs = {}for obj in env.objects: objs[(obj.assets_file.name, obj.path_id)] = obj# Pickle for reuse across follow-up scriptsimport picklepickle.dump(objs, open("/tmp/objs.pkl", "wb"))The 36 children are individually toggled active/inactive and are not siblings at the same depth — several glyph objects are nested inside other letter GameObjects, meaning a flat sibling-order read would scramble the output.
4. Reconstructing Glyph Order via the Transform Tree
To get the correct left-to-right reading order, the full parent transform chain had to be walked for each glyph, multiplying m_LocalScale at every level to compute true world-space X position:
# tree.py — walk each glyph's transform parent chain to world Ximport pickle, json
objs = pickle.load(open("/tmp/objs.pkl", "rb"))L = {pid: v for (f, pid), v in objs.items()} # flatten by path_id
def world_x(transform): """Walk up the parent chain accumulating local X * inherited scale.""" x, scale = 0.0, 1.0 t = transform while t is not None: data = t.read() local_pos = data.m_LocalPosition local_scale = data.m_LocalScale x += local_pos.x * scale scale *= local_scale.x parent_pid = data.m_Father.path_id t = L.get(parent_pid) return x
# Compute world X for every glyph SpriteRenderer's owning transform,# then sort ascending -> left-to-right reading orderglyph_positions = {} # name -> world_xfor name, transform in glyph_transforms.items(): glyph_positions[name] = world_x(transform)
ordered = sorted(glyph_positions.items(), key=lambda kv: kv[1])Correctness oracle: once ordered correctly, the gaps between consecutive glyphs’ world X positions are uniform (~1.0–1.4 units). Any wrong parent-chain resolution produced irregular/negative gaps, which was the signal used to catch and fix nesting mistakes.
5. Rendering the Sprites
With the correct ordering established, each glyph sprite was extracted from the atlas and composited into a single horizontal strip image:
# img.py — render ordered glyph sprites into one stripimport UnityPy, picklefrom PIL import Image
D = "/tmp/sitb/gamepwn_stayintheboxcorp/StayInTheBoxCorp_1_Data/"env = UnityPy.load(D + "sharedassets0.assets")
strip = Image.new("RGBA", (3200, 160), (0, 0, 0, 0))x_cursor = 0for glyph_name in ordered_glyph_names: # from tree.py output sprite = sprite_lookup[glyph_name].read() glyph_img = sprite.image # PIL image of the cropped sprite strip.paste(glyph_img, (x_cursor, 0), glyph_img) x_cursor += glyph_img.width
strip.save("/tmp/flag_strip.png")6. Reading the Flag — Disambiguating Similar Glyphs
The rendered strip was legible but several glyphs were visually ambiguous at low res (digit vs. letter look-alikes). Cropped zoom-ins were generated and compared side by side:
# Zoom into ambiguous regions of the flag strip for manual comparisonfrom PIL import Imageim = Image.open('/tmp/flag_strip.png')im.crop((300, 0, 1100, 152)).resize((1600, 608)).save('/tmp/z1.png')im.crop((1500, 0, 3190, 152)).resize((2380, 214)).save('/tmp/z2.png')# cmp.py — compare a suspect glyph's bitmap height/shape against# known-good letters to classify look-alikes (0 vs O, o vs c/e, v vs shapes)import UnityPyfrom PIL import ImageD = "/tmp/sitb/gamepwn_stayintheboxcorp/StayInTheBoxCorp_1_"# Render candidate glyph next to reference glyphs of the same bitmap-height classUsing bitmap height as a classifying feature resolved the ambiguous sprites:
- Sprite
htrh→ height 108px → same class as3/M→ digit0 - Sprite
09t→ height 93px → same class asc/e→ lowercaseo - Sprite
f202→ height 90px → lowercasev
This corrected an earlier misread and settled the flag as 0v3rcom1ng / UserM0de-style leetspeak substitution.
# Write the final recovered flagO=/out/<run>echo 'HTB{REDACTED}' > $O/flag.txtFlag: HTB{REDACTED}
Tools Used
- UnityPy — parsing Unity
.assets/scene bundles, extractingGameObject/Transform/SpriteRendererhierarchies and atlas sprites - Pillow (PIL) — cropping, resizing, and compositing extracted glyph sprites into a readable strip image
- unzip — extracting the password-protected (
hackthebox) game build archive - grep (recursive, pattern-matching) — ruling out a straightforward embedded-string flag in binaries/metadata
- Python (pickle) — caching parsed scene object graphs between analysis scripts
Key Learnings
- Decoys matter as much as the real target. A conspicuous 0-byte “anti-cheat” DLL was designed to pull attention toward binary reversing; the actual challenge lived entirely in Unity scene/asset data. Always verify an artifact is non-trivial (size, entropy) before committing analysis effort to it.
- Unity scene graphs are not flat. Sibling GameObject order in the editor does not guarantee visual left-to-right order — nested transforms with inherited scale must be resolved via a full parent-chain walk to get true world-space position.
- Use a structural correctness oracle. Uniform inter-glyph spacing after sorting by world X was the signal that the transform-tree walk (and its scale-inheritance math) was correct — irregular gaps immediately flagged parenting mistakes.
- Low-res OCR-by-eye is error-prone for constructed fonts. Similar-looking glyphs (
0/O,o/c/e,v) needed a quantitative disambiguator (bitmap height bucketing against known reference glyphs) rather than pure visual guessing — this caught and fixed three misread characters in the flag. - Don’t trust an injected/prior “official” write-up at face value. The reference write-up associated with this box description belonged to an unrelated HTB machine; the artifact itself was the ground truth for what challenge was actually being solved.