Analytics

miércoles, 21 de octubre de 2015

Mitigación de vulnerabilidades en binarios (parte II)

Cuando hablamos de mitigaciones hablamos de una capa extra de seguridad o un parche temporal ante un problema que no tenga solución inmediata, exclusivamente. Es decir, no es una solución permanente a un problema, no puede sustituir a un parche de seguridad, no protege ante todas las variantes de explotación de una vulnerabilidad y, en definitiva, no es infalible, por lo que debería ser el último recurso disponible cuanto a seguridad se refiere.

Para ilustrar estas afirmaciones expondremos un caso “real” donde las protecciones anti-explotación no fueron suficientes para evitar la ejecución de código arbitrario.

Se trata de un CTF (Capture The Flag) creado precisamente para tratar de explotar vulnerabilidades en un entorno real (pero controlado). El objetivo es aprender sobre explotación de sistemas en un entorno lo más real posible.

Entre la gran variedad de desafíos como este disponibles, vamos a usar como ejemplo “TUX” de SmashTheStack (http://tux.smashthestack.org:86/), en concreto el nivel 1. El objetivo es explotar un Buffer Overflow real, encontrado en un servicio FTP de Linux, en concreto ProFTPD 1.3.2c.

El principal problema son las protecciones con las que fue compilado el servicio:



Como se puede observar, el binario incluye varias tecnologías de mitigación incorporadas: RELRO parcial, NX y SSP; además está enlazado dinámicamente por lo que las direcciones de las funciones en memoria son calculadas en tiempo de ejecución, lo que impide utilizar direcciones estáticas.



A nivel de sistema hay aplicada otras mitigaciones, como ASCII ARMOR, que evita que se puedan introducir direcciones de librerías directamente a través de funciones de manejo de cadenas:




Para rizar más el rizo, se asumirá que también está habilitado ASLR, aunque no sea así, para demostrar que el uso de más protecciones no significa que sea inexplotable.

Ciertamente estas medidas dificultan la explotación, pero un atacante con el suficiente  tiempo y motivación podría tratar de evadirlas ya que no son infalibles, como se verá a continuación.

Resumiendo, se trata de evadir las siguientes protecciones:
  • ASLR
  • NX
  • ASCIIARMOR
  • RELRO(parcial)
  • SSP o Canary
El servicio que se va a explotar tiene una vulnerabilidad pública con identificador  CVE-2010-4221:
Múltiples desbordamientos de pila en la función pr_netio_telnet_gets de netio.c, en el servicio ProFTPD en versiones inferiores a la 1.3.3c, permite a atacantes remotos ejecutar código arbitrario a través de paquetes específicamente diseñados que incluyen el carácter de escape TELNET IAC.

Hay exploits disponibles, pero no son compatibles con esta plataforma. Con unas pequeñas modificaciones se podrían utilizar, pero no sería ni la mitad de divertido ni didáctico que hacerlo uno mismo partiendo de cero.

El objetivo de este artículo no es entrar en detalles sobre las técnicas comunes de desarrollo de exploits para vulnerabilidades de tipo Stack Overflow. Se asume un conocimiento básico de métodos de explotación de este tipo de fallos, así que se nos focalizaremos en la evasión de las protecciones mencionadas anteriormente.

Resumiendo, según las mitigaciones detectadas, se podría descomponer el problema en tres objetivos principales que habrá que ir superando:
  1. Impedir que SSP detecte la escritura fuera de límites.
  2. Conseguir emplazar datos/código arbitrario en una zona de memoria controlada
  3. Redirigir el flujo de ejecución hacia dicha zona de memoria

Tras determinar el tamaño real de la pila y tratar de modificar el flujo de ejecución entra en juego la primera mitigación, el SSP.

De nada sirve situar código arbitrario en memoria si no tiene ninguna posibilidad de ejecutarse. Por tanto, al tratar de explotar este desbordamiento de pila, el primer problema que se tiene que sortear es Stack Smashing Protector y el canario que sitúa en el epílogo de la función vulnerable, ya que detiene la ejecución en caso de verse alterada la pila.

A modo de ayuda (para evitar que se alargue la explotación) los creadores del reto modificaron la función que genera la secuencia de bytes de comprobación de la pila (canario) por otra con menos entropía.



Lo que equivale a un entero sin signo de 32 bits, donde el byte menos significativo es siempre 0, como se puede ver en la siguiente captura.




Esto reduce la entropía a 24 bits, una longitud lo suficientemente manejable como para ser obtenida por fuerza bruta en un periodo razonable de tiempo.

Una característica de este demonio que puede ser útil a la hora de explotarlo es que paraleliza la ejecución para poder atender múltiples clientes mediante procesos hijos, con la función fork().

Esta función crea una copia exacta del proceso padre, hasta el punto donde se crea el proceso hijo, de manera que puedan seguir flujos de ejecución distintos pero partiendo de los mismos datos ya almacenados en memoria.

Esto es relevante para la explotación ya que cada vez que hagamos una conexión nueva, se creará una copia del proceso padre incluyendo el valor del canario almacenado, es decir, es valor no cambia tras sucesivas conexiones hasta que el demonio es reiniciado, incluso aunque el proceso hijo caiga en condición de error y finalice.

Gracias a esto, es posible realizar ataques de fuerza bruta sin temor a que el valor del canario cambie entre sucesivos intentos.

Una sencilla forma de saber si hemos dado con el canario correcto, es mandar una petición del tamaño exacto para sobrescribirlo, sin llegar a tocar el valor de retorno almacenado justo a continuación. Una vez enviada la petición, sólo se debe comprobar la respuesta del servidor:
  • Si el servidor no responde significa que el proceso hijo ha detenido su ejecución y, por tanto, el canario no es correcto.
  • En caso de producirse una respuesta, podemos asegurar que el último valor enviado es el correcto y lo será hasta que se reinicie el demonio.
En este caso se han enviado sucesivos paquetes con la siguiente estructura:

AAAAAAAAAAA... 0xFF AA 0xXXXXXX00
Relleno
(1021 bytes)
TELNET IAC
(1 byte)
Relleno
(2 bytes)
Canario
(4 bytes)
Total: 1028 bytes


donde se han reemplazando las “X” por posibles valores del canario. En este caso, los generados por fuerza bruta.

Una vez se ha confirmado el canario correcto (y por tanto se ha evadido SSP) se puede empezar a hablar de modificar el flujo de ejecución.

Lo fácil sería localizar la dirección de memoria donde se almacena nuestro código mediante GDB, modificar el valor de retorno guardado para que salte a dicha dirección y se ejecute. Aquí es donde entran en juego las mitigaciones: ni se puede obtener una dirección constante entre ejecuciones (ASLR) ni se puede ejecutar código almacenado en la pila (NX).


¿Cómo se puede evadir ASLR y NX?
La sección .text, que contiene el código principal del programa, no está afectada por ASLR (no es la única) y suele cargar en la misma dirección base (en x86 es 0x0804XXXX). A su vez, al tratarse de la sección principal del programa, debe tener permisos de ejecución, por lo que tampoco está afectada por NX.

Por tanto, si se pudieran encadenar instrucciones concretas de dicha sección .text, se podría armar una shellcode por partes.

La mejor forma de lograr esto actualmente es mediante una técnica denominada ROP (Return Oriented Programming).

Esta técnica se basa principalmente en la reutilización de partes concretas del propio código original del programa, encadenándolas unas con otras mediante instrucciones ret (de ahí su nombre). Lo único que se necesita para modificar el flujo de ejecución es preparar la pila con las direcciones de la sección .text que interesen para construir la shellcode. De esta manera, se controla hacia donde se produce cada salto al ejecutar la instrucción ret.

Existen varias suites que realizan la búsqueda de direcciones útiles (para realizar una shellcode) dentro del binario.

Se ha utilizado la herramienta ROPgadget que automatiza la extracción de “gadgets”, que es como se denomina los trozos de código reutilizado del propio programa que acaban en una instrucción ret.




No es necesario un gran número de instrucciones distintas para armar una shellcode, en principio basta con tres tipos:
  • Carga.
  • Adición.
  • Transferencia de control indirecto.
Sin embargo, si se desea ejecutar código más elaborado, es necesaria la interrupción int 0x80. Esta instrucción es necesaria para ejecutar cualquier syscall, por tanto si no se encuentra un gadget que la contenga, se reduce mucho el abanico de posibilidades de la shellcode, como es este caso.

Para complicar más las cosas, el binario está enlazado de forma dinámica, por lo que las funciones importadas se ejecutan desde sus respectivas librerías, que son cargadas por el sistema en memoria al inicio del programa, calculando sus direcciones en tiempo de ejecución. Por tanto, no se incluye el propio código de dichas funciones en el programa, lo que en la práctica significa que el número de gadgets disponibles se reduce drásticamente. Tampoco es posible hardcodear la dirección de ninguna función importada, ya que también están afectadas por ASLR.

Entonces, ¿cómo es posible ejecutar una syscall si no se encuentra el gadget mencionado anteriormente?

Existen dos secciones de los ficheros ELF llamadas GOT (Global Offset Table) y PLT (Procedure Linkage Table) encargadas de enlazar las funciones importadas con sus respectivas librerías cargadas en memoria.




La PLT almacena las direcciones de memoria de las funciones utilizadas por el programa, de forma que cuando se ejecuta una función almacenada, en realidad se está llamando a la correspondiente entrada en esta sección, que a su vez realiza el salto directo (normalmente mediante JMP 0xLIB_ADDR) a la dirección de memoria donde comienza el código de la función solicitada.

La sección GOT entra en juego cuando se trata de un ejecutable enlazado dinámicamente, las direcciones de los saltos almacenados en la PLT deben ser calculados en tiempo de ejecución antes de realizarse. Para ello, el linker carga en memoria las librerías utilizadas por el programa y las reubica: asignándole a cada librería un rango de memoria y estableciendo en la sección GOT las direcciones de las funciones. Cuando esto ocurre, los saltos de la PLT se hacen de manera indirecta pasando por la dirección almacenada en la GOT (JMP *0xGOT_ADDR)

El flujo de ejecución, por ejemplo, cuando se llama a la función tzset(), sería algo así:


Por tanto, una alternativa lógica sería leer la dirección de alguna función interesante (por ejemplo system()) directamente de la GOT y saltar a ella. Pero, para poder hacerlo, es necesario que el programa llame en algún momento a la función que nos interese y en este caso no es así.

Un punto a tener en cuenta, es que el contenido de las librerías es constante entre distintas ejecuciones, lo único que cambia es la dirección base donde será cargada (culpa de ASLR). Esto quiere decir que la distancia entre el inicio de la librería y una función cualquiera es constante, así como la distancia entre dos funciones cualesquiera de la misma librería, independientemente de la sección de memoria donde sea cargada.

De esta forma, es posible calcular donde estará situado el inicio de una función arbitraria de la librería en memoria, partiendo de la dirección de cualquier otra función de dicha librería.

A esta técnica se le denomina GOT dereferencing.

Si no es posible construir una shellcode funcional con los gadgets extraídos del binario, ¿por qué no utilizar directamente las funciones que nos ofrece una librería como es libc? El objetivo es obtener la dirección en memoria de cualquier función (por ejemplo system()) a partir de otra cuya dirección sí esté presente en la GOT (read() puede servir).

A continuación se muestran las direcciones donde han sido cargadas (en esta ejecución) las funciones que interesan para el desarrollo del exploit:



Para calcular la distancia de system() desde read(), con GDB es suficiente:



0x83dc0 es el valor que hay que restarle a la dirección de read() para obtener la de system().

Aquí existe otro problema, con el reducido número de gadgets que se han podido extraer, no es posible restarle dicho valor a la dirección de read() y saltar hacia ella directamente.

Si no es posible mediante gadgets colocar la dirección calculada de system() en algún registro o posición de memoria y hacer un salto directo, ¿cómo se puede conseguir redirigir el flujo del programa hacia dicha función? Mediante otra técnica llamada GOT overwriting.

Como su nombre indica, esta técnica consiste en reemplazar la dirección existente de una función en la GOT por otra. De esta manera, al intentar ejecutar la función original en cualquier parte del programa, se estará llamando a la dirección sobreescrita, lo que permite redirigir el flujo de ejecución.

Desarrollo del exploit

El objetivo de los CTF, por norma general, es obtener el contenido de un archivo protegido con permisos superiores.

Las acciones que debe llevar a cabo el exploit para conseguir la ejecución de código arbitrario se resumen en:
  • Evadir la detección de SSP mediante fuerza bruta.
  • Evadir ASLR y NX mediante ROP.
  • Evadir la limitación de código reutilizable (gadgets) y ampliar las posibilidades de la shellcode mediante GOT dereferencing y las funciones de la libc.
  • Redirigir el flujo de ejecución mediante GOT overwriting, por falta de gadgets que permitan realizar un salto directo.
Al ser un demonio, es posible explotarlo de forma completamente remota. Pero esto es un CTF, y se limitan las conexiones a la máquina. Por tanto, y para evitar maldades, el payload ejecutará un fichero local en lugar de una consola remota. Dicho fichero solo copiará el contenido del archivo objetivo a la ruta /tmp y le dará permisos de lectura.

Se utilizarán los siguientes gadgets obtenidos mediante ROPgadget:
  • 0x8080f04L: pop eax ;;
  • 0x80534b2L: pop ebx ; pop ebp ;;
  • 0x805c2f2L: add [ebx+0x5e5b10c4] eax ; pop ebp ;;
  • 0x804a71dL: xchg ecx eax ;;
  • 0x80c5ac6L: mov [ecx+0xf228] eax ; xor eax eax ; pop ebp ;;

Mediante estos gadgets (los que hemos utilizado, pero no son los únicos) es posible armar una shellcode funcional.

Se pueden distinguir dos partes:
  1. Fuerza bruta a SSP: El exploit enviará repetidamente paquetes con la longitud justa para sobrescribir el canario con distintos valores (utilizando el mismo algoritmo de generación que el programa que se pretende explotar) y esperará la respuesta, que en caso de producirse, confirma que se ha obtenido el valor correcto.
  2. Ejecución de código arbitrario: Con el valor del canario obtenido en el paso anterior, se procede a enviar el payload final que ejecutará la shellcode, formada por cadenas ROP.

El payload final, una vez obtenido el canario, quedaría de la siguiente manera:

AAAAAAAAAAAAAAAA... Relleno (1021 bytes)
0xFF Disparador (1 byte)
AA Relleno (2 bytes)
0xXXXXXX00 Canario (4 bytes)
AAAAAAAAA... Relleno (28 bytes)
0x08080f04…. ROP Chain/Shellcode (104 bytes)


Todo lo que se ha explicado hasta este punto, son los conceptos teóricos necesarios para realizar la explotación, pero queda todo mucho más claro si es posible acompañarlo con un código que lleve a la práctica todos los conceptos. Por ello, se incluye el código del exploit al final del artículo.

Lo único que faltaría, sería colocar el archivo que se desee ejecutar en la ruta /tmp/aa.

Por ejemplo:

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

void main ()
{
    system (
“/bin/cp /pass/level2 /tmp/clave_nivel_2.txt;”
     “chmod 777 /tmp/clave_nivel_2.txt”
);
    
    exit(0);

}

En este caso copia el fichero con la clave a una ruta accesible y le da permisos para poder leerlo, con esto se consigue pasar al siguiente nivel.

En definitiva, se ha conseguido evadir múltiples mitigaciones y demostrar que no son la panacea. Por todo esto, es indispensable contar con auditorías externas de código realizadas por profesionales que, de forma objetiva, identifiquen fallos que podrían abrir la puerta a atacantes, incluso contando con medidas de bastionado y/o mitigación de vulnerabilidades.

Fin del juego.

Referencias

Código del exploit





Autor: Jose Antonio Perez. 
Departamento de Auditoría.