Challenge Arno
Este reto consiste en obtener la flag de un juego de Android en Unity mediante análisis estático y/o dinámico y la solución consiste en configurar un entorno local con herramientas de ingeniería inversa para extraer los binarios de IL2CPP del APK y descifrar la flag localmente.
La inspección inicial del APK requiere la extracción de su contenido para acceder a los archivos principales de Unity. Esto se logra descomprimiendo la aplicación para copiar los archivos global-metadata.dat y libil2cpp.so en un directorio de trabajo limpio para su posterior análisis.
$ 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
Una vez aislados los archivos centrales, se utiliza Il2CppDumper para fusionar libil2cpp.so y global-metadata.dat. Este proceso genera una DLL ficticia de C# (Assembly-CSharp.dll), la cual es necesaria para reconstruir la lógica de la aplicación.
$ 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...
El archivo Assembly-CSharp.dll resultante se carga entonces en el descompilador ILSpy para examinar la estructura interna de la aplicación. Dentro de la clase FlagControl, se descubre una función DecryptFlag, junto con otros métodos relevantes como GetKey(), GetIV() y GetFlag(). Estas funciones sugieren fuertemente los mecanismos criptográficos utilizados para ocultar la 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;
}
La descompilación adicional del binario utilizando Cutter revela las asignaciones de memoria exactas para los componentes criptográficos. Específicamente, se reservan 32 bytes para la llave, 16 bytes para el vector de inicialización (IV) y 48 bytes para la flag encriptada.
uVar3 = fcn.015fc014(*puVar1, 0x20);
uVar3 = fcn.015fc014(*puVar1, 0x10);
uVar3 = fcn.015fc014(*puVar1, 0x30);
Inicialmente, se intenta un enfoque de análisis dinámico. Dado que no hay dispositivos con acceso root disponibles, se emplea objection patchapk para crear una versión modificada del APK y así poder instrumentarlo.
$ 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...
El APK modificado se instala en el dispositivo a través de ADB.
$ adb install Arno.objection.apk
Performing Streamed Install
Success
Sin embargo, los juegos de Unity compilados con IL2CPP a menudo implementan controles de integridad y presentan estructuras de memoria delicadas que se corrompen fácilmente al ser reempaquetadas. La aplicación sufre un cierre inesperado internamente pocos segundos después de iniciarse, lo que hace imposible la instrumentación dinámica.
Debido a que el análisis dinámico resultó inviable, el enfoque se centra nuevamente en el análisis estático. Al inspeccionar el archivo dump.cs generado previamente por Il2CppDumper, se encuentra la clase <PrivateImplementationDetails>, la cual es generada por el compilador. Esta clase almacena los arreglos estáticos predeterminados utilizados por InitializeArray, que corresponden directamente al material criptográfico.
$ 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
}
...
Estos offsets de los metadatos proporcionan las ubicaciones exactas en el archivo global-metadata.dat para las variables necesarias: el IV de 16 bytes se encuentra en el offset 0x3EBA48, la llave de 32 bytes está en el offset 0x3EBAB0 y la flag encriptada de 48 bytes se halla en el offset 0x3EBA78. Con esta información, los bytes hexadecimales en bruto se extraen directamente del archivo de metadatos empleando 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
La clase AesManaged de Unity utiliza AES-CBC con relleno PKCS7 por defecto. Utilizando el vector de inicialización, la llave AES y el texto cifrado obtenidos de la memoria, se elabora un script de Python para ejecutar el descifrado AES fuera de línea. La ejecución de este código recupera exitosamente la flag en texto plano, dando por finalizado el reto.
$ 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}