Arno
Arno
Platform: HackTheBox | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-02-08 | Status: Solved Techniques: aes_256_cbc_decryption, arm64_disassembly, il2cpp_metadata_extraction, static_data_extraction
Summary
Android APK (Unity IL2CPP game) themed around Assassin's Creed Unity. Arno Dorian — the game's protagonist — is "known for his memorable quotes". We need to make him "say the magic words". Inside the APK — an encrypted flag, with key and IV stored as static arrays in IL2CPP metadata.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
HackTheBox| ID:20260208_hackthebox_arno - Tags: encryption, android, apk, aes, unity, il2cpp, arm64, metadata_extraction
- Indicators: Unity IL2CPP Android APK, libil2cpp.so, global-metadata.dat, GetKey/GetIV/GetFlag methods, AES encryption with static key material
- Source:
20260208_hackthebox_arno.md
Foothold
Vulnerability / Misconfiguration
- Aes_256_cbc_decryption
- Arm64_disassembly
- Il2cpp_metadata_extraction
- Static_data_extraction
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- aes_256_cbc_decryption
- arm64_disassembly
- il2cpp_metadata_extraction
- static_data_extraction
- Tags: encryption, android, apk, aes, unity, il2cpp, arm64, metadata_extraction
Original Writeup
<details><summary>Click to expand original content</summary>Description
Android APK (Unity IL2CPP game) themed around Assassin's Creed Unity. Arno Dorian — the game's protagonist — is "known for his memorable quotes". We need to make him "say the magic words". Inside the APK — an encrypted flag, with key and IV stored as static arrays in IL2CPP metadata.
"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?"
Analysis
APK Structure
File Arno.apk inside challenge.zip (password: hackthebox). Key files:
| File | Description |
|---|---|
lib/arm64-v8a/libil2cpp.so | 54MB ARM64 ELF — compiled IL2CPP code |
assets/bin/Data/Managed/Metadata/global-metadata.dat | IL2CPP metadata v31 — class names, methods, string literals |
assets/bin/Data/data.unity3d | Unity asset bundle with game objects |
Game objects from data.unity3d: Quote, QuoteButton, quotereversed, Flag, Submit — indicate mechanics involving quotes and flag verification.
IL2CPP Metadata Recovery (Il2CppDumper)
IL2CPP compiles C# → IL → C++ → native ARM64, but preserves metadata (class names, methods, fields) in global-metadata.dat. Il2CppDumper recovers original signatures:
DOTNET_ROLL_FORWARD=LatestMajor dotnet Il2CppDumper.dll libil2cpp.so global-metadata.dat output/
Output: dump.cs, stringliteral.json, script.json
Key Class: FlagControl (TypeDefIndex 8092)
public class FlagControl : MonoBehaviour
{
public GameObject textField;
public List<string> quotes;
public void PopulateQuotes() { } // RVA: 0x16D135C
public void ShowQuote() { } // RVA: 0x16D1740
private void Start() { } // RVA: 0x16D1834
public byte[] GetKey() { } // RVA: 0x16D1838
public byte[] GetIV() { } // RVA: 0x16D18A8
public byte[] GetFlag() { } // RVA: 0x16D1918
public string DecryptFlag(byte[] key, byte[] iv, byte[] encryptedData) { } // RVA: 0x16D1988
public void .ctor() { } // RVA: 0x16D2120
}
The signatures immediately reveal the architecture: AES encryption with key, IV, and encrypted flag stored as static byte arrays. The DecryptFlag method takes all three parameters and returns a string.
ARM64 Disassembly (radare2)
Disassembling GetKey(), GetIV(), GetFlag() to determine array sizes:
GetKey() @ 0x16D1838:
mov w1, 0x20 ; allocate 32 bytes → AES-256 key
GetIV() @ 0x16D18A8:
mov w1, 0x10 ; allocate 16 bytes → standard AES IV (128-bit block)
GetFlag() @ 0x16D1918:
mov w1, 0x30 ; allocate 48 bytes → encrypted flag (3 AES blocks)
Each function follows the same pattern:
- Load pointer from GOT
- Create
byte[]of specified size - Call
RuntimeHelpers.InitializeArray()to copy static data
Static Arrays in PrivateImplementationDetails
In dump.cs (TypeDefIndex 8095-8100) found class <PrivateImplementationDetails> with fixed-size fields:
| Field | Size | Offset in metadata | Purpose |
|---|---|---|---|
__StaticArrayInitTypeSize=32 | 32 bytes | 0x3EBAB0 | AES-256 Key |
__StaticArrayInitTypeSize=16 | 16 bytes | 0x3EBA48 | AES IV |
__StaticArrayInitTypeSize=48 | 48 bytes | 0x3EBA78 | Encrypted Flag |
Sizes exactly match the disassembly data (32, 16, 48 bytes).
Solution
Step 1: Extracting Raw Bytes from global-metadata.dat
with open("global-metadata.dat", "rb") as f:
# AES-256 Key (32 bytes)
f.seek(0x3EBAB0)
key = f.read(32)
# AES IV (16 bytes)
f.seek(0x3EBA48)
iv = f.read(16)
# Encrypted Flag (48 bytes)
f.seek(0x3EBA78)
enc = f.read(48)
print(f"Key: {key.hex()}")
print(f"IV: {iv.hex()}")
print(f"Enc: {enc.hex()}")
Extracted values:
Key: cfdc33ccbee6dc775ba146b95d0fea6cbcc3ee3e5e76531d2cd79c140758f08d
IV: bbf5a8d7066fd51b43d959c044365cdf
Enc: 13ebf3953a9b8c13c6e5471f7eeaa0174b6c1fac41802002da16eb32fa88f63c570185a8bc218d9ef3ac03e218d30c55
Step 2: AES-256-CBC Decryption
#!/usr/bin/env python3
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
key = bytes.fromhex("cfdc33ccbee6dc775ba146b95d0fea6cbcc3ee3e5e76531d2cd79c140758f08d")
iv = bytes.fromhex("bbf5a8d7066fd51b43d959c044365cdf")
enc = bytes.fromhex("13ebf3953a9b8c13c6e5471f7eeaa0174b6c1fac41802002da16eb32fa88f63c570185a8bc218d9ef3ac03e218d30c55")
cipher = AES.new(key, AES.MODE_CBC, iv)
flag = unpad(cipher.decrypt(enc), 16).decode()
print(flag)
IL2CPP Reverse Engineering Methodology
C# Source → IL Bytecode → C++ (il2cpp) → Native ARM64 (libil2cpp.so)
+ global-metadata.dat (metadata v31)
Il2CppDumper recovers:
├── dump.cs — Class/method signatures with RVA addresses
├── script.json — RVA → name mapping for IDA/Ghidra
└── stringliteral.json — String literals
Workflow:
1. Il2CppDumper → get dump.cs (class names, methods, RVA)
2. Find interesting classes (FlagControl, DecryptFlag, etc.)
3. radare2/Ghidra → disassemble by RVA for implementation details
4. Extract static data from global-metadata.dat by offsets
5. Reproduce the logic (in this case — just AES decrypt)
</details>
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR