Diseccionando binarios - 0x01 Introduccion

Publicada en Publicada en Reversing

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 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

2 comentarios en “Diseccionando binarios - 0x01 Introduccion

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *