Tutorial GDB. Parte 2


Un poco de Info.

Cuando Ejecutamos un programa, el sistema operativo se encarga de realizar un serie de tareas para que todo funcione correctamente. Entre esas tareas se encuentran cargar el programa ejecutable en una zona de memoria libre, reservar el espacio de memoria para la pila conocida como stack y también reserva una parte de memoria conocida como heap.

La pila es la zona de memoria donde se van a ir guardando diferentes parámetros de las funciones que se ejecutan durante la ejecución del programa. Es un procedimiento normal, apilar los datos necesarios para la ejecución de una función justo antes de llamar a esa función. Una vez ejecutada la función, es la propia función la que se encarga de descargar de la pila los parámetros necesarios que necesita para ejecutarse. También se guardan variables locales y las direcciones de memoria de retorno para continuar con una correcta ejecución del programa.

En el heap se almacenan normalmente datos que han sido reservados mediante instrucciones tipo malloc o new, lo que vienen a ser instrucciones de reserva de memoria para realizar el almacenamiento de algunos datos o para realizar algún proceso en dicha zona de memoria reservada. Las reservas de memoria realizadas en el head tienen mecanismos para eliminar esas reservas de memoria una vez que no se necesiten. Normalmente son controladas por programación o bien el sistema operativo puede liberar el espacio.

Podemos encontrarnos también con una reserva de memoria dedicada para variables globales o variables estáticas dependiendo del lenguaje empleado y siempre que estos lenguajes puedan definir este tipo de variables. La ventaja de este tipo de variables es que son variables que pueden estar disponibles en cualquier momento de la ejecución del programa. A diferencia de variables locales que solo estarán disponibles en la función en la que se definan.


¿Cuando se produce un BufferOverFlow o un StackOverFlow?

Básicamente cuando por alguna forma se consigue grabar en la zona de la pila más información de la que se ha reservado para un proceso de ejecución. Si esto sucede, podemos estar grabando información en zonas de memoria donde se encuentran otros datos como por ejemplo la dirección de memoria de retorno a la que debe regresar el proceso una vez finaliza la ejecución de esa función que se está ejecutando en ese momento.

Y que pasa si se encuentra un BufferOverFlow. Pues que se podría controlar la ejecución de un programa y realizar operaciones no establecidas por el programa en ejecución o bien inyectar código malicioso para hacerse con el control del equipo donde se está ejecutando el programa.

Veamos con un programa de ejemplo porque sucede el BufferOverFlow y como podemos realizar una operación no prevista en el propio programa para extraer información.

Archivo fuente gdbcrack2.c y ejecutable gdbcrack2.


Si observamos el código vemos una variable BUFSIZE de 100 posiciones, una función flag() que imprime la flag pero que no es llamada en ningún momento por el programa, una función vuln() que simplemente realiza un input de datos con la función gets() y un output del valor introducido. La función main() muestra en pantalla el mensaje de “Entra tu clave ….” Y llama a la función vuln() y después finaliza el programa.

Como he comentado, en ningún momento se realiza una llamada a la función flag(), el objetivo de este este tutorial es explotar la vulnerabilidad de BOF motivada por la función gets() y tratar de cambiar la normal ejecución del programa para acceder a la función flag() que en ningún momento es llamada.

Pasemos ahora a la parte 2 del tutorial de gdb Debugger.

Primer Paso.

Ejecutar el programa gdbcrack2 inyectando más caracteres de los establecidos en la variable “buf”. Como vemos en el código fuente, se reservan 100 posiciones. ¿ Qué pasa si ponemos por ejemplo 120 caracteres ?

Podemos realizar una prueba de ejecución desde la terminal utilizando un pequeño script en python que rellene automáticamente  el input que requiere el programa “Entra tu clave para obtener la Flag”. El script es el siguiente:

python -c "print ('A'*120)” | ./gdbcrack2   

El resultado de la ejecución del script es el que se muestra en la siguiente imagen :


Podemos ver que el sistema lanza un mensaje de “Violación de segmento”, esto significa que se ha producido un error en la ejecución del programa y es el resultado de intentar ejecutar una dirección de memoria que no existe ocasionada por la infección de muchas “A” que probablemente han pisado información necesaria para la correcta ejecución del programa.

Segundo Paso.

Con la ayuda de gdb Debugger vamos a explorar el ejecutable y vamos a ver en que posición se modifica la Return Address de la función vuln() para tomar el control del flujo de ejecución y poder saltar a la función flag() y para ello necesitaremos también ver cual es la dirección de inicio de la función flag(), empecemos por esto último que es lo más fácil.

Obtener la dirección de memoria de flag().

Empecemos por iniciar el gdb Debugger con el ejecutable y realizar un info functions para ver las direcciones de memoria de las funciones y tomar la dirección de la función flag() los comandos a ejecutar son:

gdb ./gdbcrack2       <- para cargar el gdb con el ejecutable


info functions <- para ver las diferentes funciones


break main <- para poner un breakpoint en el main ya que necesitamos ejecutar el proceso para obtener las direcciones de memoria correctamente.


run <- Ejecuta el debugger y se parará en el breakpoint del main



disas flag <- Para obtener el desensamblado de la función flag y ver la dirección de memoria de inicio de la función.


En la captura anterior podemos ver la dirección de memoria de la función flag() 0X004011CA. Ya tenemos una de las dos piezas que necesitamos. Pasemos a la siguiente fase.

Averiguar cuando se pisa la Return Address de la función vuln().

Para tratar de averiguar la dirección de retorno vamos a cambiar el script de python que utilizamos para descubrir el BOF ( BufferOverFlow ) añadiendo unas cuantas letras diferentes que nos ayudarán en esta parte. Para ello, iniciaremos python para realizar un print con 100 “A” + abecedario con esto será suficiente.


Copiaremos las “A” y el abecedario para volver a iniciar el debugger y utilizarlo en la introducción del input del ejecutable.

Iniciamos el gdb debugger, ponemos un breakpoint en la función vul() y ejecutamos el programa con run.



Ahora continuamos la ejecución con varios ni ( siguiente instrucción sin entrar el calls ) hasta llegar la la parte donde nos pedirá la entrada del dato, no veremos que nos está pidiendo el dato ya que la impresión en pantalla de la frase “ XXXXXXX “ no se verá, pero veremos que a diferencia de los anteriores ni no muestra la siguiente dirección de memoria y se queda como esperando la entrada de datos sin el “prompt” del gdb. En la siguiente imagen se ve el proceso paso a paso con las direcciones de memoria en cada “ni”. Debemos pegar el resultado del print de python anteriormente elaborado con las 100 “A” + el abecedario y pulsamos intro.


Seguiremos ejecutando el comando ni ( siguiente instrucción sin entrar en calls ) hasta que el proceso ropa como se muestra a continuación. Podéis ver paso a paso por las diferentes direcciones de memoria que se van ejecutando. Una vez se rompa fijaros que aparece una dirección 0x706f6e6d ??, algo pasa.


Si ejecutamos un info registers podemos ver que el registro EIP ha sido alterado con valores hexadecimales correspondientes a letras del abecedario.


Ahora vamos a averiguar el contenido del registro EIP y ver el valor de las letras que oculta para detectar la posición exacta en la que se pisa el registro EIP. Para ello, ejecutamos un print chr de los valores hexadecimales en python por ejemplo. También podemos ver una tabla ASCII y localizar las letras. En la siguiente imagen se muestra como hacerlo con python por ejemplo.


Vemos que el resultado es “ponm” recordar que siempre lo veremos en orden inverso al introducido ( little endian ) por lo que si lo invertimos nos daría como resultado “mono” que ya tiene más sentido. Luego si hacemos un pequeño cálculo podemos ver que hay que inyectar 112 caracteres y después deberíamos poner la dirección de flag() para que se superponga en el registro EIP suplantando la Return Address de la función vuln() y conseguir saltar a la función flag(). 

Esto último podemos hacerlo desde la terminal modificando nuestro primer script python que utilizamos para detectar el BOF teniendo en cuenta la siguiente formula = 112 Posiciones + Return Address ese sería nuestro Payload y quedaría así:

python -c "print ('A'*112+'\xc9'+'\x11'+'\x40'+'\x00')" | ./gdbcrack2  

Importante !! La dirección de inicio de la función flag() era 0x004011c9 y si os fijáis en el script están al revés tener en cuenta que trabajamos con ( little endian ), además recordar que para este tipo de pruebas en las que trabajamos con direcciones de memoria tenéis que tener desactivo el ASRL para que el sistema operativo os dé siempre las mismas direcciones de memoria ( ya perdí varios días sin entender en su momento porque los ejemplos que yo seguía no funcionaban, hasta que encontré a alguien que explicaba esto mismo ). Para desactivarlo por ejemplo en Kali Linux que es con lo que yo realizo las pruebas tenéis que ejecutar el siguiente script desde la terminal: echo 0 | sudo tee /proc/sys/kernel/randomize_va_space esto desactiva la “Random Virtual Address” de forma que los programas se ejecuten siempre con las mismas direcciones de memoria. Queda claro, que esto es un mecanismo de defensa del sistema operativo y que en un entorno real debe estar activado. 

Y finalizando, la ejecución del script pasando el valor al ejecutable da como resultado:


Conseguido!!!. Hemos visto un claro ejemplo paso a paso de como explotar un BOF por supuesto que este tipo de vulnerabilidades permite realizar cosas más complejas que veremos más adelante como la ejecución de una shellcode aprovechando el BOF.

Hasta la próxima entrega.

gr33nc0l1bry



Enlaces de Interés.





Etiquetas