HTB: Arno Challenge
Arno - HackTheBox Challenge Writeup
Challenge Information
| Field | Value |
|---|---|
| Name | Arno |
| Category | Mobile |
| Difficulty | Easy |
| Author | d3vn0mi |
Description
Arno Dorian is known for his memorable quotes, but he also has a knack for letting his sharp tongue get him into trouble, often saying the wrong thing at the worst possible moments. Can you make him say the magic words?
Despite the site listing it under a generic/forensics-sounding header, Arno is actually a Unity mobile game shipped as an APK. The “magic words” turn out to be an AES-encrypted flag baked into the app’s IL2CPP metadata.
Solution
The provided download initially looked broken — a 0-byte Arno.apk. Re-fetching the challenge archive (rather than treating it as an OSINT dead end) produced the real, correctly sized APK, protected with the standard HTB zip password. Unpacking it revealed a Unity IL2CPP build: the interesting logic lives in libil2cpp.so plus global-metadata.dat, not in readable Java/Kotlin bytecode, since Unity strips the C# assemblies down to native code and a metadata blob at build time.
With no Il2CppDumper available in the environment, the metadata header was parsed directly in Python to locate the string, type, and field-default-value tables. That surfaced exactly one interesting user class, FlagControl, exposing GetKey, GetIV, GetFlag, and DecryptFlag methods — a strong signal the flag is decrypted at runtime from embedded key material rather than stored in plaintext.
Recovering the actual byte arrays required navigating two IL2CPP quirks:
- The
Il2CppFieldDefaultValuetable is ordered(fieldIndex, typeIndex, dataIndex), not the more commonly assumed(fieldIndex, dataIndex, typeIndex)— using the “obvious” ordering pulls nothing. - Roslyn compiles constant byte arrays into a
<PrivateImplementationDetails>class where each backing field is named after the uppercase SHA-256 hash of its own contents. So rather than guessing which blob is which, each candidate array’s length and hash were checked to positively identify it.
That resolved five embedded arrays of sizes 16, 17, 32, 38, and 48 bytes. Two were decoys — the source file path string (\Assets\Scripts\FlagControl.cs, 38 bytes) and the literal class name FlagControl (17 bytes) — while the real material was the 32-byte AES key, 16-byte IV, and 48-byte ciphertext. Decrypting the ciphertext with AES-256-CBC and the recovered key/IV yielded the flag with clean PKCS#7 padding, confirming the correct blobs had been identified.
Key Steps
1. Recover the real APK (the initial download was a 0-byte artifact):
# Re-download and unzip the HTB challenge archive with the standard passwordunzip -o -P hackthebox a12c738d-....zip -d .ls -la && file *.apk2. Unpack the APK and confirm it’s a Unity IL2CPP build:
mkdir -p ex && unzip -o -q Arno.apk -d exls -la ex/assets/bin/Data/xxd ex/assets/bin/Data/Managed/Metadata/global-metadata.dat | head# libil2cpp.so (~54MB) + global-metadata.dat (v31) confirm Unity IL2CPP3. Parse the IL2CPP global-metadata header manually (no Il2CppDumper available):
import struct
d = open('ex/assets/bin/Data/Managed/Metadata/global-metadata.dat', 'rb').read()
# Probe each (offset, size) pair in the header and sanity-check# against the known string-literal region to identify each table.so, ss = 0x5d404, 0x11c31 # example: located string-literal sectionstrings_blob = d[so:so + ss]4. Locate FlagControl and its field default values (note the non-obvious tuple order):
# Il2CppFieldDefaultValue rows are (fieldIndex, typeIndex, dataIndex) —# NOT (fieldIndex, dataIndex, typeIndex) as one might assume.for row in field_default_values: field_index, type_index, data_index = row # ...5. Identify each embedded byte array by its SHA-256-named backing field:
import hashlib
# Roslyn names <PrivateImplementationDetails> fields after the# uppercase SHA-256 hash of their own byte contents.for blob in candidate_arrays: digest = hashlib.sha256(blob).hexdigest().upper() print(len(blob), digest) # match against field names to confirm identity6. Decrypt the flag with the recovered key/IV/ciphertext:
from Crypto.Cipher import AESfrom Crypto.Util.Padding import unpad
key = arrays[32] # 32-byte AES keyiv = arrays[16] # 16-byte IVct = arrays[48] # 48-byte ciphertext
cipher = AES.new(key, AES.MODE_CBC, iv)flag = unpad(cipher.decrypt(ct), AES.block_size)print(flag.decode()) # -> HTB{REDACTED}7. Submit the flag:
from htb_api import HTBClientc = HTBClient()c.submit_challenge_flag(challenge_id, "HTB{REDACTED}")Tools Used
unzip— extracting the HTB archive and the APK contents- Python 3 (
struct,hashlib) — manual IL2CPPglobal-metadata.datparsing (noIl2CppDumperavailable) pycryptodome(Crypto.Cipher.AES) — AES-256-CBC decryption with PKCS#7 unpaddingstrings,xxd— initial triage of the metadata blob and APK assets
Key Learnings
- A 0-byte download isn’t always a dead end to be OSINT’d — it can just be a bad fetch. Re-pulling the actual challenge archive resolved the “empty file” cleanly instead of chasing red herrings.
- Unity mobile challenges hide logic in IL2CPP metadata, not Java/Kotlin. When an APK’s
assets/bin/Datacontainslibil2cpp.soandglobal-metadata.dat, standard Android decompilers won’t show the interesting code — you need an IL2CPP-aware approach (ideallyIl2CppDumper, or manual header parsing when it’s unavailable). - IL2CPP’s
Il2CppFieldDefaultValuetable ordering is a known trap — it’s(fieldIndex, typeIndex, dataIndex), and assuming the more intuitive(fieldIndex, dataIndex, typeIndex)silently yields nothing. - Roslyn-compiled constant arrays are self-identifying.
<PrivateImplementationDetails>field names are the uppercase SHA-256 hash of the byte array they back — a reliable way to confirm which extracted blob is the key, IV, or ciphertext versus a decoy string constant, without guessing by size alone. - Decoy data is a real hazard in reversing challenges. Two of the five candidate byte arrays here were plausible-looking but irrelevant (a source path and a class name string); hash-verifying each candidate against its field name avoided wasting a decrypt attempt on the wrong blob.