Evitando que un programa explote, técnicas para detectar buffer overflows
Como lo prometí hace algunos días en mi artículo sobre buffer overflows, hoy les traigo las técnicas de defensa más importantes contra esta vulnerabilidad.
Existen varias técnicas que se pueden aplicar a distintos niveles de la vida de un programa, así que las dividí siguiendo esta idea. A continuación planteo qué es lo que podemos hacer, dependiendo en qué nivel nos encontremos:


qué puede hacer el programador?

El programador es el principal encargado de eliminar la posibilidad de buffer overflows, pero muchas veces es el que tiene menos idea del tema, por eso es importante que preste atención a lo siguiente:
- elegir un lenguaje que chequee los límites de un buffer antes de asignarle datos. Los lenguajes más riesgosos conocidos son C y C++, los cuales no realizan ningún chequeo y básicamente permiten que uno "haga lo que quiera". Los que si chequean correctamente los límites son los interpretados, o los compilados a bytecodes, como python, java, ruby, etc.
- si se utiliza un lenguaje como C/C++, utilizar funciones de librería que chequeen límites antes de leer datos de entrada. Hay funciones conocidas por no realizar chequeos y ser utilizadas comúnmente, las cuales deben evitarse, como por ejemplo gets, scanf, strcpy, etc. Igualmente, como muestran en Advances in adjacent memory overflows, ni siquiera utilizando versiones seguras de librerías estaremos a salvo =S
- utilizar librerías bien testeadas. A veces nuestro programa puede estar libre de bofs, pero si las librerías utilizadas tienen el citado problema, entonces estamos en peligro. El problema de los exploits encadenados es muy peligroso.
- por supuesto, como última e importante recomendación está la siempre sabia frase "trust no one", es decir, siempre que se ingresen datos al programa, chequearlos antes de asignarlos a una variable.


qué puede hacer un compilador?

Existen algunas extensiones a compiladores como gcc que permiten al compilador ayudar bastante en la protección contra los buffer overflows. Entre las extensiones más conocidas se pueden citar:

- StackGuard. Esta extensión para el compilador gcc existe desde 1997 (osea, hace rato) y lo que hace es que el compilador inserte una marca (llamada "canario") en la pila inmediatamente después de haber almacenado la dirección de retorno en la misma. De esta forma, si ocurre un buffer overflow que intenta sobre-escribir la dirección de retorno, éste deberá primero sobre-escribir el canario (pobre canario). Antes de retornar, la función revisará el valor del canario y si detecta que fué sobre-escrito, arrojará la excepción segmentation fault. El canario es un valor difícil de utilizar por un atacante y puede ser de tres tipos: terminator (NULL, CR, LF, -1), random y random xor. Es más seguro utilizar valores random para el canario, pero otros terminadores como NULL serían difícil de insertar en un buffer overflow que intente sobre-escribir la dirección de retorno.

- Stack-Smashing Protector (ProPolice). Otra extensión para gcc que funciona de manera similar a StackGuard, esto es, utilizando un canario para detectar sobre-escritura. SSP presenta una mejora al sistema de StackGuard, dado que protege todos los registros salvados en el prólogo de una función, no sólo la dirección de retorno.
SSP actualmente es estándar en OpenBSD (el SO más seguro del mundo), FreeBSD, Ubuntu, DragonFly BSD, IPCop.

En el ambiente actual, StackGuard y SSP son los más utilizados y mantenidos, pero también han surgido otras extensiones interesantes, que parecen no seguir siendo soportadas (no encontré info que supere el año 2000). Debido a que valen la pena, las menciono a continuación:

- Stack-Shield. También trabaja con gcc (es un preprocesador en assembler) y protege tanto de buffer overflow directos e indirectos. Su funcionamiento se basa en agregar instrucciones durante la compilación para copiar las direcciones de retorno en un segmento de datos aparte. Sería difícil para un atacante modificar la dirección de retorno almacenada en la pila y la copia en el segmento de datos al mismo tiempo. Cuando una función retorna, se comparan ambos valores y se lanza un alerta si no coinciden.
Stack Shield también provee un mecanismo secundario. Este se basa en implementar un chequeo de rango en la llamada a función y la dirección de retorno. Si el programa intenta realizar una llamada a una función que caiga fuera del rango predefinido, o si una función retorna a una locación fuera del rango, entonces se asume que ocurrió un ataque y se termina el proceso.

- Return Address Defender (RAD). Otro parche para gcc que funciona de forma similar a StackShield. Este almacena una copia de la dirección de retorno en un repositorio, pero luego usa las funciones de protección de memoria del sistema operativo para detectar ataques contra este repositorio. RAD hace que el repositorio entero sea read-only o bien marca las páginas vecinas como read-only.

Cabe citar que Microsoft también provee un parche similar en Visual Studio llamado "Microsoft Visual Studio /GS". Este funciona de forma muy similar a StackGuard, utilizando canarios para proteger direcciones de retorno y frame buffer.
Existe un paper interesante llamado "Four Different Tricks to bypass StackShield and StackGuard protection" donde se explica cómo es posible saltear las protecciones de StackShield, StackGuard e incluso la de Microsoft Visual Studio /GS.


qué se puede hacer una vez que se ejecuta el programa?

Una vez que el programa llegó categoría ejecutable, tendremos que atrapar los problemas que nos relegaron los programadores a nivel de sistema operativo. Para esto se han desarrollado algunas modificaciones que harán lo posible por proteger al sistema contra los buffer overflows. Las técnicas más conocidas son las siguientes:

- Marcar los segmentos de datos y pila como NO ejecutables. Esa es la salida más lógica y fácil de implementar, dado que el hardware hace años que ofrece facilidades marcar las páginas/segmentos como read-only, escribible, ejecutable y algunas otras variantes. Es decir, simplemente tenemos que setear un bit para marcar las páginas como no ejecutables. Si hacemos que la sección de datos y la pila no sean ejecutables, evitaremos que el atacante ejecute código insertado por él en el overflow. Sin embargo esta solución no es completa, dado que el atacante todavía puede realizar el conocido ataque return to libc, donde se modifica la dirección de retorno para apuntar a una librería de sistema cuya dirección es conocida, y utilizar variables sobre escritas como parámetros de la función.

- Address space layout randomization (ASLR). Esta técnica involucra organizar aleatoriamente la posición de áreas de datos claves, incluyendo la base de lo ejecutable y la posición de librerías, heap, y stack en el espacio de direcciones del proceso. Esta randomización permite evitar que un atacante pueda precedir las direcciones de los objetivos.
Esta solución permite evitar ataques del tipo return to libc, dado que necesita localizar el código a ejecutar, además, ataques que inyectan shellcode en la pila deberán hallar la dirección de la pila primero.

- Utilizar una modificación de librería llamada Libsafe, la cual intercepte todas las llamadas a funciones vulnerables conocidas y ejecute una versión "segura" de las llamadas. Las versiones seguras estiman un limite para el tamaño del buffer destino.
El inconveniente de Libsafe es que sólo añade seguridad a funciones conocidas y sólo en programas linkeados dinámicamente.

- Proof-carrying code. Esta técnica basa su funcionamiento en una prueba (poof) sobre cómo un programa debería funcionar. A medida que el programa se ejecuta se observa el comportamiento y se compara con la prueba, si se detecta una desviación, estaremos frente a un problema y el programa puede abortarse.
El problema de este mecanismo es que los programas deben venir con esta prueba, la cual debe ser desarrollada con precaución, y el mecanismo puede significar una sobrecarga apreciable para el sistema.

- Program Shepherding. Esta técnica previene la ejecución de código malicioso monitoreando todas las transferencias de control (jmp, call, return, etc) para asegurarse que cada una satisface una dada política de seguridad. Para lograr esto utiliza tres técnicas: código orígen restringido (se clasifica qué código puede ser ejecutable y qué no), transferencias de control restringidas (condiciona qué saltos son válidos), y agregando Sandboxing en las operaciones.
Como uno se puede imaginar, este sistema, si no está bien optimizado, podría incurrir en severos problemas de performance.


Tanto el seteo de los segmentos/páginas de datos como no ejecutables, como ASLR están incluídos en el parche para el kernel de linux PaX. PaX implementa protección de mínimos privilegios para las páginas de memoria. Hasta donde tengo entendido, PaX aún no fue incorporado en el kernel por defecto debido a que se considera en estado beta.
Por su parte, Windows también incorporó la protección de datos a partir de XP SP2 a través de Data Execution Prevention (DEP), aunque no viene activado por defecto por compatibilidad hacia atrás. Este puede activarse, pero he leído sobre algunos programas que dejan de funcionar correctamente. También a partir de Windows Vista se incorporó ASRL en los SO de Microsoft.


Cómo puede ayudarnos el hardware?

Como sucede con todo nuevo reto, el hardware se va adaptando para ayudar al sistema operativo a realizar sus funciones. Ayudas como los bits de protección entre modo monitor y modo usuario, bits de protección de páginas y muchas otras cosas se han ido incorporando a medida que surgía la necesidad. Por esto es importante saber qué puede brindarnos el hardware a la hora de defendernos de los buffer overflows, y qué implementaciones están haciendo uso de ello.
Entre los proyectos más destacados, podemos citar:

- SmashGuard: usa versiones modificadas de las micro codificadas instrucciones para CALL y RET para habilitar de forma transparente protección contra ataques por buffer overflow. Esta solución aprovecha que las CPUs modernas cuentan con grandes espacios de memoria para crear una pila secundaria para almacenar las direcciones de retorno (similar a lo que hace StackShield). Al igual que StackShield, cuando se realiza un CALL, se almacena la dirección de retorno tanto en la pila común como en la pila secundaria, luego, cuando se ejecuta el RET, se comparan los valores de la pila con la copia y si difieren ocurre una excepción.
Cabe destacar que ésta solución no es parte de ningún CPU, sólo se ha probado en simuladores.

- Split Stack and SRAS (secure return address stack). Este sistema (similarmente a SmashGuard) divide la pila en dos, una pila control donde se almacenan las direcciones de retorno y una pila de datos, donde, obviamente, se almacenan los datos. Gracias a esta división, sería imposible que un overfow sobre-escriba una dirección de retorno.
Split Stack presenta tanto una solución a nivel de compilador como a nivel de arquitectura, así que podría haber entrado en la sección de lo que puede hacer el compilador.
Por otra parte se ofrece la solución Secure Return Address Stack (SRAS), donde se almacenan en el SRAS todas las direcciones de retorno cuando se realiza un CALL que luego se usan en las subsiguientes instrucciones RET.

- StackGhost. Parche para el kernel de OpenBSD que corre en arquitecturas Sparc que ajusta las rutinas spill/fill de venana de registros para hacer que los buffer overflows sean mucho más difíciles de explotar. Este usa una característica del hardware para detectar modificaciones en las direcciones de retorno transparentemente, automáticamente sin requerir ninguna modificación en las aplicaciones.
Es necesario conocer un poco de la arquitectura Sparc para entender mejor cómo funciona esta técnica, así que en las referencias les dejo el nombre del paper donde lo explican.


qué se puede hacer a nivel de red?

A nivel de red podemos evitar que exploits conocidos lleguen a tocar nuestras máquinas utilizando sistemas de detección de intrusos (IDS) como snort, los cuales buscan patrones de exploits en los payloads de los paquetes. Esta es la primer barrera que podemos poner para evitar que los problemas ni siquiera lleguen a las máquinas.


y qué hay de los antivirus?

Muchos antivirus modernos incorporaron mecanismos de protección contra buffer overflows, los cuales suelen funcionar bastante bien. He navegado en busca de explicaciones sobre los mecanismos utilizados para realizar tal tarea, pero no tuve éxito. Siempre que se busca información sobre un producto comercial propietario, uno suele encontrar solo información comercial (alias, yo lo hago mejor que todos mis competidores).
Igualmente ninguno inventa nada nuevo, así que deben utilizar alguna de las técnicas de tiempo de ejecución que cito en este artículo. Si bien no confiaría ciegamente en ésta protección, no cuesta nada tenerla activada (bueno, tal vez algunos recursos =P), he hecho algunas pruebas con un McAfee 8.5i y la verdad que funciona bastante bien, detectó todos los overflows.

Conclusiones

A lo largo de los años se han propuesto muchas técnicas para prevenir/detectar/evitar buffer overflows, muchas de las cuales llegaron a implementarse y realizan un trabajo decente. Sin embargo, el problema de la compatibilidad, el "hacer que todo sea más fácil de usar" y la ignorancia de los administradores lleva a que los mecanismos de seguridad no se utilicen o vengan desactivados. Además la ignorancia de los programadores, en conjunto con las presiones de tiempos, llevan a que los programas sigan incorporando errores recontra conocidos y explicados, y es por ello que hoy día sigan existiendo tantos problemas de overflows explotables.
Es importante poner énfasis en los controles de programación, porque todo surge de allí. Es interesante ver cómo OpenBSD ganó su fama del sistema más seguro gracias a los chequeos de códigos y los mecanismos de prevención que presenta, muchos deberían copiar aunque sea parte del trabajo de OpenBSD.


Referencias

Prevention and Detection of Stack Buffer Overflow Attacks
Buffer Overflow Protection wiki
ASRL wiki
Buffer Overflow wiki
Type-Assisted Dynamic Buffer Overflow Detection
Secure Execution Via Program Shepherding
Architecture Support for Defending Against Buffer Overflow Attacks
StackGhost: Hardware Facilitated Stack Protection
Dynamic Buffer Overflow Detection
Network-Based Buffer Overflow Detection by Exploit Code Analysis

2 comentarios:

JaviZ dijo...

yep, OpenBSD RuleZ! :)

Definitivamente el tema está muy bueno y tu desarrollo es claro y conciso, si... dije conciso: hay mucho escrito al respecto y nuestro bloggero lo resume muy bien!

Salú,
J

d3m4s1@d0v1v0 dijo...

Hoy encontré una técnica que permite bypassear los controles DEP y ASLR. Hasta hace tiempo era solo teoría, pero hace unos días salió el PoC.
La mencionada técnica se llama JIT-Spray, y utiliza compiladores Just In Time... habrá que seguir la idea para ver sus progresos:
http://www.securityfocus.com/archive/1/509910

Publicar un comentario