Reversing 101. Análisis Dinámico.

Imagen de G. Martí

Continúo con esta segunda parte de la resolución de un “crackme” orientada a iniciados en ingeniería inversa. Si no has leído la primera parte, te dejo el enlace a continuación y recomiendo que lo hagas antes de continuar con esta.

El debugger

En esta segunda parte, como bien se indica en el subtítulo, se resuelve el crackme usando análisis dinámico.

Lo que significa que, se ejecutará el programa dentro de un entorno que permite ejecutarlo paso a paso o parar la ejecución para poder observar los valores de los registros del procesador o los valores de ciertas posiciones de memoria.

El programa para poder llevar a cabo esto es el debugger, o traducido al castellano, un depurador.

A diferencia del análisis estático, en este caso hay que depurar el programa dentro del mismo sistema operativo para el que está desarrollado, es decir, Linux.

Recordemos que el mismo comando file de Linux ya nos informaba que era un ejecutable ELF (“Executable and Linkable Format”).

Información del comando file que indica el tipo de archivo

El depurador que se va a usar en este análisis es edb que viene preinstalado en Kali Linux.

Pantalla de muestra de edb debugger cargado

Pasando argumentos al crackme

El primer paso es cargar el crackme accediendo al menu File → Open (o pulsando F3), y cuando aparezca el cuadro de diálogo, y tras seleccionar el fichero, en el campo “Program arguments” escribimos nuestro argumento.

Esto es porque el programa requería que se le pasara un flag como argumento. Como en este momento se supone que no lo sabemos (aunque ya lo descubrimos en el artículo anterior), añadiremos cualquier texto como un supuesto flag, para poder comprobar en qué parte del programa se lleva a cabo la instrucción que nos lo va a revelar.

Yo usaré la palabra “hola”.

Carga del crackme y establecimiento del argumento pasado al programa

Primer paso

Tras cargar programa este se hallará parado en la primera instrucción que debe de ejecutar. Al mismo tiempo se abrirá una pequeña ventana de terminal donde se muestra la salida que pueda mostrar el programa o las peticiones de entrada de teclado si fuera el caso.

Programa cargado y detenido junto a ventana de terminal que muestra la salida

En ese momento situamos el puntero de ratón sobre dicha instrucción y le damos a botón derecho de ratón con lo que nos aparecerá un menú emergente. De ese menú elegimos opción “Analyze Here”. También podemos usar la combinación de teclas CTRL + A.

Selección de análisis de código desde el menú contextual

Ejecución paso a paso

Es probable que el programa se haya parado en el punto de entrada del programa, pero que no sea la función principal, main(), con lo que podríamos darle a la tecla F9 (menú Debug → Run) y ejecutará el programa hasta detenerse en el main. En caso de que no fuera así, el programa habrá finalizado pues no solicita nada por teclado, y deberíamos de volver a cargarlo.

Desde el momento que estamos en el main podemos ir ejecutando las instrucciones en ensamblador paso a paso. Para ello disponemos de las teclas F7 y F8. Con F7 se ejecutan una instrucción y si se encuentra una subrutina (por ejemplo, una llamada <call> a un printf) también entraría en la subrutina y la ejecutaría paso a paso (cada paso una pulsación de tecla F7).

Para evitar esto podemos utilizar la tecla F8, que ejecuta directamente la subrutina sin detenerse en cada instrucción de esta y se para justo en la siguiente instrucción después del call. Esto es importante saberlo, porque en algunos casos, si son subrutinas del propio programa, nos puede interesar hacerlas paso a paso, y en otros casos no nos conviene hacerlo.

El main

Al llegar al main, si antes ya no estábamos en él, es probable que necesitemos volver a analizar esta parte de código, por lo que repetimos la combinación CTRL + A. Esto se verá evidenciado por la barra de color negro que aparece en la parte superior, donde quedará marcado en color verde las zonas de código y un pequeño triángulo amarillo que indica donde nos encontramos en ese momento.

Inicio de la función main()

Si vamos ejecutando el programa paso a paso con F8 podemos ver como se cargan valores de memoria a los registros o viceversa y cuando esta dirección apunte a una cadena de texto se nos mostrará en el contenido apuntado por el registro. Por ejemplo, la cadena “hola” que hemos pasado como argumento al cargar el programa. Lo que hace en ese momento es guardar en la pila el valor que hemos pasado como argumento para ser utilizado más tarde.

Texto pasado como parámetro apuntado por el registro RAX

Ahora te podría decir cómo llegar hasta el punto crítico. Pero ten un poco de paciencia, y ejecuta el código paso a paso y observa cómo cambian los valores en los registros y en la pila (stack).

Valores en volcado de memoria y la pila

Así que seguimos ejecutando el código, paso a paso, dándole a la tecla F8, hasta que llegamos a una llamada de función que hace que se muestre el primer mensaje en pantalla.

Llamada a función (o subrutina) que muestra un texto por pantalla

Dicho mensaje queda evidenciado por el texto que ha cargado previamente que se ve en la parte derecha. En este caso mostrado por la dirección de memoria donde apunta el registro RDI.

Comprobación del argumento

Ahora ya estamos situados en una parte del programa que conocemos.

Si nos aparece el texto de cómo usar el crackme es que no hemos puesto el flag como argumento al cargar el programa. En caso de pasar un argumento llegaremos al punto donde se carga la cadena del saludo.

Carga en registro la dirección de memoria con el mensaje de saludo

Tras comprobar que se ha recibido un argumento, se muestra el mensaje de saludo en la ventana de terminal junto con el texto que le hayamos pasado como argumento.

Salida por terminal del mensaje del saludo y argumento pasado

A medida que vamos ejecutando código podremos comprobar cómo va cargando otras cadenas de texto, como un falso flag para despistar.

Falsa flag en el código para despistar

Este código anterior lo tenemos que obviar. No nos interesa. Seguimos dándole al F8. También podríamos marcar un punto de parada (breakpoint) en una parte más avanzada del código y saltarlo todo de golpe. Pero como no hay muchas instrucciones, de momento seguimos con F8.

Descifrado XOR

Y aquí es cuando llegamos a la parte importante, que es el bucle, donde se recorren los datos de memoria y se descifra el flag.

Muestra de bucle donde se descifra la flag mediante operaciones XOR

A medida que vamos ejecutando paso a paso, iremos viendo como en el stack va apareciendo la cadena que se va descifrando. Evidentemente, ya la conocemos porque habíamos hecho anteriormente el análisis estático, pero no importa. También la podríamos conocer sin ver esto y avanzando un poco más.

Muestra de la parte de código de descifra la flag y parte de la pila donde se va mostrando

Ir dando al F7 / F8 todo el rato puede ser pesado y tedioso. Imagina que, en lugar de 22 caracteres, es una cadena de 500, o de 1000. Todo este proceso se nos haría eterno.

Lo ideal es comprobar donde se hace la comparación que controla el fin del bucle y a qué dirección de memoria salta al finalizar. Una vez identificada dicha dirección, podemos poner en esta (o en la siguiente instrucción) un “punto de ruptura” (breakpoint), es decir, una parada. Esto se hace pulsando la tecla F2, con lo que nos aparecerá un punto rojo en el lado izquierdo de la dirección de memoria.

Código completo de descifrado XOR y establecimiento de punto de parada al final

Ahora solo nos queda pulsar la tecla F9, que corresponde al comando RUN, y lo que hará es ejecutar todas las instrucciones seguidas, no paso a paso, hasta que se encuentre dicho breakpoint.

Descubriendo la flag

Ahora seguimos ejecutando unas cuantas instrucciones paso a paso (F8) y llegaremos a un punto donde se hace una llamada a una función strcmp(). Si sabes lenguaje C, sabrás que sirve para comparar dos cadenas de texto.

Justo antes de la llamada a la función se ve como se cargan los valores de los registros RDX y RAX que contienen las direcciones de memoria de las cadenas a comparar. Ahí tenemos la cadena que hemos pasado como argumento, “hola”, y la cadena con la que tiene que comparar, el flag, y que es “BED{D3buggers_ar3_fun}” y con esto llegamos al mismo resultado que en el análisis estático.

A continuación, dejo el enlace de la tercera parte de este artículo.

https://medium.com/%40gabimarti/ingenier%C3%ADa-inversa-de-un-crackme-parte-3-4daa1e6765f9

¡¡¡ Happy Reversing !!!

--

--

Gabriel Martí

Ex-Docente CFGM, CFGS Ciberseguridad. Actualmente Consultor en Ciberseguridad. Intereses en robótica, ciberseguridad, reversing. Twitter @gmarti @310hkc41b