Buenas a todos. En la pasada entrada vimos conceptos generales como la segmentación de memoria, los registros, algunas instrucciones y análisis estático. En esta entrada continuaremos con el mismo binario usando el Debugger que nos proporciona radare2.
Antes de nada lo abrimos con la opción -d, hacemos un breakpoint con db en la función main y con dc continuamos el proceso de ejecución hasta el breakpoint establecido. Usamos Vpp para verlo en modo gráfico.
Inicialización de la variable secret
Avanzamos con la tecla s hasta que el EIP sea la dirección de memoria 0x0804841b, es decir, apunta a la siguiente instrucción a ejecutar. La instrucción actual corresponde a la dirección de memoria 0x08048414 y vemos como mueve el contenido en hexadecimal a la memoria situada en la dirección almacenada en la variable local_4h o [ebp-4]. En ebp-4, es dónde esta almacenada la variable en la pila. Por tanto, almacena el valor de la variable en dicha localización en memoria.
En la parte de arriba vemos el Stack o Pila diferenciando desde la parte más de la izquierda las direcciones de memoria y sus offset, en el centro el valor correspondiente a cada dirección de memoria dividido por bytes hexadecimales(0-F), y a la derecha su correspondiente valor en ascii.
Abajo podemos ver los registros, y el valor que tiene en cada momento. En la imagen podemos darnos cuenta del EIP. Esta apuntando a la siguiente instrucción apreciando que el valor correspondiente a ebp-4 almacena el hexadecimal 0xc7d04, empezando el primer byte «04» en la dirección 0xfffe4f84 de la pila. Si os fijáis los Bytes están invertidos en la parte central(valores o contenido) del stack con respecto a cuando hicimos la instrucción mov. Los Bytes están invertidos tipo Little-Endian.
Si tenemos un dominio de lo que ocurre en memoria cuando se ejecuta un programa, podremos saber que es lo que ocurre en cada instrucción en el flujo de ejecución de cualquier binario, entendiendo por supuesto el lenguaje ensamblador.
Continuamos y ejecutamos la siguiente instrucción. Apreciamos con el Debugger, como ESP apunta a la dirección de memoria 0xfffe4f70, por tanto cuando se ejecute la instrucción mov moverá la dirección de memoria 0x8048544 a ESP.
Estado de las eflags
Hacemos un breakpoint en la función main para saber la dirección de memoria correspondiente al salto condicional jne, y visualizamos con Vpp.
Hacemos lo mismo, pero ahora el breakpoint lo ponemos en esa dirección de memoria 0x0804844d.
Al ejecutar el programa con el Debugger y poniendo el breakpoint después de que se ejecute la llamada call a la función scanf(), nos pide que introduzcamos por la salida estándar del stdin (teclado), el número. En este caso nosotros lo conocemos y ponemos el correcto para ver el estado de la ZF(Zero Flag).
Observamos como las eflags para la PF,ZF,IF esta el bit igual a 1. También apreciamos con el Debugger, el EIP apuntando a la instrucción donde colocamos el breakpoint(b). Si ahora en el modo visual Vpp donde nos encontramos hacemos «s» el EIP irá saltando a la siguiente instrucción a ejecutar. Como introdujimos el número correcto saltará a la dirección de memoria 0x0804844f.
Imprime por pantalla la salida estándar la función printf(), y seguidamente con la instrucción JMP salta a la dirección de memoria indicada. Si introducimos un numero erróneo, la ZF=0, ya que la comparación no es igual y con el salto condicional jne solo salta si son distintos o no es cero, o salta si su ZF=0.
Vemos como la ZF su bit esta a cero, ya que no aparece donde eflags.
Y el correspondiente salto a que el número introducido es erróneo. Para finalizar mueve el valor «0» al registro eax y restaura ebp con la instrucción LEAVE. Esta instrucción es equivalente a:
mov esp, ebp
pop ebp
Hemos visto un ejemplo muy sencillo entrando en materia en cuanto al uso básico de radare2 y entender algunas instrucciones del lenguaje ensamblador, iremos subiendo el nivel poco a poco. Veremos ahora un binario que vamos a intentar conocer o interpretar su código fuente realizando un análisis del mismo.
Reversing del binario
Antes de nada, compilamos con gcc en un sistema linux con una arquitectura de 32 bits.
gcc flag.c -o flag
Empezamos realizando un análisis de las funciones, y localizamos main.
Entramos en la función con s sym.funcion y con pdf realizamos el desensamblado de la función.
[0x0804856b]> s sym.funcion [0x0804858f]> pdf / (fcn) sym.funcion 323 | sym.funcion (); | ; var int local_18h @ ebp-0x18 | ; var int local_14h @ ebp-0x14 | ; var int local_10h @ ebp-0x10 | ; var int local_ch @ ebp-0xc | ; CALL XREF from 0x0804857c (sym.main) | 0x0804858f 55 push ebp | 0x08048590 89e5 mov ebp, esp | 0x08048592 83ec18 sub esp, 0x18 | 0x08048595 65a114000000 mov eax, dword gs:[0x14] ; [0x14:4]=1 | 0x0804859b 8945f4 mov dword [local_ch], eax | 0x0804859e 31c0 xor eax, eax | 0x080485a0 c745ec000000. mov dword [local_14h], 0 | 0x080485a7 8345ec01 add dword [local_14h], 1 | 0x080485ab 8345ec02 add dword [local_14h], 2 | 0x080485af b831000000 mov eax, 0x31 ; '1' | 0x080485b4 2b45ec sub eax, dword [local_14h] | 0x080485b7 8945ec mov dword [local_14h], eax | 0x080485ba 836dec01 sub dword [local_14h], 1 | 0x080485be c745f0000000. mov dword [local_10h], 0 | 0x080485c5 8345f001 add dword [local_10h], 1 | 0x080485c9 b832000000 mov eax, 0x32 ; '2' | 0x080485ce 2b45f0 sub eax, dword [local_10h] | 0x080485d1 8945f0 mov dword [local_10h], eax | 0x080485d4 83ec0c sub esp, 0xc | 0x080485d7 6860870408 push str.Introduce_un_numero_para_ver_la_Flag: ; 0x8048760 ; "Introduce un numero para ver la Flag: " ; const char * format | 0x080485dc e80ffeffff call sym.imp.printf ; int printf(const char *format) | 0x080485e1 83c410 add esp, 0x10 | 0x080485e4 83ec08 sub esp, 8 | 0x080485e7 8d45e8 lea eax, [local_18h] | 0x080485ea 50 push eax | 0x080485eb 6887870408 push 0x8048787 ; "%d" ; const char * format | 0x080485f0 e85bfeffff call sym.imp.__isoc99_scanf ; int scanf(const char *format) | 0x080485f5 83c410 add esp, 0x10 | 0x080485f8 8b45e8 mov eax, dword [local_18h] | 0x080485fb 3b45ec cmp eax, dword [local_14h] | ,=< 0x080485fe 0f8e84000000 jle 0x8048688 | | 0x08048604 8b45e8 mov eax, dword [local_18h] | | 0x08048607 3b45f0 cmp eax, dword [local_10h] | ,==< 0x0804860a 7f7c jg 0x8048688 | || 0x0804860c 8b45e8 mov eax, dword [local_18h] | || 0x0804860f 83f82f cmp eax, 0x2f ; '/' ; '/' | ,===< 0x08048612 742a je 0x804863e | ||| 0x08048614 83f82f cmp eax, 0x2f ; '/' ; '/' | ,====< 0x08048617 7f07 jg 0x8048620 | |||| 0x08048619 83f82e cmp eax, 0x2e ; '.' ; '.' | ,=====< 0x0804861c 740e je 0x804862c | ,======< 0x0804861e eb54 jmp 0x8048674 | ||`----> 0x08048620 83f830 cmp eax, 0x30 ; '0' ; '0' | ||,====< 0x08048623 742b je 0x8048650 | |||||| 0x08048625 83f831 cmp eax, 0x31 ; '1' ; '1' | ,=======< 0x08048628 7438 je 0x8048662 | ========< 0x0804862a eb48 jmp 0x8048674 | ||`-----> 0x0804862c 83ec0c sub esp, 0xc | || |||| 0x0804862f 688a870408 push str.WW4gc3ludCByZjog ; 0x804878a ; "WW4gc3ludCByZjog" ; const char * format | || |||| 0x08048634 e8b7fdffff call sym.imp.printf ; int printf(const char *format) | || |||| 0x08048639 83c410 add esp, 0x10 | ||,=====< 0x0804863c eb48 jmp 0x8048686 | ||||`---> 0x0804863e 83ec0c sub esp, 0xc | |||| || 0x08048641 689b870408 push str.c2p1dm9vdm ; 0x804879b ; "c2p1dm9vdm" ; const char * format | |||| || 0x08048646 e8a5fdffff call sym.imp.printf ; int printf(const char *format) | |||| || 0x0804864b 83c410 add esp, 0x10 | ||||,===< 0x0804864e eb36 jmp 0x8048686 | |||`----> 0x08048650 83ec0c sub esp, 0xc | ||| ||| 0x08048653 68a6870408 push str.d7MXkxeDNlM2 ; 0x80487a6 ; "d7MXkxeDNlM2" ; const char * format | ||| ||| 0x08048658 e893fdffff call sym.imp.printf ; int printf(const char *format) | ||| ||| 0x0804865d 83c410 add esp, 0x10 | |||,====< 0x08048660 eb24 jmp 0x8048686 | `-------> 0x08048662 83ec0c sub esp, 0xc | |||||| 0x08048665 68b3870408 push str.kzZWYxYXR9 ; edi ; 0x80487b3 ; "kzZWYxYXR9" ; const char * format | |||||| 0x0804866a e881fdffff call sym.imp.printf ; int printf(const char *format) | |||||| 0x0804866f 83c410 add esp, 0x10 | ,=======< 0x08048672 eb12 jmp 0x8048686 | ||||||| ; JMP XREF from 0x0804862a (sym.funcion) | ||||||| ; JMP XREF from 0x0804861e (sym.funcion) | -`------> 0x08048674 83ec0c sub esp, 0xc | | ||||| 0x08048677 68be870408 push str.Hola_amigos__:_ ; 0x80487be ; "Hola amigos! :)" ; const char * format | | ||||| 0x0804867c e86ffdffff call sym.imp.printf ; int printf(const char *format) | | ||||| 0x08048681 83c410 add esp, 0x10 | |,======< 0x08048684 eb12 jmp 0x8048698 | ||||||| ; JMP XREF from 0x0804864e (sym.funcion) | ||||||| ; JMP XREF from 0x08048660 (sym.funcion) | ||||||| ; JMP XREF from 0x08048672 (sym.funcion) | ||||||| ; JMP XREF from 0x0804863c (sym.funcion) | `-```---> 0x08048686 eb10 jmp 0x8048698 | | ``-> 0x08048688 83ec0c sub esp, 0xc | | 0x0804868b 68ce870408 push str.El_numero_no_es_correcto ; 0x80487ce ; "El numero no es correcto" ; const char * s | | 0x08048690 e89bfdffff call sym.imp.puts ; int puts(const char *s) | | 0x08048695 83c410 add esp, 0x10 | | ; JMP XREF from 0x08048686 (sym.funcion) | | ; JMP XREF from 0x08048684 (sym.funcion) | -`------> 0x08048698 a140a00408 mov eax, dword obj.stdin ; [0x804a040:4]=0x312d302e ; ".0-12ubuntu2) 6.3.0 20170406" | 0x0804869d 83ec0c sub esp, 0xc | 0x080486a0 50 push eax ; FILE *stream | 0x080486a1 e85afdffff call sym.imp.fflush ; int fflush(FILE *stream) | 0x080486a6 83c410 add esp, 0x10 | 0x080486a9 83ec0c sub esp, 0xc | 0x080486ac 68e7870408 push str._nEnter_para_finalizar. ; 0x80487e7 ; "\nEnter para finalizar." ; const char * format | 0x080486b1 e83afdffff call sym.imp.printf ; int printf(const char *format) | 0x080486b6 83c410 add esp, 0x10 | 0x080486b9 e852fdffff call sym.imp.getchar ; int getchar(void) | 0x080486be 90 nop | 0x080486bf 8b45f4 mov eax, dword [local_ch] | 0x080486c2 653305140000. xor eax, dword gs:[0x14] | ,=< 0x080486c9 7405 je 0x80486d0 | | 0x080486cb e850fdffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void) | `-> 0x080486d0 c9 leave \ 0x080486d1 c3 ret [0x0804858f]>
Lo primero que nos encontramos dentro de la función es el prólogo.
- Coloca el puntero base en la pila o el Saved EBP.
- Mueve el contenido del puntero de pila al puntero base con el objetivo de colocar a este último en el Top del stack.
- Y finalmente resta el valor en hexadecimal 0x18 al puntero base, con el fin dejar espacio en la pila disponible para las variables locales. Lo podeis ver con el Debugger.
Una vez tenemos el marco de pila configurado antes de pasarle las variables locales, el » Stack Canaries» es implementado por el compilador. Cuando usamos GCC para compilar el código fuente hace que los «canaries» se utilicen en cualquier función potencialmente vulnerable. El prólogo de la función carga un valor al marco de la pila entre la dirección de retorno y las variables locales. Este valor «canary» es un número aleatorio de 4 Bytes(dword) elegido cuando el programa comienza. El epílogo asegura que el valor está intacto mediante una operación XOR y un salto condicional. Si no es así, probablemente ocurrió un desbordamiento de búfer (o error) y el programa se aborta mediante la llamada a la función __stack_chk_fail. El valor «canary» lo obtiene del offset(desplazamiento) 0x14 del registro gs para una arquitectura de 32 bits y lo mueve al registro EAX. Por lo tanto, el «Stack Canaries» funcionan modificando las regiones de prólogo y epílogo de cada función para colocar y comprobar un valor en la pila, respectivamente.
La instrucción situada en la dirección de memoria 0x0804859b almacenará el valor de EAX a la dirección de memoria de la variable local_ch o ebp-0xc en la pila, es decir, almacena ese valor en la variable. Y antes de empezar con reversear nuestro código realiza una operación XOR para poner el valor del registro EAX a 0.
La siguiente imagen vemos como se inicializa la variable local_14h de tipo integer de 4 Bytes(Dword), y realiza una serie de operaciones. La primera de ellas ADD suma el valor decimal 1 al contenido de la variable y la siguiente le suma el valor 2, por tanto la variable queda así local_14h = 3. Ahora con mov mueve el valor en hexadecimal 0x31 correspondiendo en decimal 49 al registro EAX. Restamos con sub el contenido de la variable a EAX quedando almacenado el valor en el registro. EAX tendría este valor en decimal, 46. Movemos con mov el contenido del registro a la variable y por último restamos con sub el valor decimal 1 a la variable quedando local_14h = 45.
Seguimos avanzando y vemos como se inicializa la variable local_10h de tipo integer 4 Bytes. Añade a 1 al contenido de la variable, mueve el valor en hexadecimal 0x32 correspondiendo en decimal 50 al registro EAX. Resta el contenido de la variable con el registro quedando almacenado en EAX y por último mueve el contenido del registro a la variable.
Con el Debugger vamos a ver como resta al Top del Stack Frame, es decir, a ESP el valor en hexadecimal 0xc. El valor del puntero de pila cuando EIP apunta a la instrucción sub es 0xff9bd780. Si continuamos la ejecución del programa con s tenemos que ESP vale 0xff9bd774. Esta operación se realiza debido a que el EIP ahora apunta a la instrucción push colocando en la pila donde esta actualmente apuntando el puntero ESP, el argumento que va a recibir la función printf(). Ese argumento corresponde a una dirección de memoria donde esta situado el String en el segmento de datos. Recordamos que lo primero que va a recibir el stack o pila de cualquier función serían los argumentos antes de ser llamada con call y seguidamente se colocaría el marco de base EBP una vez dentro de la función (Saved EBP).
Vemos como en los siguientes offset del ESP esta la dirección de memoria almacenada en la pila que recibirá la función printf() como argumento antes de ser llamada.
La función printf() ya la hemos visto al igual que seguidamente scanf(), por tanto obviaré esa parte. Solo remarcaré que la variable local_18h corresponde al almacenamiento del valor introducido por teclado, ya que como veremos ahora comparará ese valor introducido con la variable local_14h para hacer una cosa u otra con el correspondiente salto condicional dependiendo del estado de la eflag.
CMP resta el operando fuente al operando destino pero sin que éste almacene el resultado de la operación, solo afecta el estado de las eflags. Con JLE salta si es menor o igual o salta si no es más grande. Por tanto si el valor introducido es menor que la variable local_14h el resultado de la operación será negativa ya que se tiene en cuenta el signo, y si fuese igual sería cero alterando el estado de la ZF=1 o SF!=OF y se realiza el salto(bifurcación verde). Ejemplo 13-45=-32. Podemos ver en radare2 la descripción de los saltos condicionales.
Y si colocamos un breakpoint justo en la instrucción cmp, podemos ver el estado de las eflags. Y podemos comprobar que cumple una de las condiciones de la descripción de jle ya que SF es distinto a OF.
No es necesario estar viendo el estado de las eflags en cada salto condicional con saber si es menor o igual considerando el signo y el resultado de la comparación, nos sirve para interpretar si salta[(bifurcación verde) y (menor o igual)] o no[(bifurcación roja) y (mayor)]. En el caso de que sea mayor, no se cumple el estado de las eflags y no salta. Vemos el EIP en la imagen inferior apuntando a la siguiente instrucción a ejecutar. Si no salta implica que hemos acertado la primera parte de la estructura de control, es decir, del if. Podemos deducir con el gráfico que el if consta de dos condiciones teniendo que cumplir ambas, es decir, una operación AND. Por tanto, si el valor introducido no satisface ambas condiciones, saltará(true) aunque aun solo hemos visto la primera, vamos a por la segunda.
En el caso de la segunda comparará el valor de la variable local_10h(49 en decimal) con lo introducido por teclado. El salto condicional JG salta si es más grande o salta si no es menor o igual. Por lo tanto podemos deducir que estas dos condiciones están acotando un rango de números, ya que, si introducimos 50 este valor es más grande de 49 y procede a saltar o ir por la bifurcación verde(True), mientras que si introducimos 47 es menor que 49, no salta y va por la bifurcación roja(False) y también este valor es valido para la condición anterior que vimos. Entonces se ve ahora más claro la operación AND que realiza la estructura de control if.
Seguidamente mueve el valor de la variable local_18 al registro EAX para compararlo con el valor en hexadecimal 0x2f, en decimal 47. Con el salto condicional JE, salta si es igual o es cero teniendo activada la Zero Flag. Por tanto si nuestro valor introducido en la variable es 47, es igual al valor en decimal 47 y procede a realizar el salto(True) e imprimir por pantalla unos Strings. Si observais luego se realiza un salto jmp para cambiar el flujo de ejecución del programa hacia el final de la función.
Ahora observamos un salto jg y previamente una comparación que usará el mismo valor en hexadecimal 0x2f, pero en este caso el valor de EAX va a ser seguro si o si diferente a 47. Saltará si es mayor o si no es menor o igual. Si el valor introducido es mayor a 47 (igual no puede ser, porque si fuese igual cumpliría la anterior condición), será True. Si es menor, es False. A continuación realiza el mismo procedimiento que explicamos con el salto je para comparar si los valores 48,46 y 49 son iguales e imprimir en caso de que sea True, los Strings.
Podemos deducir que se trata de una estructura de control Switch en el cual si nuestro valor introducido es desde 46-49, ambos inclusive, va a imprimir unos Strings dependiendo de cual sea el valor.
Al final de función vemos que llama a la función fflush() para limpiar el buffer de la entrada estándar stdin, getchar() esperando a que pulsemos Enter para finalizar, y la correspondiente comprobación del valor «canary» por si ha existido algun tipo de modificación debido a un desbordamiento de buffer (o error) y así saltar a la función __stack_chk_fail().
Si reunimos todos nuestros Strings tenemos algo así: WW4gc3ludCByZjogc2p1dm9vdmd7MXkxeDNlM2kzZWYxYXR9
Sabiendo que es un base64 lo decodificamos.
Esta codificado en ROT13, lo decodificamos y obtenemos la flag.
Código fuente
#include <stdio.h> int main(void){ funcion(); } void funcion(){ int count=0; count++; count = count+2; count = 49-count; count--; int count1=0; count1++; count1 = 50-count1; int number; printf("Introduce un numero para ver la Flag: "); scanf("%d",&number); if((number>count)&&(number<=count1)){ switch(number){ case 46: printf("WW4gc3ludCByZjog"); break; case 47: printf("c2p1dm9vdm"); break; case 48: printf("d7MXkxeDNlM2"); break; case 49: printf("kzZWYxYXR9"); break; default: printf("Hola amigos! :)"); } } else{ printf("El numero no es correcto\n"); } fflush(stdin); printf("\nEnter para finalizar."); getchar(); }
Hasta aquí la entrada de hoy, seguiremos en las siguientes. Un saludo, Naivenom
3 comentarios en «Introducción al Reversing – 0x01 Introduccion»
Los comentarios están cerrados.