Echoes (Reversing) – H4ck3d CTF [ES]

Este binario es un desafío de la categoría Reversing del CTF de la conferencia H4ck3d. Lo pueden descargar acá. Para resolverlo tuve que desempaquetarlo a mano y después usé un enfoque similar al que se usa para resolver crackmes.

Arranqué ejecutandolo y pude ver que el programa responde de forma distinta según el largo del input. Acá hay algunos ejemplos para diferentes largos donde se puede ver que cambia la imagen que muestra. En algunos casos imprime un mensajes como Error 0x432872.

test input test input largo test input mas largo

Luego, intenté desensamblarlo usando IDA pero el código que obtuve no parecía coincidir con la funcionalidad observada. Esto me llevó a suponer que podía estar ofuscado o empaquetado. Para corroborarlo, usé Detect It Easy y pude ver las secciones TTX0 y TTX1 que suelen indicar que el binario fue empaquetado usando UPX. Además, el escanéo heurístico también sugería que se usó UPX. Otra alternativa es usar PEiD.

secciones upx

Este packer es bastante usado porque es libre y se puede desempaquetar usando el mismo programa. Lo intenté pero no pude, probablemente porque se modificó el binario para evitar esto o porque se usó otra versión o variante de UPX distinta a la que probé.

Entonces, decidí tratar de desempaquetarlo a mano usando x32dbg. Para eso primero lo dejé ejecutar hasta el código de usuario:

ejecutar hasta codigo usuario

Acá se puede ver que UPX comienza pusheando los registros al stack para preservarlos usando la instrucción pushad y luego comienza a ejecutar las rutinas para desempaquetar el código. Entonces decidí probar la estrategía de poner un breakpoint de acceso por hardware en el último registro pusheado para frenar la ejecución cuando el programa recupere los valores de los registros usando popad.

pushad

Para eso, primero ví en el volcado de memoria (Hexdump) la dirección de ESP.

esp en dump

Seleccioné los 4 bytes apuntados por ESP y puse el breakpoint tocando el botón derecho del mouse para elegir “Punto de interrupción”, “Hardware, Acceso”, “Dword”.

breakpoint esp

Luego, continué la ejecución hasta que la misma se detuvo por el breakpoint justo después de ejecutar la instrucción popad como se muestra a continuación. Además, se puede ver que luego de recuperar los registros el programa termina haciendo un salto que se conoce como tail jump para ir al punto de entrada original (OEP por sus siglas en ingles) del programa, que ahora se encuentra desempaquetado en memoria.

popad

Entonces, continué la ejecución hasta el OEP y luego use Scylla para hacer un dump del programa desempaquetado en memoria. Para esto, hay que ir al menú de “plugins”, elegir Scylla y luego presionar dump como se muestra en la siguiente imagen.

scylla

Este dump no va a funcionar porque tiene la tabla de imports (IAT) rota. Para arreglarla hay que tocar “IAT Autosearch” para encontrar la tabla, “Get Imports” para listar las entradas y “Fix Dump” para arreglar la tabla en el dump.

Ahora que tenemos el dump del binario desempaquetado y andando podemos desensamblarlo. Yo usé IDA y arranqué buscando el string de error que vi cuando probaba entradas de distinto largo.

busqueda string error

Luego, usé la herramienta de referencias cruzadas (tecla x) para ver en que parte del código se estaba usando el mensaje de error.

referencia al string

Con esto llegué a una función que IDA no definió correctamente. Entonces subí un poco hasta encontrar el prólogo de la función y la definí manualmente (Botón derecho, “Create function”).

definir funcion

Pasé a la vista gráfica tocando la tecla espacio y esto me permitió ver que la función ejecuta un bloque básico y luego toma una serie de desciones que determinan cual de los siguientes bloques, que IDA grafica uno al lado de otro a la misma altura, se va a ejecutar. Uno de ellos es el que referencia al string de error que observé antes.

decisiones

Haciendo un poco de zoom pude ver que las decisiones se basan sólamente en el valor del registro ESI.

dependen de esi

Esto, sumado a que el programa responde distinto según el largo del texto ingresado, me llevó a suponer que en ESI se guarda el largo del mismo y que el bloque a ejecutar depende de ese largo.

Entonces, busqué un bloque con otro mensaje (Segmentation fault) y miré que condiciones debían darse para que obtener ese mensaje (bloques amarillos). La primer comparación es que el largo sea mayor o igual a 56 (cmp esi, 38h y jge), la segunda es que sea menor o igual a 72 (cmp esi, 48h y jg) y la tercera es que sea igual a 72 (jz, usa el cmp anterior).

otro mensaje

Armé un string de largo 72 y, al ingresarlo en el programa, observé que llega a ejecutar ese bloque básico que imprime Segmentation fault y además le agrega algo que parece la flag pero no es. Con esto pude confirmar que ESI contiene el largo del texto ingresado y la decisión de que bloque ejecutar depende de esto.

consigo otro mensaje

Entonces, busqué otro bloque que no tuviera uno de estos mensajes de error y miré las condiciones para llegar a su ejecución (bloques verdes). En este caso el largo del texto ingresado debe ser mayor a 72 y, al restarle 73 (add esi, 0xFFFFFFB7), restarle 6 (sub esi, 6) y restarle 2 (sub esi, 2), debe dar cero (jz).

target

La suma de 0xFFFFFFB7 es en realidad una resta porque los enteros usan la representación de complemento a 2. En la siguiente imagen se muestra la conversión. Como resultado de todas las condiciones, el largo del texto debe ser 81. Entonces, ingresé un texto de largo 81 y pude obtener la flag.

consigo target