Arno (HackTheBox) — Android Unity IL2CPP
Classic Unity IL2CPP flow: unpack the APK, dump libil2cpp.so +
global-metadata.dat, find key / IV / ciphertext, then decrypt in Python.
0. Context
Challenge:
Arno (HackTheBox). The only file you get is an Android app: Arno.apk.
Opening the APK makes the challenge type immediately clear: both libil2cpp.so and global-metadata.dat are present, so this is Unity IL2CPP — not plain Mono C#.
The basic idea:
- Confirm it’s Unity IL2CPP (not plain Mono C#).
- Use Il2CppDumper to get fake C# + mappings.
- Track where key, IV and encrypted flag are stored.
- Recreate the AES decrypt in Python and print the flag.
1. Decompiling the APK
First step: unpack the APK with apktool.
apktool d Arno.apk -o app_dec
Then I checked the usual Unity IL2CPP locations:
app_dec/lib/arm64-v8a/libil2cpp.so
app_dec/assets/bin/Data/Managed/Metadata/global-metadata.dat
If you see both libil2cpp.so and global-metadata.dat, you’re in IL2CPP territory —
there’s no direct C# decompile path, so I had to go through the native side plus metadata.
2. Running Il2CppDumper
With those two files found, the next step is Il2CppDumper. I had it in my $PATH, so I just ran:
il2cppdumper \
app_dec/lib/arm64-v8a/libil2cpp.so \
app_dec/assets/bin/Data/Managed/Metadata/global-metadata.dat \
Arno_dump
It produced:
dump.csscript.jsonil2cpp.hDummyDll/Assembly-CSharp.dlland friends
For this challenge, the important ones are:
dump.cs– all types / methods / fields in one huge C# file.DummyDll/Assembly-CSharp.dll– for quick browsing withilspycmd.script.json+il2cpp.h– for IDA so that functions get proper names.
3. Quick C# view with ilspycmd
Before loading ARM64 in IDA, I used ilspycmd to get a quick view of the managed code.
I dumped the dummy DLL with ilspycmd:
cd Arno_dump/DummyDll
ilspycmd -p Assembly-CSharp.dll -o src
Then I searched for anything flag-related:
grep -R "FlagControl" -n src
sed -n '1,200p' src/FlagControl.cs
FlagControl is exactly what you expect: Unity stuff for the UI, plus the interesting methods:
GetKey(), GetIV(), GetFlag(), DecryptFlag().
The methods are tagged with attributes like
[Address(RVA = "0x16D1838", VA = "0x16D1838")], which makes it easy to line them up with the
native side in IDA later.
4. IDA Pro + Il2CppDumper script
Next, I opened libil2cpp.so in IDA (ARM64). To get sane function names, I ran the Il2CppDumper
script that comes with the tool:
- Open
libil2cpp.soin IDA (64-bit ARM). File → Script file…→ selectida_with_struct.py(or similar).- When prompted:
script.json→Arno_dump/script.jsonil2cpp.h→Arno_dump/il2cpp.h
After that finishes, IDA suddenly looks much friendlier: functions like
FlagControl__GetKey, FlagControl__GetIV,
FlagControl__GetFlag, FlagControl__DecryptFlag show up in the names list.
5. Finding key / IV / flag in metadata
The app doesn’t hardcode the key/IV/flag as obvious byte arrays in the native code. Instead, they’re stored
in global-metadata.dat under the usual <PrivateImplementationDetails>
umbrella.
5.1. <PrivateImplementationDetails> in dump.cs
At the end of dump.cs I searched for
PrivateImplementationDetails:
grep -n "PrivateImplementationDetails" dump.cs
There are a bunch of lines like:
private struct <PrivateImplementationDetails>.__StaticArrayInitTypeSize=16 { ... }
private struct <PrivateImplementationDetails>.__StaticArrayInitTypeSize=32 { ... }
...
internal static readonly <PrivateImplementationDetails>.__StaticArrayInitTypeSize=38 2694... /*Metadata offset 0x3EBA20*/; // 0x0
internal static readonly <PrivateImplementationDetails>.__StaticArrayInitTypeSize=16 36CB... /*Metadata offset 0x3EBA48*/; // 0x26
internal static readonly <PrivateImplementationDetails>.__StaticArrayInitTypeSize=17 4356... /*Metadata offset 0x3EBA60*/; // 0x36
internal static readonly <PrivateImplementationDetails>.__StaticArrayInitTypeSize=48 6F30... /*Metadata offset 0x3EBA78*/; // 0x47
internal static readonly <PrivateImplementationDetails>.__StaticArrayInitTypeSize=32 703A... /*Metadata offset 0x3EBAB0*/; // 0x77
Each line gives you:
- the size of the array (16, 32, 48, ...),
- a hash-like name,
- and a metadata offset inside
global-metadata.dat.
From IDA, you can see which of these fields each of GetKey, GetIV and
GetFlag uses.
For Arno, it ended up like this for me:
- Key: 32 bytes → offset
0x3EBAB0 - IV: 16 bytes → offset
0x3EBA48 - Encrypted flag: 48 bytes → offset
0x3EBA78
5.2. Extracting the bytes with xxd
With the offsets known, I went into the metadata directory and used xxd to slice the bytes
straight out of global-metadata.dat:
cd app_dec/assets/bin/Data/Managed/Metadata
# KEY (32 bytes)
xxd -s 0x3EBAB0 -l 0x20 global-metadata.dat
# IV (16 bytes)
xxd -s 0x3EBA48 -l 0x10 global-metadata.dat
# FLAG ciphertext (48 bytes)
xxd -s 0x3EBA78 -l 0x30 global-metadata.dat
After cleaning the output into plain hex strings (no spaces, no offsets), I had:
key_hex = "cfdc33ccbee6dc775ba146b95d0fea6cbcc3ee3e5e76531d2cd79c140758f08d"
iv_hex = "bbf5a8d7066fd51b43d959c044365cdf"
ct_hex = "13ebf3953a9b8c13c6e5471f7eeaa0174b6c1fac41802002da16eb32fa88f63c570185a8bc218d9ef3ac03e218d30c55"
6. Understanding DecryptFlag in IDA
The last missing piece is how the app actually decrypts the flag. That’s all in
FlagControl__DecryptFlag.
Following the calls in IDA, you can spot the usual suspects:
Aes.Create()(or the IL2CPP equivalent)- Key and IV being set from
GetKey()/GetIV() MemoryStream+CryptoStreamin decrypt mode- Finally,
Encoding.UTF8.GetString(...)on the decrypted bytes
AES-CBC with PKCS7 padding, output is a UTF-8 string (the flag).
7. Python script to decrypt the flag
pycryptodome. The hex values are the ones
extracted with xxd earlier.
from Crypto.Cipher import AES
from binascii import unhexlify
key_hex = "cfdc33ccbee6dc775ba146b95d0fea6cbcc3ee3e5e76531d2cd79c140758f08d"
iv_hex = "bbf5a8d7066fd51b43d959c044365cdf"
ct_hex = "13ebf3953a9b8c13c6e5471f7eeaa0174b6c1fac41802002da16eb32fa88f63c570185a8bc218d9ef3ac03e218d30c55"
key = unhexlify(key_hex)
iv = unhexlify(iv_hex)
ct = unhexlify(ct_hex)
cipher = AES.new(key, AES.MODE_CBC, iv)
pt = cipher.decrypt(ct)
# strip PKCS7 padding
pad_len = pt[-1]
flag = pt[:-pad_len].decode("utf-8")
print(flag)
Running this prints the flag in the expected HTB{...} format.
Wrap-up
Arno follows the standard Unity IL2CPP Android challenge pattern:
- Find
libil2cpp.so+global-metadata.datinside the APK. - Use Il2CppDumper to get native mappings and a reconstructed C# view.
- Use IDA with
script.jsonto recover function names. - Map
<PrivateImplementationDetails>fields to offsets in the metadata file. - Rebuild the AES-CBC decryption in a script and recover the flag.