Cracking a keygen with Z3 SMT Solver

By Jesus Dominguez aka n0tM4l4f4m4 @chucho_domz

Reto de la categoria de 'reverse engineering' del NACTF 2019

Challenge

Can you figure out what the key to this program is? keygen

File recognition

Empezaremos con el comando file. (Fig1)

algo
Fig1




Como podemos observar es un archivo ejecutable de 32-bit.

Lo siguiente es revisar los 'strings' del binario. (Fig2)

Fig2

Nos arroja cadenas interesantes entre ellas el inicio de la 'flag' nactf{ y otros 'strings' que seguramente veremos a la hora de ejecutarlo, también nos da una pista de un posible parámetro con el que se debe ejecutar.

Procedemos a ejecutar el binario con el parámetro thisIsTheKey de ahora en adelante lo llamaremos nuestro input. (Fig3)

Fig3
El ejecutarlo nos manda el mensaje 'Invalid key', que previamente vimos en los 'strings'.

Como siguiente paso usaremos ltrace para ver la llamadas que el programa hace. (Fig4)

Fig4
Vemos que hace un strlen a nuestro input, quizás lo utilice para algún calculo. Posteriormente nos arroja el mensaje 'Invalid key' con la función puts.


Analisys

Para decompilar el binario usaremos IDA 7

Al decompilar el binario nos vamos directo a la función 'main', confirmaremos rápidamente lo que ya sabemos, el binario requiere un argumento.(Fig5)

Fig5
Observamos que en  dword ptr [eax] esta almacenada la cantidad de parámetros que pasamos al binario y en caso de ser mayor a 1, el flujo del programa se irá hacia la derecha (línea verde), caso contrario se irá a la izquierda (línea roja), imprimiendo el mensaje de como ejecutar el binario y terminando su ejecución.

Después de esta confirmación, vemos que en caso de meter los parámetros correctos, hace la llamada a una función que renombraremos como calc_of_key_sub_804928C pasandole como parámetro la dirección de memoria de nuestro input, al retorno de la función hace test al, al  y en caso de que el valor de al sea cero, nos manda el mensaje 'Invalid key'. Por lo tanto seguiremos con el análisis de dicha función.(Fig6)

Fig6
En las primeras lineas de la función notamos que hace la llamada a strlen y que el valor retornado debe ser igual a 0Fh (15 decimal) , para poder continuar con la validación. (Fig7)

Fig7
En la siguiente parte observamos que hace la validación de los primeros 6 caracteres de nuestro input y si estos no son los que están en la dirección del offset needle, termina la ejecución, con esto deducimos que los primeros caracteres del 'key' son nactf{, es decir el inicio de la 'flag', por lo tanto el 'key' es la 'flag'. (Fig8)

Fig8
En esta validación observamos que compara el final de nuestro input con 7Dh, que en ASCII es el caracter '}', esto nos indica el final del 'key' y por lo tanto de la 'flag', en caso de no ser ese valor, termina la ejecución. (Fig9)

Fig9
En la siguiente parte observamos que a la dirección inicial de nuestro input le suma 6 y lo pasa a la función que renombrados como calcula_resto_flag_sub_80491B6. Con esto deducimos que pasa lo que esta entre las llaves del input eg. nactf{thisIsTheKey}. ya que al sumarle 6 a la dirección inicial de nuestro input queda en el caracter 't'. (Fig10)

Fig10
Al retorno de esa función hace un XOR con los valores en azul y los registros EAX y EDX , después  les hace un OR para comprobar que todo sea cero. Por lo tanto pasaremos al análisis de la función calcula_resto_flag_sub_80491B6. (Fig11)
Fig11
Ya en la función podemos observa que es un ciclo for que itera 'ciertas veces' más 8 y al cumplir este ciclo nos devuelve dos valores que llamaremos acumulador_var_8 y acumulador_var_8+4, posteriormente veremos de que tratan. (Fig12)

Fig12
Las primeras instrucciones del ciclo vemos que son varias y guardan los valores en las variables acumulador_var_8 acumulador_var_8+4, a pesar de que son varias lineas y con ayuda del 'debugging at runtime' vemos que lo que hace es simplemente multiplicar por 3Eh (62 decimal) a la misma variable y acumular el resultado. El pseudocodigo es el siguiente:

acumulador_var_8 = acumulador_var_8  * 62

La razón de dos variables acumulador_var_8 acumulador_var_8+4, es porque el resultado de esto nos dará un número más grande de 32bits lo que supera el tamaño de nuestro registro, es por eso que usa el registro EDX para guardar ese excedente, lo que nos deja con dos variables en ensamblador. (Fig13)

Fig13

Al continuar con la ejecución vemos que empieza a comparar los caracteres que le fueron pasados a la función uno por uno, recuerden que los caracteres fueron thisIsTheKey y con base en esto hace ciertos cálculos, veremos cada caso a continuación.

Primer comparación

Vemos que para comparar el carácter pasa al registro EAX el contenido de la dirección de memoria byte ptr [eax] y este tiene que ser  mayor que 40h (64 decimal) y menor o igual a 5Ah (90 decimal), si el carácter entra dentro de ese rango entonces le resta 41h (65 decimal) a EAX y el resultado lo pasa a acumulador_var_8 con esta información armaremos un pseudocodigo. (Fig14)

if  byte ptr [eax] > 64 and byte ptr [eax] <= 90
then
EAX = EAX - 65
acumulador_var_8 = acumulador_var_8 + EAX

Fig14

Segunda comparación

Esta comparación tiene la misma lógica, con diferentes valores.

Para comparar el caracter pasa al registro EAX el contenido de la dirección de memoria byte ptr [eax] y este tiene que ser  mayor que 60h (96 decimal) y menor o igual a 7Ah (122 decimal), si el caracter entra dentro de ese rango entonces le resta 47h (71 decimal) a EAX y el resultado lo pasa a acumulador_var_8 con esta información armaremos un pseudocodigo.(Fig15)

if  byte ptr [eax] > 96 and byte ptr [eax] <= 122
then
EAX = EAX - 71
acumulador_var_8 = acumulador_var_8 + EAX

Fig15

Tercer comparación

Seguimos con la misma lógica pero diferentes valores.

Para comparar el caracter pasa al registro EAX el contenido de la dirección de memoria byte ptr [eax] y este tiene que ser  mayor que 2Fh (47 decimal) y menor o igual a 39h (57 decimal), si el caracter entra dentro de ese rango entonces le resta 4h (4 decimal) a EAX y el resultado lo pasa a acumulador_var_8 con esta información armaremos un pseudocodigo. (Fig16)

if  byte ptr [eax] > 47 and byte ptr [eax] <= 57
then
EAX = EAX + 4
acumulador_var_8 = acumulador_var_8 + EAX

FIg16
Al terminar las comparaciones el ciclo se repite hasta terminar cada caracter que le fue pasado a la función.

Con el análisis deducimos que los valores que aceptan son letras minúsculas, letras mayúsculas y números, esto por los rangos de comparación de las condiciones anteriores, consultar tabla ASCII.

Al terminar el ciclo de comparaciones y los operaciones que conlleva cada condición, retorna el valor de acumulador_var_8 en los registros EAX y EDX, esto porque el valor almacenado ahí pasa los 32bits que nos permite almacenar cada registro. (Fig17)

Fig17
Esto nos retorna a la función calc_of_key_sub_804928C y hace los cálculos que ya mencionamos, para limpiar los registros EAX y EDX con XOR (recordemos que cuando hacemos XOR a un mismo valor el resultado es siempre 0) y despues hacer un OR para probar que todo este en 0. Con este pedazo de código comprendemos que las operaciones de la funcion calcula_resto_flag_sub_80491B6 nos debe de dar 0FCAACF98h en EAX 1371h en EDX, que todo junto el resultado final es 1371FCAACF98h. (Fig18)

Fig18


Entonces los caracteres que faltan de la 'flag' nos debe de dar 1371FCAACF98h, esto después de todas las operaciones que hacen en la función calcula_resto_flag_sub_80491B6 , para poder implementar esto, vamos a tener que un análisis más.

Las comparaciones nos quedaron así:

if  x > 64 x <= 90
x - 65

65 - 65 = 0
90 - 65 = 25

if  x > 96 x <= 122
x - 71

97 - 71 = 26
122 - 71 = 51

if  x> 47 x<= 57
x + 4

48 + 4 = 52
57 + 4 = 61

Con esta lógica obtuvimos el rango de números que vamos a calcular del 0 al 65 y posteriormente haremos el inverso para poder obtener los caracteres de la 'flag'.

Solution

Dada la experiencia en otros retos de este estilo, usaremos Z3 para poder calcular los valores que necesitamos para pasar las condiciones del binario, aquí el código:

#!/usr/bin/python
# -*- coding: utf-8 -*-
## Author:  Jesus Dominguez aka n0tM4l4f4m4

from z3 import *

serial = [BitVec('val_%i'%i,64) for i in range(0,8)];

res = '0x1371FCAACF98'

s = Solver();

s.add(serial[0] >= 0 , serial[0] <= 61)
s.add(serial[1] >= 0 , serial[1] <= 61)
s.add(serial[2] >= 0 , serial[2] <= 61)
s.add(serial[3] >= 0 , serial[3] <= 61)
s.add(serial[4] >= 0 , serial[4] <= 61)
s.add(serial[5] >= 0 , serial[5] <= 61)
s.add(serial[6] >= 0 , serial[6] <= 61)
s.add(serial[7] >= 0 , serial[7] <= 61)

#encierra todo al final
s.add((((((((((((((serial[0] * 62) + serial[1]) * 62) + serial[2]) * 62) + serial[3]) * 62) + serial[4]) * 62) + serial[5]) * 62) + serial[6]) * 62) + serial[7] == int(res, 16))

if (s.check() == sat):
    m = s.model()
    # print m
    flag = ''
    for value in range(8):
        num =  m[serial[value]].as_long()
        if num >= 0 and num <= 25:
            flag += chr(num + 65)
        elif num >= 26 and num <= 51:
            flag += chr(num + 71)
        elif num >= 52 and num <= 61:
            flag += chr(num - 4)
            pass
        pass
print "nactf{%s}" % (flag)


Probamos el 'output' del 'script' nactf{GEZhxWsw}. (Fig19)
Fig19

Flag

nactf{GEZhxWsw}

Go Mayas!

References





Comentarios