Hoy traigo la resolución de un challenge Fácil de HackTheBox, un reto bastante disfrutón aunque tengo sensaciones encontradas ya que la resolución se acerca más a la categoría de reversing que a la de mobile. Aún así buen reto e interesante para aprender acerca de apps compiladas con Unity.
Overview
La APK es un juego Unity compilado con IL2CPP dicho componente permite una traducción a un lenguaje intermedio para permitir compilación en ciertos dispositivos (reemplaza a la antigua mono). La aplicación no es especialmente útil, al pulsar un botón, muestra frases aleatorias.

Extraer ficheros del APK
Comenzamos extrayendo los ficheros de la app y echando un vistazo:
apktool d Arno.apk
En este punto me encontraba bastante perdido ya que con un primer vistazo al código no encontré gran cosa. Sin embargo, al ver que teníamos un libil2cpp.so y que estaba compilado con Unity comprendí que la parte importante estaba en dichas librerías.
Ejecutar Cpp2IL
Para la extracción y análisis del código de la librería utilicé Cpp2Il una herramienta alternativa a Il2CppDumper que me dió problemas en debian.
./Cpp2IL-2022.1.0-pre-release.20-Linux --game-path Arno.apk --output-as dll_il_recovery
Esto genró un fichero FlagControl.cs con el siguiente contenido:
public class FlagControl : MonoBehaviour
{
public GameObject textField; //Field offset: 0x20
public List<String> quotes; //Field offset: 0x28
public FlagControl() { }
public string DecryptFlag(Byte[] key, Byte[] iv, Byte[] encryptedData) { }
public Byte[] GetFlag() { }
public Byte[] GetIV() { }
public Byte[] GetKey() { }
public void PopulateQuotes() { }
public void ShowQuote() { }
private void Start() { }
}
Como vemos la flag está siendo encriptada probablemente con AES. Además tenemos un archivo _PrivateImplementationDetails_.cs, que contiene:
[CompilerGenerated]
internal sealed class <PrivateImplementationDetails>
{
private struct __StaticArrayInitTypeSize=16 {}
private struct __StaticArrayInitTypeSize=17 {}
private struct __StaticArrayInitTypeSize=32 {}
private struct __StaticArrayInitTypeSize=38 {}
private struct __StaticArrayInitTypeSize=48 {}
internal static readonly __StaticArrayInitTypeSize=38 2694407F2358DB433C52827F5FB5F1567F7EB2FA89053E8D6B162179C6DAAE7B; //Field offset: 0x0 || Has Field RVA (address hidden for diffability) || Field RVA Decoded (hex blob): [00 00 00 01 00 00 00 1E 5C 41 73 73 65 74 73 5C 53 63 72 69 70 74 73 5C 46 6C 61 67 43 6F 6E 74 72 6F 6C 2E 63 73]
internal static readonly __StaticArrayInitTypeSize=16 36CB71326BD2E601D33D1ECCA3CD621FA05ED31A26608CF775331557E461F8EB; //Field offset: 0x26 || Has Field RVA (address hidden for diffability) || Field RVA Decoded (hex blob): [BB F5 A8 D7 06 6F D5 1B 43 D9 59 C0 44 36 5C DF]
internal static readonly __StaticArrayInitTypeSize=17 43563FB6E1278B5116042C66FC24F6F5641E3D931363433DB0CE8626281DA4BB; //Field offset: 0x36 || Has Field RVA (address hidden for diffability) || Field RVA Decoded (hex blob): [00 00 00 00 0C 7C 46 6C 61 67 43 6F 6E 74 72 6F 6C]
internal static readonly __StaticArrayInitTypeSize=48 6F30F150623A66022280A433383C0BC30B7A0B571923A2762FD18DC078AE8A28; //Field offset: 0x47 || Has Field RVA (address hidden for diffability) || Field RVA Decoded (hex blob): [13 EB F3 95 3A 9B 8C 13 C6 E5 47 1F 7E EA A0 17 4B 6C 1F AC 41 80 20 02 DA 16 EB 32 FA 88 F6 3C 57 01 85 A8 BC 21 8D 9E F3 AC 03 E2 18 D3 0C 55]
internal static readonly __StaticArrayInitTypeSize=32 703A5CD6F009A97282036DDF638EFAE313527A0F5709E6C83CC971FEF17B59EB; //Field offset: 0x77 || Has Field RVA (address hidden for diffability) || Field RVA Decoded (hex blob): [CF DC 33 CC BE E6 DC 77 5B A1 46 B9 5D 0F EA 6C BC C3 EE 3E 5E 76 53 1D 2C D7 9C 14 07 58 F0 8D]
}
Con esto tenemos los tamaños de cada array:
GetKey() → 32 bytes
GetIV() → 16 bytes
GetFlag() → 48 bytes
Estos tamaños encajan con:
AES-256 → key de 32 bytes
CBC → IV de 16 bytes
ciphertext múltiplo de 16 → 48 bytes (3 bloques AES)
Cuando .NET compila arrays constantes, a menudo los mete en <PrivateImplementationDetails> y luego los rellena con RuntimeHelpers.InitializeArray.
Con el siguiente comando podemos extraerlos fácilmente:
sed -n '1,220p' cpp2il_out/DiffableCs/Assembly-CSharp/_PrivateImplementationDetails_.cs
Ahí aparecen blobs hex ya decodificados. Los tres que nos interesan son:
IV (16 bytes):
BB F5 A8 D7 06 6F D5 1B 43 D9 59 C0 44 36 5C DF
Ciphertext (48 bytes):
13 EB F3 95 3A 9B 8C 13 C6 E5 47 1F 7E EA A0 17
4B 6C 1F AC 41 80 20 02 DA 16 EB 32 FA 88 F6 3C
57 01 85 A8 BC 21 8D 9E F3 AC 03 E2 18 D3 0C 55
Key (32 bytes):
CF DC 33 CC BE E6 DC 77 5B A1 46 B9 5D 0F EA 6C
BC C3 EE 3E 5E 76 53 1D 2C D7 9C 14 07 58 F0 8D
Descifrar (AES-256-CBC + PKCS7)
Con python podemos descifrar fácilmente:
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
key = bytes.fromhex("CFDC33CCBEE6DC775BA146B95D0FEA6CBCC3EE3E5E76531D2CD79C140758F08D")
iv = bytes.fromhex("BBF5A8D7066FD51B43D959C044365CDF")
ct = bytes.fromhex(
"13EBF3953A9B8C13C6E5471F7EEAA017"
"4B6C1FAC41802002DA16EB32FA88F63C"
"570185A8BC218D9EF3AC03E218D30C55"
)
cipher = AES.new(key, AES.MODE_CBC, iv)
pt = unpad(cipher.decrypt(ct), 16)
print(pt.decode())
Y finalmente obtendríamos la flag:
HTB{c@n_90u_533_w17h_90ur_5h@rp_e9e5}
Al final un reto más de reversing que de explotación como tal, pero que nos da una pista grande para próximas ocasiones, ya que en CTFs con Unity e IL2CPP, podemos probar a mirar en librerías con el método que hemos seguido aquí.
¡Nos vemos en el próximo challenge!
