Mitigación de vulnerabilidades en binarios

Últimamente se están publicando nuevas vulnerabilidades críticas casi a diario, muchas de ellas son explotadas como 0-day masivamente in-the-wild. Ya no basta con tener el sistema actualizado y un buen antivirus, los cibercriminales usan cada vez métodos más sofisticados. La demanda de vulnerabilidades sin parchear (0-days) en aplicaciones muy distribuidas se abre paso en el mercado negro, donde un fallo de este tipo se vende al mejor postor.

Por ello es cada vez más importante una correcta configuración de los sistemas, contar con una adecuada política de seguridad y disponer de medidas de detección/mitigación de vulnerabilidades.

Con el paso de los años y la evolución de exploits, los distintos sistemas operativos han tratado de dificultar la explotación de bugs que puedan conducir a la ejecución de código arbitrario, por ejemplo añadiendo comprobaciones en tiempo de ejecución, restringiendo la ejecución en determinadas zonas de memoria o asignando dicha memoria de forma aleatoria.

Algunas de estas protecciones han sido adaptadas por igual en los sistemas operativos más usados (Windows, Linux, OSX, Android, etc.), aunque su aplicación está más extendida en unos que en otros. Otras medidas de seguridad son específicas o implementadas de diferente manera dependiendo del sistema o de las características del formato de ejecutable (PE en Windows, ELF en Linux, Mach-O en OSX, etc.).

Las protecciones principales son comunes, aunque llamadas de distinta manera según el sistema operativo:

  • Aleatorización de direcciones de memoria: Address Space Layout Randomization (ASLR), las direcciones de memoria (stack y heap) en cada ejecución cambian, lo que dificulta calcular la situación exacta del código malicioso en memoria (shellcode).
  • Zonas de memoria no ejecutables: Not eXecutable (NX) en Unix y derivados, Data Execution Prevention (DEP) en Windows. Marca determinadas zonas de memoria como no ejecutable, normalmente la pila (stack).
  • Detección de sobreescritura de la pila: Stack canary / Stack Smashing Protector / ProPolice. Añade un epílogo a las funciones que para verificar que la pila se encuentre intacta antes de continuar con la ejecución. A grandes rasgos, funciona como un canario en una mina (de ahí su nombre): si el canario muere, hay que detener el trabajo y salir. Lo que hace en la práctica es insertar un valor aleatorio en la pila durante el prólogo de la función y comprobarlo en el epílogo. Si los valores difieren, la pila fue modificada y no es segura, por lo que se detiene la ejecución.
Además de estas medidas, cada sistema dispone de sus propias protecciones y herramientas de seguridad que dificultan aún más la explotación.

En el caso de Windows:
  • Protección contra sobreescritura de SEH: Structured Execution Handling Overwrite Protection (SEHOP), intenta detectar si la tabla de manejadores de excepciones (SEH) ha sido modificada antes de pasarle el control de la ejecución. Muchos de los exploits utilizan este método (reescritura de SEH) para controlar el flujo de ejecución del binario.
  • Control de la tabla de funciones exportadas: Export Address Table Filtering (EAF+). Cuando se intenta leer la tabla de funciones exportadas por las librerías NTDLL.dll, KERNEL32.dll o KERNELBASE.dll, trata de comprobar si el acceso es legítimo. Es una técnica usada por atacantes para tratar de evadir ASLR a partir de una dirección de memoria conocida.
En el caso de la familia Unix, debido a su personalización y modularidad, existen distintos métodos y capas de protección.

Estos métodos se distinguen según sean implementadas las protecciones:
  • Alerta del uso de funciones potencialmente vulnerables: FORTIFY_SOURCE. Es una directiva del compilador, analiza el código en busca de funciones peligrosas: gets(), strcpy(), strcat(), etc. y alerta de su uso. En su configuración más estricta también añade comprobaciones en tiempo de ejecución para evitar la escritura fuera de límites (overflow).
  • Mapeo de direcciones de memoria con byte nulo: ASCII Armor. Mapea la memoria estableciendo un byte nulo (0x00) en la dirección. Esto debería dificultar la explotación de funciones que manejen cadenas de texto, ya que el byte nulo detiene la copia en funciones como strcpy() o strcat() e impide seguir copiando código malicioso.
  • Sección .GOT de solo lectura: RELocation Read-Only (RELRO). Establece la tabla de importaciones como solo lectura. Esta tabla almacena las direcciones de las funciones importadas. Es una técnica habitual, desde el punto de vista de un atacante, intentar sobrescribir alguna de las direcciones originales, para que al llamar a esa función el flujo del programa sea redirigido hacia una dirección arbitraria.
  • Direcciones de memoria relativas: Position Independent Executable (PIE). Las direcciones de memoria utilizadas no son calculadas y escritas por el enlazador al iniciar la aplicación como es habitual, sino que son pre-calculadas a la hora de compilar. Por tanto, el código cargado funcionará independientemente de la zona de memoria en la que se emplace, ya que los saltos y cálculos de direcciones son relativos a la posición actual. Esto permite situar la dirección base del ejecutable de forma aleatoria, algo que no implementa ASLR, que sólo aleatoria el stack y el heap pero deja fuera otras secciones como .text, que en sistemas x86 suele comenzar siempre con 0x0804XXXX. Esta dirección base común puede ser explotada para evadir otras protecciones mediante técnicas ROP (Return Oriented Programming), consistente en reutilizar partes del mismo programa (la dirección no cambia tras cada ejecución), encadenados de tal manera que permiten la ejecución arbitraria de código.
COMO APLICAR LAS DISTINTAS MEDIDAS DE MITIGACIÓN

En Windows, para aplicar estas protecciones al sistema de forma global (sin tener que recompilar), se puede utilizar la herramienta EMET (Enhaced Mitigation Experience Toolkit).

Además, Visual C++ desde la versión 2003 implementa varias de estas medidas por defecto como ProPolice.

En Unix existen distintos parches y módulos para el kernel que mejoran la seguridad y establecen medidas de mitigación de vulnerabilidades.

Los más conocidos son:
  • Grsecurity: Es un complemento de seguridad para el kernel, implementa multitud de medidas de seguridad no sólo de mitigación. El principal componente de mitigación de vulnerabilidades se llama PaX, un parche de seguridad que establece los permisos mínimos para las zonas de memoria, así como NX y ASLR.
  • Selinux: Desarrollado en un principio por la NSA estadounidense, consta de un módulo de seguridad para el kernel de Linux y varias herramientas para el entorno de usuario, enfocado principalmente a establecer fuertes políticas de seguridad y controles de acceso al núcleo.
  • Apparmor: Similar a Selinux. La principal diferencia radica en la forma en la que se controlan las políticas de seguridad. Mientras que en Selinux las protecciones se aplican basadas en inodos del sistema de archivos, en Apparmor se controlan mediante las rutas de acceso en el sistema. Apparmor se considera más sencillo de configurar que Selinux, por lo que está más extendido su uso entre la comunidad Unix.

Además, el compilador GCC actualmente implementa por defecto varias de las medidas anteriores, como el SSP, y otras directivas son opcionales y se indica a la hora de compilar, por ejemplo FORTIFY_SOURCE o RELRO.

CONCLUSIÓN
Aunque todas estas medidas de mitigación añaden una capa adicional (o varias) de seguridad, no se puede confiar la integridad de un sistema exclusivamente en ellas.
Hoy en día tener un sistema sin ningún fallo de seguridad es una utopía, sin embargo se puede contribuir a la reducción del impacto y a hacer cada vez sistemas más seguros.
En general, una vulnerabilidad se puede deber a 3 causas: problemas de configuración, problemas de actualización o 0-days.

La configuración de un sistema son los cimientos en lo que a seguridad se refiere. Por ejemplo, una aplicación desarrollada siguiendo las directrices y buenas prácticas de la industria puede resultar comprometida si se configura de forma incorrecta. Además de las configuraciones recomendadas para la aplicación proporcionadas por el fabricante, podemos contar con guías de bastionado de sistemas y servicios que ayudan a limitar el impacto en caso de encontrarse una vulnerabilidad sin afectar a los clientes legítimos.

Eso es un buen punto de partida, pero hay que acompañarlo de una buena política de actualizaciones y procedimientos para mantener el sistema actualizado sin afectar a su disponibilidad, que también es importante.

Aun siguiendo una correcta política de actualizaciones y disponiendo de un sistema bien configurado, siempre existe la posibilidad de que aparezca un 0-day que ponga a prueba todas estas medidas. Un 0-day se puede deber a varias causas, pero existen medidas para controlar el desarrollo de una aplicación para que ésta sea segura: incorporando prácticas de seguridad en cada etapa del SDLC y realizando un mantenimiento continuo tras su puesta en producción y operación.

Debido a la visión cerrada que tienen los desarrolladores, que frecuentemente trabajan sólo en una parte específica del proyecto, las pruebas realizadas pueden no ser todo lo exhaustivas que deberían ser y algo puede, finalmente, escapar al radar. Lo que ocurre en este campo se puede aplicar a todo lo dicho anteriormente.

Por ello, lo más importante una vez creemos que el sistema es seguro, independientemente de las medidas que se adopten, es evaluar que lo que se ha hecho (o creemos que se ha hecho pero no es así), se ha llevado a cabo de manera correcta.

La mejor manera de validar todos estos aspectos, es a través de evaluaciones periódicas del nivel de seguridad, preferiblemente por un tercero, que aporte una visión global de la infraestructura desde distintos puntos de vista de la seguridad, en definitiva, como lo haría un atacante con la suficiente motivación o recursos.



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

No hay comentarios:

Publicar un comentario en la entrada