Use the tree to jump between collections without leaving the reader.

archive Select writeup Open tree
HackTheBox/Challenges/Challenge Arno.en.md READ_ONLY

Challenge Arno

This challenge consists of obtaining the flag from an Android Unity game through static and/or dynamic analysis and the solution consists of setting up a local environment with reverse engineering tools to extract the IL2CPP binaries from the APK and decrypt the flag locally.

The initial inspection of the APK requires extracting its contents to access the core Unity files. This is accomplished by decompressing the application to copy the global-metadata.dat and libil2cpp.so files into a clean workspace for further analysis.

$ unzip Arno.apk
Archive:  Arno.apk
  inflating: META-INF/com/android/build/gradle/app-metadata.properties
  inflating: META-INF/version-control-info.textproto
  extracting: assets/dexopt/baseline.prof
  ...
  extracting: res/u3.png
  inflating: res/uJ.xml

With the core files isolated, Il2CppDumper is utilized to merge libil2cpp.so and global-metadata.dat. This process generates a dummy C# DLL (Assembly-CSharp.dll), which is necessary to reconstruct the application's logic.

$ il2cpp-dumper libil2cpp.so global-metadata.dat ./dumper
Initializing metadata...
Metadata Version: 31
Initializing il2cpp file...
Applying relocations...
WARNING: find JNI_OnLoad
ERROR: This file may be protected.
Il2Cpp Version: 31
Searching...
CodeRegistration : 3022e58
MetadataRegistration : 3182b70
Dumping...
Done!
Generate struct...
Done!
Generate dummy dll...
Done!
Press any key to exit...

The resulting Assembly-CSharp.dll file is then loaded into the ILSpy decompiler to examine the application's internal structure. Inside the FlagControl class, a DecryptFlag function is discovered, alongside other relevant methods such as GetKey(), GetIV(), and GetFlag(). These functions strongly suggest the cryptographic mechanisms used to hide the flag.

using Il2CppDummyDll;

[Token(Token = "0x6000007")]
[Address(RVA = "0x16D1988", Offset = "0x16D0988", VA = "0x16D1988")]
public string DecryptFlag(byte[] key, byte[] iv, byte[] encryptedData)
{
  return null;
}
using Il2CppDummyDll;

[Token(Token = "0x6000006")]
[Address(RVA = "0x16D1918", Offset = "0x16D0918", VA = "0x16D1918")]
public byte[] GetFlag()
{
  return null;
}

// FlagControl
using Il2CppDummyDll;

[Token(Token = "0x6000005")]
[Address(RVA = "0x16D18A8", Offset = "0x16D08A8", VA = "0x16D18A8")]
public byte[] GetIV()
{
  return null;
}

// FlagControl
using Il2CppDummyDll;

[Token(Token = "0x6000004")]
[Address(RVA = "0x16D1838", Offset = "0x16D0838", VA = "0x16D1838")]
public byte[] GetKey()
{
  return null;
}

Further decompilation of the binary using Cutter reveals the exact memory allocations for the cryptographic components. Specifically, 32 bytes are reserved for the key, 16 bytes for the initialization vector (IV), and 48 bytes for the encrypted flag.

uVar3 = fcn.015fc014(*puVar1, 0x20);
uVar3 = fcn.015fc014(*puVar1, 0x10);
uVar3 = fcn.015fc014(*puVar1, 0x30);

Initially, a dynamic analysis approach is attempted. Since no rooted devices are available, objection patchapk is used to create a "trojanized" version of the APK for instrumentation.

$ objection patchapk -s Arno.apk -a arm64-v8a
Unpacking Arno.apk
PlayerGameActivity.smali
Signed the new APK
...
Copying final apk from /tmp/tmppaf7rty5.apktemp.aligned.objection.apk to Arno.objection.apk in current directory...
Cleaning up temp files...

The modified APK is installed on the device via ADB.

$ adb install Arno.objection.apk
Performing Streamed Install
Success

However, Unity games compiled with IL2CPP often implement integrity checks and feature delicate memory structures that corrupt easily when repackaged. The application consistently crashes internally shortly after launch, rendering dynamic hooking impossible.

Because the dynamic approach proved unviable, the focus returns to static analysis. Inspecting the dump.cs file generated earlier by Il2CppDumper reveals the compiler-generated <PrivateImplementationDetails> class. This class stores the static default arrays used by InitializeArray, which correspond directly to the cryptographic material.

$ cat dumper/dump.cs
...
internal sealed class <PrivateImplementationDetails> // TypeDefIndex: 8100
{
  internal static readonly <PrivateImplementationDetails>.__StaticArrayInitT\
ypeSize=16 36CB71326BD2E601D33D1ECCA3CD621FA05ED31A26608CF775331557E461F8EB\
  /*Metadata offset 0x3EBA48*/; // 0x26
  internal static readonly <PrivateImplementationDetails>.__StaticArrayInitT\
ypeSize=48 6F30F150623A66022280A433383C0BC30B7A0B571923A2762FD18DC078AE8A28\
  /*Metadata offset 0x3EBA78*/; // 0x47
  internal static readonly <PrivateImplementationDetails>.__StaticArrayInitT\
ypeSize=32 703A5CD6F009A97282036DDF638EFAE313527A0F5709E6C83CC971FEF17B59EB\
/*Metadata offset 0x3EBAB0*/; // 0x77
}
...

These metadata offsets provide the exact locations in the global-metadata.dat file for the necessary variables: the 16-byte IV is at offset 0x3EBA48, the 32-byte Key is at offset 0x3EBAB0, and the 48-byte Encrypted Flag is at offset 0x3EBA78. With this information, the raw hexadecimal bytes are extracted directly from the metadata file using xxd.

$ xxd -s $((0x3EBA48)) -l 16 global-metadata.dat
003eba48: bbf5 a8d7 066f d51b 43d9 59c0 4436 5cdf  .....o..C.Y.D6\.
$ xxd -s $((0x3EBAB0)) -l 32 global-metadata.dat
003ebab0: cfdc 33cc bee6 dc77 5ba1 46b9 5d0f ea6c  ..3....w[.F.]..l
003ebac0: bcc3 ee3e 5e76 531d 2cd7 9c14 0758 f08d  ...>^vS.,....X..
$ xxd -s $((0x3EBA78)) -l 48 global-metadata.dat
003eba78: 13eb f395 3a9b 8c13 c6e5 471f 7eea a017  ....:.....G.~...
003eba88: 4b6c 1fac 4180 2002 da16 eb32 fa88 f63c  Kl..A. ....2...<
003eba98: 5701 85a8 bc21 8d9e f3ac 03e2 18d3 0c55  W....!.........U

Unity's AesManaged class utilizes AES-CBC with PKCS7 padding by default. Using the extracted initialization vector, AES key, and ciphertext, a short Python script is implemented to perform the AES decryption offline. Executing this script successfully recovers the plaintext flag, solving the challenge.

$ cat exploit.py
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

iv = bytes.fromhex("bbf5a8d7066fd51b43d959c044365cdf")
key = bytes.fromhex(
    "cfdc33ccbee6dc775ba146b95d0fea6cbcc3ee3e5e76531d2cd79c140758f08d"
)
ciphertext = bytes.fromhex(
    "13ebf3953a9b8c13c6e5471f7eeaa0174b6c1fac41802002da16eb32fa88"
    "f63c570185a8bc218d9ef3ac03e218d30c55"
)

cipher = AES.new(key, AES.MODE_CBC, iv)
try:
    decrypted = unpad(cipher.decrypt(ciphertext), AES.block_size)
    print("Flag:", decrypted.decode('utf-8'))
except Exception as e:
    print("Decryption failed:", e)
$ python3 exploit.py
Flag: HTB{FLAG}