← Back to Writeups
HTBN/AReversing

Arno

XESXOR8/23/20265 min read
#reversing#htb#n/a

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
PortServiceVersionNotes
<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

  1. Aes_256_cbc_decryption
  2. Arm64_disassembly
  3. Il2cpp_metadata_extraction
  4. 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

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

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:

FileDescription
lib/arm64-v8a/libil2cpp.so54MB ARM64 ELF — compiled IL2CPP code
assets/bin/Data/Managed/Metadata/global-metadata.datIL2CPP metadata v31 — class names, methods, string literals
assets/bin/Data/data.unity3dUnity 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:

  1. Load pointer from GOT
  2. Create byte[] of specified size
  3. Call RuntimeHelpers.InitializeArray() to copy static data

Static Arrays in PrivateImplementationDetails

In dump.cs (TypeDefIndex 8095-8100) found class <PrivateImplementationDetails> with fixed-size fields:

FieldSizeOffset in metadataPurpose
__StaticArrayInitTypeSize=3232 bytes0x3EBAB0AES-256 Key
__StaticArrayInitTypeSize=1616 bytes0x3EBA48AES IV
__StaticArrayInitTypeSize=4848 bytes0x3EBA78Encrypted 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-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR