Workshop | Buffer Overflow
Johnny Pan (codeskill)
2023-11-28
Descripción:
En la siguiente taller vamos analizar dos formas de explotar el programa reto.c.
La primera forma, vamos a tratar que el programa nos imprima la bandera, accediendo a la función flag(), alterando el flujo del programa y para la segunda, vamos a obtener una shell local en la propia máquina.
Antes de comenzar debemos revisar si la configuración de ASLR (Address Space Layout Randomization) está activada en la máquina virtual de Linux, este mecanismo de seguridad genera direcciones aleatorias en el stack y en el heap para evitar las vulnerabilidades de desbordamiento de buffer. Si se encuentra habilitado será necesario desactivarlo.
Si nos retorna un 2, significa que esta activo, así que procedemos a desactivarlo, con el siguiente comando.
Si no funciona podemos usar alguno de los siguientes comandos, para deshabilitarlo temporalmente
Ahora procedemos a compilar el programa reto.c agregando ciertos parámetros que debemos indicarle a gcc, los cuales explicó a continuación.
Parámetro | Descripción |
---|---|
-fno-stack-protector | Poder sobrescribir la pila |
-z execstack | Poder ejecutar código en la pila |
-mpreferred-stack-boundary=2 | Asignar 4 bytes límite a la pila |
-m32 | Compilar el programa en 32 bits |
Durante el desarrollo de este taller, realiaremos dos pruebas de concepto que nos permitirán resolver los retos solicitados. Inicialmente se analizará el código fuente suministrado reto.c y posteriormente se utilizarán varias herramientas en GNU/Linux, así como mi experiencia en diferentes lenguajes de programación para obtener acceso a la función flag() y a la shell.
Estos métodos no son los únicos que existen y varian dependiendo del entorno en que nos encontramos.
Para realizar correctamente las pruebas de concepto, se creará un laboratorio de pruebas mediante una máquina virtual con la distribución Ubuntu Server 20.04 y nos conectaremos a la VM mediante SSH para poder copiar y pegar los comando directamente en la terminal.
En la primer prueba de concepto, se realizá una explicación paso a paso para obtener el acceso a la función flag(), modificando el flujo del programa mediante un desbordamiento de buffer.
Primero analizaremos el código fuente del programa reto.c y comentaremos el código para una mejor comprensión
Salvamos el archivo con Ctrl+O y salimos con Ctrl+X
Probamos el programa que acabamos de compilar, enviandole como argumento una cadena de caracteres para determinar su funcionamiento.
Luego utilizaremos algún lenguaje de script como PHP, Perl, Python o Ruby para generar una cadena de texto suficientemente grande como para sobrescribir la variable buffer de nuestro programa. Como sabemos que la varible buffer tiene un máximo de almacenamiento de 100 bytes, deberemos introducir más de 99 caracteres, ya que el carácter '\0' de fin de cadena
lo añade siempre al final.
Usaremos el acento invertido para pasar como argumentos de la función la salida del script. Mostraré la forma de realizarlo en diferentes lenguajes y quedará a gusto de cada quien cual lenguaje utilizar.
PHP
Perl
Python
Ruby
Vamos a ver que ocurre si probamos pasando cadenas de texto con más caracteres. Esta vez utilizaré el código en Python, iniciando en 100 caracteres e iré aumentando de 5 en 5 hasta obtener el mensaje de Segmentation fault.
Es aquí como observamos un comportamiento extraño y comenzamos nuestro análisis del desbordamiento del buffer. Vemos que si sobrepasamos los caracteres soportados obtenemos el mensaje de "Segmentation fault". Este mensaje significa que hemos sobrescrito el registro EIP y ahora apunta a una dirección no permitida.
Para saber exactamente qué hemos sobrescrito en EIP, utilizaremos el comando gdb que es el depurador que viene por defecto en Linux.
Si deseamos generar dumps de core para dar seguimiento a la
dirección de memoria, por defecto esta opción está en deshabilitada y se puede comprobar con el comando ulimit -a
, para habilitar la opción deberemos colocar la palabra unlimited o un número grande para que se puedan escribir dumps para realizar el debug, de la siguiente forma.
Los pasos con el archivo core son opcionales
Podemos generar errores de desbordamiento de buffer agregando cada vez más caracteres al argumento, de esta forma podemos darle seguimiento a las direcciones de memoria con los archivos core que se crean.
Lo analizamos de la siguiente forma.
Para esta PoC no voy a utilizar los archivos core, sino que realizaré el debug directamente desde gdb, de la siguiente manera.
Al volver a ejecutar el código anterior obtenemos el valor de EIP es 0xf7004141. Esa dirección es debido al valor de la letra "A" en hexadecimal es 0x41, es decir, hemos sobrescrito parcialmente EIP con la letra "A". Usamos el comando i r
que equivale a info registers dentro de gdb para observar el valor de los registros.
Continuamos probando y aumentamos la cantidad de "A" hasta sobreescribir completamente el registro EIP. En esta ocasión aumentaré la cantidad de "A" a 112 y vemos que sobreescribimos completamente el registro EIP con el valor 0x41414141.
Una vez que calculamos el desplazamiento adecuado, verificamos que realmente se esta escribiendo correctamente el registro EIP y cambiamos el script anterior agregando la letra "B" y restando la cantidad de letras "A".
Vemos que ahora el valor del registro EIP es 0x42424242 que equivale a "BBBB". Con estas pruebas ya podemos alterar el flujo del programa para tener acceso a la función flag().
Dentro del proceso de Debug con gdb desensamblamos la función flag() para conocer en que segmento de la memoria se encuentra escribiendo el siguiente comando.
Vemos que la función flag() está en la dirección 0x56556211 la cual debemos pasar a formato hexadecimal de la siguiente forma "\x11\x62\x55\x56". Entonces modificaremos nuestro script de Python y colocaremos la dirección en el registro EIP recordando ponerla en formato Little-endian.
De esta forma resolvimos el primer reto y según muchas recomendaciones que encontré en Internet, podemos utilizar NOP para no generar tantos caracteres en pantalla. Programando de la siguiente forma nuestro script de Buffer Overflow para obtener la función flag().
Ahora ejecutamos el programa reto pasando como argumento la salida del script en Python que acabamos de crear. De la siguiente forma.
Con eso terminamos la PoC #1.
En la segunda prueba de concepto, se realizá una explicación paso a paso para obtener una shell local en la propia máquina, modificando el flujo del programa mediante un desbordamiento de buffer.
Para realizar este ejercicio utilizamos gran parte del conocimiento adquirido en el PoC #1 y vamos a programar primero la shell en lenguaje C, que posteriormente debemos pasar a código ensamblador para realizar nuestro ataque de buffer overflow.
Comenzamos escribiendo la shell nano shellcode.c
. Existen muchos ejemplos en Internet, pero tomaremos el código fuente de la siguiente página
http://www.kernel-panic.it/security/shellcode/shellcode5.html
Compilamos el código fuente y lo probamos.
Luego pasamos el código de C a lenguaje ensamblador.
Posteriormente ensamblamos el archivo shellcode.asm y utilizamos la herramienta objdump para desensamblar el programa y así poder utilizarlo como shellcode en nuestro script de ataque.
Utilizamos gdb y realizamos los cálculos correspondiente para obtener la dirección exacta del registro ESP (Stack Pointer) cuando inyectamos el shellcode, de la siguiente manera.
En el ejercicio anterior habiamos visto que se realizaba el desborde del buffer con 112 bytes. Entonces realizamos la operación correspondiente para obtener la cantidad de caracteres que debemos incluir en la prueba.
Cantidad de caracteres = 112 - tamaño del shellcode - tamaño de EIP
Cantidad de caracteres = 112 - 38 - 4
Cantidad de caracteres = 70
run $(python -c 'print "SHELLCODE" + "A" * 70 + "BBBB"')
Ahora buscamos la dirección donde se ejecuto el shellcode. Dentro de gdb ejecutamos el siguiente comando restando los 112+4 bytes (buffer overflow + tamaño de EIP), de esta forma podemos observar el registro.
Vemos que nuestro shellcode 0x315e18eb inicia en la dirección 0xffffd4ac: por lo tanto tomamos la dirección que esta a la izquierda de 0x315e18eb, que en nuestro caso sería 0xffffd4b0 y la convertimos en formato hexadecimal ordenado de forma invertida.
Luego sustituimos la cadena "BBBB" por \xb0\xd4\xff\xff en el script de prueba.
Y con esto obtenemos el acceso a una shell local mediante una inyección de nuestro shellcode en el registro EIP del programa en ejecución.
Ahora generamos un pequeño script en Python para pasarlo como argumento al programa.
Si queremos ejecutar el programa fuera de gdb no obtendremos la shell, debido a que gdb añade algunos bytes a la pila (entre 32 y 64) y la dirección a la que saltamos ya no es la de nuestra shell.
Para solucionarlo, utilizaremos un sled de NOPs, que consistirá en llenar el buffer con la instrucción NOP antes de la shellcode, así cuando realicemos el salto a una dirección, habrá más posibilidades de que caigamos en una instrucción NOP y se deslice el flujo del programa hasta llegar a la shellcode.
Modificaremos nuestro script de nuevo añadiendo NOPs en vez de letras "A". También cambiaremos el valor de EIP por lo que hemos dicho antes de gdb y le sumaremos entre 32 y 64 bytes, en este caso 40.
Podemos usar Python para saber cuanto tiene que ser el desplazamiento. Ejecutamos el comando python y ponemos la dirección anterior de EIP y le sumamos los 112 que es el tamaño que habiamos encontraba donde ocurría el desbordamiento + 40 por los ajustes en gdb.
Modificamos nuestro script.
Probamos que el nuevo código funcione correctamente desde la terminal.
Vemos que funciona correctamente y tenemos acceso a una shell local.
Con eso damos por finalizado el PoC#2 y resolvimos los dos retos de manera exitosa.
Videos