Latest Tweets

Desclavando espinas 2/3: UAD360 go4fun writeup

El reto

Revisamos la información básica sobre el binario:

file go4fun.uu
go4fun.uu: ELF 32-bit MSB executable, MIPS, MIPS32 version 1 (SYSV), statically linked, not stripped

Estamos ante un binario ELF (Linux), con arquitectura MIPS de 32 bits. Además, es Big Endian (MSB).

Ejecutando el binario con arm_now

Durante la UAD360, no conocía el proyecto arm_now. @julianjm me lo chivó (luego también lo haría @oreos_ES). Basta con instalar arm_now y luego levantar una instancia emulada con qemu de una máquina MIPS32 Big Endian:

arm_now start mips32 –sync

El binario nos pide una clave, luego parece estar haciendo algún tipo de comprobación y finalmente acaba sin ningún mensaje de error:

Ejecutando el binario MIPS32

Reverseando

Ejecutamos r2 y lo primero que nos sorprende es la cantidad de funciones que tiene este binario. Se ve claramente que estamos ante un binario escrito en golang, compilado para MIPS32. Leemos un poco sobre binarios Go para saber dónde comenzar nuestra búsqueda. La primera función que realmente nos interesa es sym.main.main, que a su vez llama a sym.main.check_key. Pedimos a r2 que nos muestre el callgraph desde sym.main.check_key:

El callgraph generado nos muestra claramente el flujo del programa con una llamada a Sleep (seguramente el “Please Wait….”) y una llamada a Decrypt. Luego, el binario desencripta probablemente la flag, que estará encriptada y almacenada en el propio binario, a partir de la key que se nos pide.

La function callgraph para check_key

En la función sym.main.main encontramos la flag cifrada:

La flag cifrada.

Recordemos que esto es Go, y las cadenas no terminan con \x00. Si observamos claramente la imagen, en realidad la flag cifrada está compuesta de los siguientes bytes:

7fd4000ff0ae3bdd14ff3bd70aa12ce69636366c77c1a7298ae9f5aeab69a0f4739f4f069561c44b2d6597bfc4834236f1756290ae

Si miramos el código de la función Decrypt, vemos claras llamadas a las funciones en Go crypto_aes.NewCipher y crypto_cipher.NewGCM:

[0x0010050c]> s sym.main.decrypt
[0x000ff758]> pdf
Do you want to print 747 lines? (y/N) 
[0x000ff758]> pdf~jal
| |: 0x000ff770 0c01b2cc jal sym.runtime.morestack_noctxt
| 0x000ff798 0c03fd8a jal sym.main.createHash
| 0x000ff7b8 0c016a6b jal sym.runtime.stringtoslicebyte ; sym.runtime.rawstringtmp+0xc4
| 0x000ff7d8 0c0222e1 jal sym.crypto_aes.NewCipher
| | 0x000ff800 0c01d9fa jal sym.crypto_cipher.NewGCM

Así que ya sabemos lo que hace el código a groso modo. A partir de nuestra entrada, hace algunas comprobaciones sobre la misma. Si la key introducida es válida, entonces genera un hash en MD5 de la clave (llamada a createHash) y la utiliza como clave AES en modo GCM (llamadas a sym.crypto_aes.NewCipher y sym.crypto_cipher.NewGCM) . Con esto descifra la flag y entonces se hace una llamada a show_flag que la imprime por pantalla.

Leyendo un poco sobre Go y AES en modo GCM, vemos que se suele concatenar el nonce delante del texto cifrado. Dentro del binario tenemos claramente la flag cifrada, pero el nonce no lo vemos por ninguna parte. Al final, con ayuda de r2, vemos claramente que hay una llamada a gcm.NonceSize() justo aquí:

0x000ff828 8c230010 lw v1, 0x10(at)
| || 0x000ff82c afa20004 sw v0, (var_4h)
| || 0x000ff830 0060f809 jalr v1; gcm.NonceSize()

Así que la flag cifrada tiene seguramente el nonce concatenado al principio. La siguiente llamada es la que ejecuta el descifrado:

0x000ff8c0 0080f809 jalr a0

La documentación sobre AES en modo GCM indica que el número de bytes para el nonce es de 12 (0xc en hexadecimal). Así pues, consideramos los 12 primeros bytes de la flag como el nonce, y el resto como la flag cifrada:

nonce =7fd4000ff0ae3bdd14ff3bd7

flag  = 0aa12ce69636366c77c1a7298ae9f5aeab69a0f4739f4f069561c44b2d6597bfc4834236f1756290ae

La única manera de descifrar la flag es, pues, con la key válida de AES. Por si acaso, revisando el código del binario en MIPS, nos encontramos con que la función createHash, que entendemos calcula el MD5 de la clave AES (aparentemente, la que introducimos como entrada), tiene un código que llama la atención.

La falsa clave AES

Si observamos el código con r2 de la función createHash, vemos un snippet de código que acaba generando 32 bytes hexadecimales (que podría perfectamente ser una clave AES válida):

0x67452301efcdab8998badcfe10325476

Las operaciones lui cargan inmediatos en el registro v0, y las operaciones ORI hacen el or con el mismo registro y otro inmediato, almacenando el resultado en el mismo registro v0. Tras cada operación, se guardan 4 bytes en at + offset. Tras el código mostrado en la siguiente captura, $at contendrá los 32 bytes del falso hash:

Este código genera un hash falso de 32 bytes.

Como ya sabemos que el código descifra la flag, cifrada con AES en modo GCM; tenemos la flag y el nonce, sabemos que la clave será un hash md5. Con todo esto, probamos el hash generado con las operaciones OR con un código Go típico para descifrar. El resultado no puede ser más desolador:

Nope, @oreos_ES nos ha jodido bien …

Por desgracia, no hay más remedio que enfrentarse a la lógica de la función check_key … A base de mucho café, claro.

Sobre check_key, retdec y otros quebrantos

Mis conocimientos de MIPS32 son bastante nulos, aunque han mejorado tras este reto :). Usando r2, el código de comprobación de la clave se me hizo bastante infumable, así que tiré de retdec para intentar obtener una representación a más alto nivel de la función. Tras instalar retdec, de-compilé la función check_key:

retdec-decompiler.py -a mips -e big –select-functions main.check_key go4fun.uu

El archivo de código C resultante mejoró considerablemente, aunque seguía siendo algo duro de roer. Más bien tedioso en su lectura e interpretación, aunque a simple vista ya se podía ver que se estaba usando una variable como base y diferentes constantes numéricas como OFFSET para realizar ciertas comprobaciones sobre lo que parecían ser las diferentes posiciones de nuestra clave. Por ejemplo:

Primera comprobación de las posiciones key[15] y key[4]

En el código anterior, v2 contendrá el valor entero de la posición 15 de la clave, o sea, key[15]. v6, en cambio, contendrá el valor entero de la posición 4 de la clave, es decir, key[4]. La primera comprobación es, si substituimos v6 por key[15] y v2 por key[4], la siguiente:

key[15] + key[4] != 12

Evidentemente, si esto se cumple se retorna y no se comprueban el resto de posiciones de la clave, con lo que no es válida y no se procede a descifrar la flag. Por lo tanto, nuestra clave debe satisfacer todo lo contrario:

key[15] + key[4] == 12

Si seguimos mirando el resto del código, podemos ver que esta realizando más comprobaciones con diferentes posiciones de la clave. Esto cada vez más parece un sistema de ecuaciones dónde diferentes operaciones sobre diferentes posiciones de la clave deben dar un valor concreto para seguir con la enésima comprobación. Si cualquiera de estas operaciones no da el resultado correspondiente, se retorna y no se llama a decrypt. Tras armarme de paciencia, llegué a identificar hasta 24 ecuaciones con diferentes posiciones de la clave. El archivo en código C generado con retdec y analizado y documentado lo podéis ver aquí.

Todas las ecuaciones se podían identificar con relativa facilidad gracias a retdec, aunque en algunos casos era algo más obtuso. También es cierto que he simplificado algunas, porque por ejemplo esto:

((v12 > 9 ? -1 : v12) – (v13 > 9 ? -1 : v13) ^ 8) == 0

es totalmente equivalente a esto:

((v12 > 9 ? -1 : v12) – (v13 > 9 ? -1 : v13) ) == 8

Entra z3 (gracias @julianjm)

Estas son todas las ecuaciones (24) que identifiqué leyendo el código de-compilado de la función check_key (os recomiendo mirar el archivo check_key.c):

int(key[4]) + int(key[15]) == 12
int(key[1]) * int(key[18]) == 15
int(key[15]) / int(key[9]) ^ 1 == 0 => int(key[15]) / int(key[9]) == 1
int(key[17]) – int(key[0]) ^ 8 == 0 => int(key[17]) – int(key[0]) == 8
int(key[5]) – int(key[17]) == -1
int(key[15]) – int(key[1]) == 6
int(key[10]) * int(key[1]) == 24
int(key[13]) + int(key[8]) == 11
int(key[8]) * int(key[18]) == 15
int(key[11]) * int(key[4]) == 18
int(key[8]) + int(key[9]) == 12
int(key[12]) – int(key[19]) == -4
int(key[9]) % int(key[17]) == 1
int(key[16]) * int(key[14]) == 2
int(key[7]) – int(key[4]) == 1
int(key[6]) + int(key[0]) == 0
int(key[2]) – int(key[16]) == -1
int(key[4]) – int(key[6]) == -3
int(key[0]) % int(key[5] == 0
int(key[11]) * int(key[5]) == 42
int(key[10]) % int(key[15]) ^ 8 == 0 => int(key[10]) % int(key[15]) == 8
int(key[11]) / int(key[3]) == 2
int(key[14]) – int(key[13]) == -7
int(key[19]) + int(key[18] == 12

Necesitaba algo que resolviera todas estas ecuaciones. Entonces fue cuando @julianjm me dijo que existía el z3 solver. Así que lo instalé, y escribí el solver a partir de las ecuaciones anteriores. El archivo del solver en z3 lo podéis descargar aquí. Para resolver estas ecuaciones, las introduje una a una y ejecuté el solver:

[s[8] = 3,
s[4] = 3,
s[19] = 7,
s[17] = 8,
s[16] = 2,
s[2] = 1,
s[9] = 9,
s[1] = 3,
s[3] = 3,
s[15] = 9,
s[11] = 6,
s[10] = 8,
s[12] = 3,
s[18] = 5,
s[0] = 0,
s[14] = 1,
s[7] = 4,
s[6] = 0,
s[5] = 7,
s[13] = 8]

El resultado me daba el valor numérico de cada posición de la clave. Por lo tanto, re-ordenando las soluciones se obtenía la clave:

03133704398638192857

Tras mi análisis de lo que hacía el código, era evidente que había que calcular el hash MD5 de la clave y usarla como clave de 32 byres AES de descifrado en mi programa escrito en go “decrypt.go”. El resultado:

The flag is mine!

Reto superado, eso sí, cerca de 72 horas más tarde de lo esperado …