Diciembre de 2017
Volumen 32, número 12
C++: soporte técnico de Visual C++ para la protección del búfer basado en la pila
Por Hadi Brais | Diciembre de 2017
Cuando el software realiza algo que no se supone que deba hacer de acuerdo con su especificación funcional, se considera que tiene defectos o errores. Las reglas de esta especificación que determinan cuándo deben permitirse accesos y modificaciones a datos y otros recursos constituyen colectivamente una directiva de seguridad. La directiva de seguridad define fundamentalmente lo que significa para el software ser seguro, y cuándo un defecto concreto debe considerarse como una tara de seguridad en lugar de un error más.
Dadas las distintas amenazas procedentes de todo el mundo, la seguridad hoy es más importante que nunca y, por tanto, debe formar parte del ciclo de vida de desarrollo del software (SDL). Se incluyen las opciones como, por ejemplo, la ubicación donde se almacenarán los datos, las API de runtime de C/C++ que se usarán y las herramientas que pueden contribuir a mejorar la seguridad del software. El seguimiento de C++ Core Guidelines (bit.ly/1LoeSRB) ayuda considerablemente a escribir código correcto y que se pueda mantener. Además, el compilador de Visual C++ ofrece numerosas características de seguridad, a las que se puede acceder fácilmente por medio de modificadores de compilador. Se pueden clasificar como análisis de seguridad estáticos o dinámicos. Algunos ejemplos de comprobaciones de seguridad estática son el uso de los modificadores /Wall y /analyze y los comprobadores de C++ Core Guidelines. Estas comprobaciones se realizan de forma estática y no afectan al código generado, aunque aumentan el tiempo de compilación. Por el contrario, las comprobaciones dinámicas se insertan en los binarios ejecutables que emiten el compilador o el enlazador. En este artículo, explicaré una opción de análisis de seguridad dinámica, concretamente /GS, que ofrece protección frente a desbordamientos del búfer basado en la pila. Explicaré cómo se transforma el código cuando ese modificador se activa y cuándo puede o no proteger el código. Usaré Visual Studio Community 2017.
Es posible que se pregunte por qué no activamos simplemente todos estos modificadores de compilador y listo. En general, debería emplear todos los modificadores recomendados, independientemente de si comprende su funcionamiento. No obstante, conocer los detalles de cómo funciona una técnica concreta permite determinar el impacto que puede tener en su código y usarla mejor. Tenga en cuenta, por ejemplo, los desbordamientos del búfer. El compilador ofrece un modificador para lidiar con estos defectos, pero usa un mecanismo de detección que fuerza el bloqueo del programa cuando se detecta un desbordamiento del búfer. ¿Eso mejora la seguridad? depende. En primer lugar, mientras que todos los desbordamientos del búfer son perjudiciales, no todos son vulnerabilidades de seguridad y, por tanto, no significa necesariamente que tuvo lugar una vulnerabilidad. Y aunque así fuera, puede que el daño se deba a la hora en que se desencadenó el mecanismo de detección. Además, según cómo esté diseñada la aplicación, bloquear de manera abrupta el programa podría no resultar adecuado, ya que podría ser en sí misma una vulnerabilidad por denegación de servicio (DoS) o conducir a una situación potencialmente peor que implicara pérdidas o daños de datos. Como explicaré en este artículo, lo único razonable que se puede hacer es que la aplicación resista a estos bloqueos, en lugar de deshabilitar o cambiar el mecanismo de protección.
He escrito varios artículos sobre optimizaciones de los compiladores para MDSN Magazine (el primero lo encontrará en msdn.com/magazine/dn904673). El objetivo principal era mejorar el tiempo de ejecución. La seguridad también se puede ver como un objetivo de las transformaciones de compilador. Es decir, en lugar de optimizar el tiempo de ejecución, la seguridad se optimizaría mediante la reducción del número de errores de seguridad. Esta perspectiva resulta útil porque sugiere que, al especificar varios modificadores de compilador para mejorar el tiempo de ejecución y la seguridad, el compilador podría tener varios objetivos en conflicto posibles. En este caso, debe equilibrar o priorizar de algún modo estos objetivos. Explicaré el impacto que /GS tiene en algunos aspectos del código, especialmente en la velocidad, el consumo de memoria y el tamaño de los archivos ejecutables. Ese es otro motivo para comprender qué realizan los modificadores en su código.
En la siguiente sección, proporcionaré una introducción a los ataques de flujo de control; me centraré especialmente en los desbordamientos del búfer de pila. Explicaré cómo se producen y cómo puede aprovecharlos un atacante. A continuación, observaré atentamente cómo influye /GS en su código y en qué medida puede mitigar estas vulneraciones. Por último, demostraré cómo usar la herramienta de análisis de binarios estático BinSkim para realizar varias comprobaciones críticas en un binario ejecutable determinado sin necesidad del código fuente.
Ataques de flujo de control
Un búfer es un bloque de memoria que se usa para almacenar datos temporalmente para su procesamiento. Los búferes se pueden asignar desde el montón en tiempo de ejecución, la pila de subprocesos, directamente con la API VirtualAlloc de Windows o como una variable global. Los búferes se pueden asignar desde el montón en tiempo de ejecución, ya sea con las funciones de asignación de memoria en C (como malloc) o mediante el nuevo operador de C++. Los búferes se pueden asignar desde la pila mediante una variable de matriz automática o por medio de la función _alloca. El tamaño mínimo de un búfer puede ser de cero bytes, mientras que el tamaño máximo depende del tamaño del bloque libre más grande.
Dos características especiales de los lenguajes de programación C y C++ que los distinguen realmente de otros lenguajes, como C#, son:
- Se puede realizar aritmética arbitraria en los punteros.
- Se puede desreferenciar correctamente cualquier puntero en cualquier momento, siempre que apunte a la memoria asignada (desde el punto de vista del sistema operativo), aunque el comportamiento de la aplicación podría no estar definido si no apunta a la memoria de su propiedad.
Estas características hacen que los lenguajes sean muy eficaces, pero constituyen al mismo tiempo una gran amenaza. En concreto, un puntero destinado a usarse para acceder al contenido de un búfer o iterarlo podría modificarse de manera errónea o maliciosa y apuntar fuera de los límites del búfer con fines de lectura o escritura de la ubicación de memoria adyacente u otras ubicaciones de memoria. Escribir después de la dirección mayor de un búfer es lo que se conoce como desbordamiento del búfer. Escribir antes de la dirección menor de un búfer (que es la dirección del búfer) es lo denominamos subdesbordamiento del búfer.
Una vulnerabilidad de desbordamiento del búfer basado en la pila se detectó recientemente en un componente de software extremadamente popular (que no mencionaré). Se produjo por el uso no seguro de la función sprintf, como se muestra en el código siguiente:
sprintf(buffer, "A long format string %d, %d", var1, var2);
El búfer está asignado desde la pila de subprocesos y tiene un tamaño fijo. No obstante, el tamaño de la cadena que se va a escribir en el búfer depende del número de caracteres necesario para representar dos enteros especificados. El tamaño del búfer no es suficiente para contener la cadena más grande posible, lo que provoca un desbordamiento del búfer cuando se especifican enteros grandes. Cuando se produce un desbordamiento, se dañan las ubicaciones de memoria adyacentes de la parte superior de la pila.
Para demostrar por qué es peligroso, tenga en cuenta dónde se ubicaría un búfer asignado desde la pila en el marco de pila de la función declarativa, de acuerdo con las convenciones de nomenclatura x86 estándar y teniendo en cuenta las optimizaciones de los compiladores, como se muestra en la Figura 1.
Figura 1 Marco de pila x86 típico
En primer lugar, el autor de la llamada inserta todos los argumentos que no han pasado a través de registros en la pila en un orden determinado. A continuación, la instrucción x86 CALL inserta el remite en la pila y pasa a la primera instrucción del destinatario. Si no se lleva a cabo ninguna optimización de omisión de puntero de marco (FPO), el destinatario inserta el puntero de marco actual en la pila. Si el destinatario usa cualquier construcción de control de excepciones que no se haya optimizado fuera, se podría colocar un marco de control de excepciones en la pila. El marco contiene los punteros y otra información sobre los controladores de excepciones definidos en el destinatario. Las variables locales no estáticas que no se han optimizado fuera y no pueden mantenerse en registros, o que se desbordan de los registros se asignan desde la pila en un orden determinado. A continuación, todos los registros guardados y usados por el destinatario se guardan en la pila. Por último, los búferes cuyo tamaño se ha establecido de forma dinámica mediante _alloca se colocan en la parte inferior del marco de pila.
Cualquier elemento de datos de la pila puede tener requisitos de alineación determinados, por lo que se pueden asignar bloques de relleno según sea necesario. El fragmento de código del destinatario que configura el marco de pila (excepto los argumentos) se denomina "prólogo". Si la función está a punto de volver al autor de la llamada, un fragmento de código denominado epílogo es responsable de desasignar el marco de pila e incluir el remite.
La principal diferencia entre las convenciones de llamada de ARM y x86/x64 es que el remite y el puntero de marco se mantienen en registros dedicados en ARM, en lugar de en la pila. Sin embargo, los accesos fuera de límites del búfer de pila constituyen un grave problema de seguridad en ARM porque otros valores presentes en la pila podrían ser punteros.
Un desbordamiento del búfer de pila (escritura por encima del límite superior de un búfer) podría sobrescribir cualquier puntero de datos o código almacenado sobre el búfer. Un subdesbordamiento del búfer de pila (escritura por debajo del límite inferior de un búfer) podría sobrescribir los valores de los registros guardados por el destinatario, que también podrían ser punteros de datos o código. Una escritura fuera de los límites arbitraria provocará que la aplicación se bloquee o se comporte de manera indefinida. No obstante, un ataque diseñado de manera malintencionada permite al atacante tomar el control de la ejecución de la aplicación o de todo el sistema. Esto se puede lograr sobrescribiendo un puntero de código (por ejemplo, el remite), de modo que apunte a un fragmento de código que ejecute la intención del atacante.
GuardStack (GS)
Para mitigar los accesos fuera de los límites basados en la pila, podría agregar manualmente las comprobaciones de límites necesarias (agregar instrucciones if para comprobar si un puntero determinado se encuentra dentro de los límites), o bien usar una API que realice estas comprobaciones (por ejemplo, snprintf). No obstante, la vulnerabilidad podría persistir por distintos motivos, como conversiones de tipo o aritmética de enteros incorrectas usadas para determinar los límites de búferes o realizar comprobaciones de límites. Por tanto, se necesita un mecanismo de mitigación dinámico para evitar o reducir la posibilidad de vulneraciones de seguridad.
Las técnicas de mitigación generales incluyen la aleatorización del espacio de direcciones y el uso de pilas no ejecutables. Las técnicas de mitigación dedicadas se pueden clasificar según si el objetivo es evitar accesos fuera de los límites detectándolos antes de que ocurran, o bien detectar accesos fuera de los límites en algún punto después de que sucedan. Ambas opciones son posibles, pero la prevención agrega una sobrecarga de rendimiento considerable.
El compilador de Visual C++ ofrece dos mecanismos de detección que son en cierto modo similares, pero tienen distintos propósitos y costos de rendimiento diferentes. El primer mecanismo forma parte de las comprobaciones de errores en tiempo de ejecución y se puede habilitar mediante el modificador /RTCs. El segundo es GuardStack (denominado "comprobación de seguridad de búfer" en la documentación "comprobación de seguridad" en Visual Studio) y se puede habilitar con el modificador /GS.
Con /RTCs, el compilador asigna bloques de memoria pequeños adicionales intercaladamente, de modo que cada variable local de la pila se emparede entre dos de estos bloques. Cada uno de estos bloques adicionales se rellena con un valor especial (actualmente, 0xCC). Esto lo controla el prólogo del destinatario. En el epílogo, se llama a una función de tiempo de ejecución para comprobar si alguno de estos bloques estaba dañado y notificar un posible desbordamiento o subdesbordamiento del búfer. Este mecanismo de detección agrega alguna sobrecarga en términos de rendimiento y espacio de la pila, pero está diseñado para usar en la depuración y para garantizar la corrección del programa, no solo como mitigación.
GuardStack, por otro lado, se diseñó para ofrecer una menor sobrecarga y como mitigación capaz de funcionar en un entorno de producción potencialmente malintencionado. Por tanto, /RTCs debería usarse para las compilaciones de depuración y GuardStack, para ambos tipos de compilaciones. Además, el compilador no le permite usar /RTCs con las optimizaciones de los compiladores, mientras que GuardStack es compatible y no interfiere en dichas optimizaciones. De manera predeterminada, ambos están habilitados en la configuración de depuración, pero solo GuardStack está habilitado en la configuración de versión de un proyecto de Visual C++. En este artículo, solo explicaré GuardStack detenidamente.
Si GuardStack está habilitado, una pila de llamadas x86 típica tendría el aspecto que se muestra en la Figura 2.
Figura 2 Marco de pila x86 típico protegido con GuardStack (/GS)
Existen tres diferencias en comparación con el diseño de pila que se muestra en la Figura 1. Primero, un valor especial, denominado "cookie" o "valor controlado", se asigna justo encima de las variables locales. En segundo lugar, las variables locales con más probabilidad de presentar desbordamientos se asignan encima de todas las demás variables locales. En tercer lugar, algunos de los argumentos que son especialmente sensibles a los desbordamientos del búfer se copian en un área bajo las variables locales. Obviamente, para que estos cambios se produzcan, se usan un prólogo y un epílogo diferentes, como explicaré a continuación.
El prólogo de una función protegida incluiría básicamente las siguientes instrucciones adicionales sobre x64:
sub rsp,8h
mov rax,qword ptr [__security_cookie]
xor rax,rbp
mov qword ptr [rbp],rax
La cantidad adicional de 8 bytes se asigna desde la pila y se inicializa en una copia del valor de la variable global __security_cookie en la que se ha ejecutado XOR con el valor incluido en el registro de RBP. Si /GS está especificado, el compilador vincula automáticamente el archivo de objeto compilado a partir del archivo de origen gs_cookie.c. Este archivo define __security_cookie como una variable global de 64 bits o 32 bits de tipo uintptr_t en x64 y en x86, respectivamente. Por tanto, cada imagen portable ejecutable (PE) compilada con /GS incluye una sola definición de esa variable que usan los prólogos y los epílogos de las funciones de esa imagen. En x86, el código es el mismo, excepto en que se usan registros y cookies de 32 bits.
La idea básica que respalda el uso de una cookie de seguridad es detectar, justo antes de que se devuelva la función, si el valor de la cookie ha cambiado respecto del de la cookie de referencia (la variable global). Indica un posible desbordamiento del búfer causado por un intento de vulneración de seguridad o un error inocente. Es esencial que la cookie tenga una entropía muy alta para que resulte extremadamente difícil de adivinar para un atacante. Si un atacante puede determinar la cookie usada en un marco de pila concreto, GuardStack generará un error. Profundizaré en lo que GuardStack puede o no puede hacer más adelante en esta sección.
La cookie de referencia recibe un valor constante arbitrario cuando el compilador emite la imagen. Por tanto, se debe inicializar con cuidado, básicamente antes de ejecutar ningún código. Las versiones recientes de Windows detectan GuardStack e inicializarán la cookie en un valor de entropía alta en tiempo de carga. Si /GS está habilitado, lo primero que el punto de entrada de un archivo EXE o DLL hace es inicializar la cookie mediante una llamada a la función __security_init_cookie definida en gs_support.c y declarada en process.h. Esta función inicializa la cookie de la imagen si el cargador de Windows no la inicializó correctamente.
Observe que la ejecución de XOR con RBP, que simplemente filtra la cookie de referencia en cualquier punto durante la ejecución (por ejemplo, mediante una lectura fuera de límites), es suficiente para la subversión de GuardStack. La ejecución de XOR con RBP le permite generar de manera eficiente distintas cookies y el atacante tendría que conocer la cookie de referencia y la RBP para averiguar la cookie de un marco de pila. RBP por sí solo no tiene garantizada una alta entropía, ya que su valor depende del modo en que el compilador optimizó el código, el espacio de la pila consumido hasta el momento y la aleatorización que se llevó a cabo mediante la selección aleatoria del diseño del espacio de direcciones (ASLR), si está habilitada.
El epílogo de una función protegida incluiría básicamente las siguientes instrucciones adicionales sobre x64:
mov rcx,qword ptr [rbp]
xor rcx,rbp
call __security_check_cookie
add esp,8h
En primer lugar, se ejecutó XOR en la cookie de la pila para producir un valor que, supuestamente, coincide con la cookie de referencia. El compilador emite instrucciones para garantizar que el valor de RBP usado en el prólogo y el epílogo es el mismo (salvo que se haya dañado de algún modo).
La función __security_check_cookie, declarada en vcruntime.h, está vinculada mediante el compilador y su finalidad es validar la cookie que se encuentra en la pila. Esto se lleva a cabo principalmente comparando la cookie con la cookie de referencia. Si la comprobación genera un error, el código salta a la función __report_gsfailure, que está definida en gs_report.c. En Windows 8 y versiones posteriores, la función termina el proceso con una llamada a __fastfail. En otros sistemas, la función termina el proceso mediante una llamada a UnhandledExceptionFilter después de quitar cualquier controlador potencial. De cualquier modo, Informe de errores de Windows (WER) registra el error, que contiene información sobre el marco de pila donde se dañó el marco de pila.
Cuando /GS se introdujo por primera vez en Visual C++ 2002, se podía invalidar el comportamiento de una comprobación de cookie de la pila con errores especificando una función de devolución de llamada. No obstante, dado que la pila tiene un estado no definido y que algún código ya se ejecutó antes de la detección del desbordamiento, no existe prácticamente nada que se pueda hacer de manera confiable en este momento. Por tanto, en las versiones posteriores a partir de Visual C++ 2005 se eliminó esta característica.
Sobrecarga de GuardStack
Para minimizar la sobrecarga, solo se protegen las funciones que el compilador considera vulnerables. Las diferentes versiones del compilador pueden usar distintos algoritmos sin documentar para determinar si una función es vulnerable, aunque, en general, si una función define una matriz o una gran estructura de datos y obtiene punteros a estos objetos, es probable que se considere vulnerable. Para especificar que una función concreta no esté protegida, puede aplicar __declspec(safebuffers) a su declaración. No obstante, esta palabra clave se ignora cuando se aplica a una función insertada en una función protegida o si una función protegida está insertada en esta. También puede forzar que el compilador proteja una o varias funciones mediante la pragma strict_gs_check. Las comprobaciones de ciclo de vida de desarrollo de seguridad (SDL), habilitadas mediante /sdl, especifican el método GuardStack estricto en todos los archivos de origen y otras comprobaciones de seguridad dinámicas.
GuardStack copia los parámetros vulnerables en una ubicación más segura bajo las variables locales, de modo que, en caso de producirse un desbordamiento, sería más difícil dañar esos parámetros. Un parámetro que sea un puntero o una referencia de C++ podría clasificarse como un parámetro vulnerable. Consulte la documentación sobre /GS para obtener más información.
Realicé varios experimentos con aplicaciones de producción de C/C++ para determinar la sobrecarga relacionada con el rendimiento y el tamaño de la imagen. Apliqué strict_gs_check en todos los archivos de origen, de modo que los resultados son independientes de lo que el compilador considera funciones vulnerables (me abstuve de usar /sdl porque habilita otras comprobaciones de seguridad que tienen sus propias sobrecargas). La mayor sobrecarga de rendimiento que obtuve fue de 1,4 por ciento y la mayor sobrecarga de tamaño de la imagen, de 0,4 por ciento. El peor de los casos sería un programa que pasara la mayor parte del tiempo llamando a funciones protegidas que llevan a cabo muy poco trabajo. Los programas reales bien diseñados no muestran este comportamiento. Tenga en cuenta también que GuardStack causa una sobrecarga de espacio de la pila potencial significativa.
Acerca de la eficacia de GuardStack
GuardStack está diseñado para mitigar solo un tipo de vulnerabilidad específico, concretamente el desbordamiento del búfer de pila. Y aún más importante, el uso de GuardStack por sí solo contra esta vulnerabilidad podría no proporcionar un alto grado de protección, ya que un atacante tiene maneras de sortearlo:
- La detección de una cookie dañada solo tiene lugar cuando la función se devuelve. Se podría ejecutar una gran cantidad de código entre la hora en que la cookie se daña y la hora en que se detecta el daño. Ese código podría usar otros valores de la pila, encima o debajo de la cookie, que se han sobrescrito. Ello ofrece a un atacante una oportunidad de tomar el control (parcial) de la ejecución de la aplicación. En ese caso, la detección podría no realizarse en lo más mínimo.
- Un desbordamiento del búfer puede producirse aún sin sobrescribir la cookie. El caso más peligroso sería el desbordamiento de un búfer asignado mediante _alloca. Incluso los argumentos protegidos y los registros guardados por el destinatario se pueden sobrescribir en este caso.
- Algunas cookies se podrían filtrar al usar lecturas de memoria fuera de los límites. Dado que imágenes diferentes usan distintas cookies de referencia y que en las cookies se ejecutó XOR con RBP, puede ser más difícil para un atacante usar cookies filtradas. No obstante, el Subsistema Windows para Linux (WSL) podría haber introducido un nuevo método para filtrar cookies. WSL proporciona una emulación de llamada del sistema Linux de bifurcación, que crea un nuevo proceso del sistema que duplica el proceso principal. Si la aplicación que sufre el ataque bifurca un nuevo proceso para controlar las solicitudes de cliente entrantes, un cliente malintencionado puede emitir un número bastante reducido de solicitudes para determinar los valores de las cookies de seguridad.
- Se han propuesto varias técnicas para adivinar la cookie de referencia de una imagen en determinadas situaciones. No conozco ningún ataque real de éxito en el que se adivinara la cookie de referencia, pero la probabilidad de éxito es lo suficientemente baja como para descartarla. La ejecución de XOR con RBP agrega otra capa de defensa muy importante contra estos ataques.
- GuardStack mitiga las vulnerabilidades gracias a la introducción de distintas vulnerabilidades potenciales (en concreto, el ataque por denegación de servicio y la pérdida de datos). Cuando se detecta que una cookie está dañada, la aplicación se cierra de repente. Para una aplicación de servidor, el atacante puede hacer que el servidor se bloquee y con ello causar posibles pérdidas o daños de datos valiosos.
Por lo tanto, es importante esmerarse primero en escribir código seguro y correcto con la ayuda de herramientas de análisis estático. A continuación, siguiendo la estrategia de defensa en profundidad, emplee GuardStack y otras mitigaciones dinámicas que ofrece Visual C++ (muchas de las cuales están habilitadas de manera predeterminada en la compilación de versión) en el código que envía.
/GS con /ENTRY
La función de punto de entrada predeterminada (*CRTStartup) que especifica el compilador cuando realiza la compilación para producir un archivo EXE o DLL realiza las siguientes cuatro acciones en este orden: inicializa la cookie de seguridad de referencia, inicializa el runtime de C/C++, llama a la función principal de la aplicación y cierra la aplicación. Puede usar el modificador del enlazador /ENTRY para especificar un punto de entrada predeterminado. Sin embargo, combinar un punto de entrada personalizado con los efectos de /GS puede dar lugar a escenarios interesantes.
El punto de entrada personalizado y todas las funciones a las que llame son candidatos para la protección. Si el cargador de Windows inicializó correctamente la cookie, cualquier función protegida usará una copia de una cookie de referencia que sea igual en sus prólogos y epílogos. Por tanto, no habrá ningún problema.
Si Windows no inicializó correctamente la cookie y lo primero que hace el punto de entrada personalizado es llamar a __security_init_cookie, todas las funciones protegidas usarán la cookie de referencia correcta, excepto para el punto de entrada. Recuerde que se realiza una copia de la cookie de referencia en el epílogo. Por lo tanto, si el punto de entrada se devuelve con normalidad, la cookie se comprobará en su epílogo y la comprobación generará un error, lo que provocará un falso positivo. Para evitar este problema, debería llamar a una función para que el programa (como exit) se cierre en lugar de devolverse con normalidad.
Si Windows no inicializó correctamente la cookie y el punto de entrada no llamó a __security_init_cookie, todas las funciones protegidas usarán la cookie de referencia predeterminada. Afortunadamente, dado que se ejecutó XOR con RBP en esta cookie, las entropías de las cookies usadas no serán cero. Por tanto, seguirá obteniendo algo de protección, especialmente con ASLR. No obstante, se recomienda inicializar correctamente la cookie de referencia mediante una llamada a __security_init_cookie.
Uso de BinSkim para la comprobación de GuardStack
BinSkim es una herramienta de análisis binario estático ligera que comprueba la corrección en el uso de algunas de las características de seguridad que se usan en un binario PE determinado. Una característica concreta que BinSkim admite es GuardStack. BinSkim es una herramienta de código abierto (github.com/Microsoft/binskim) bajo licencia de MIT y escrita completamente en C#. Admite binarios x86, x64 y ARM de Windows compilados con versiones recientes de Visual C++ (2013 y posteriores). Puede usarla como una herramienta independiente o, aún más interesante, incluirla (parcialmente) en su código. Por ejemplo, si tiene una aplicación que admite complementos de PE, puede usar BinSkim para comprobar si un complemento usa las características de seguridad recomendadas y, de lo contrario, rechazar cargarlo. En esta sección, explicaré cómo usar BinSkim como una herramienta independiente.
En lo que a GuardStack se refiere, la herramienta comprueba si el binario especificado respeta las cuatro reglas siguientes:
- EnableStackProtection: comprueba la marca correspondiente almacenada en el archivo PDB asociado. Si no se encuentra la marca, la regla genera un error. De lo contrario, se aplica correctamente.
- InitializeStackProtection: itera la lista de funciones globales según se define en el archivo PDB asociado para buscar las funciones __security_init_cookie y __security_check_cookie. Si no se encuentran, la herramienta considera que /GS no estaba habilitado. En este caso, EnableStackProtection debería generar un error. Si __security_init_cookie no estaba definido, la regla genera un error. De lo contrario, se aplica correctamente.
- DoNotModifyStackProtectionCookie: busca la ubicación de la cookie de referencia mediante los datos de configuración de carga de la imagen. Si no se encuentra la ubicación, la regla genera un error. Si los datos de configuración de carga indican que hay una cookie definida, pero su desplazamiento no es válido, la regla genera un error. De lo contrario, la regla se aplica correctamente.
- DoNotDisableStackProtectionForFunctions: usa el archivo PDB asociado para determinar si existe alguna función que tenga el atributo __declspec(safebuffers) aplicado. Si la regla genera un error, significa que no se encontró ninguna. De lo contrario, se aplica correctamente. SDL de Microsoft no permite usar __declspec(safebuffers).
Para usar BinSkim, descargue primero el código fuente del repositorio de GitHub y compílelo. Para ejecutar BinSkim, use el siguiente comando en su shell favorito:
binskim.exe analyze target.exe --output results.sarif
Para analizar más de una imagen, puede usar el comando siguiente:
binskim.exe analyze myapp\*.dll --recurse --output results.sarif --verbose
Tenga en cuenta que puede usar comodines en las rutas de archivo. El modificador --recurse especifica que BinSkim debe analizar también las imágenes de los subdirectorios. El modificador --verbose indica a BinSkim que incluya en el archivo de resultados las reglas aplicadas correctamente, no solo las que han generado errores.
El archivo de resultados tiene el formato de intercambio de resultados de análisis estático (SARIF). Si lo abre en un editor de texto, encontrará entradas como la que se muestra en la Figura 3.
Figura 3 Archivo de resultados de análisis de BinSkim
{
"ruleId": "BA2014",
"level": "pass",
"formattedRuleMessage": {
"formatId": "Pass ",
"arguments": [
"myapp.exe",
]
},
"locations": [
{
"analysisTarget": {
"uri": "D:/src/build/myapp.exe"
}
}
]
}
Cada regla tiene un id. de regla. El id. de regla BA2014 corresponde a la regla DoNotDisableStackProtectionForFunctions. El SDK de SARIF de Microsoft (github.com/Microsoft/sarif-sdk) incluye el código fuente de una extensión de Visual Studio que permite ver archivos SARIF en Visual Studio.
Resumen
La técnica de mitigación dinámica de GuardStack ofrece una mitigación basada en la detección de extrema importancia contra las vulnerabilidades de desbordamiento del búfer de pila. Está habilitada de manera predeterminada en ambas compilaciones de depuración y de versión en Visual Studio. Está diseñada para tener una sobrecarga insignificante para la mayoría de los programas, de modo que se puede usar ampliamente. No obstante, no proporciona ninguna solución definitiva para este tipo de vulnerabilidades. Los desbordamientos del búfer son comunes en los búferes asignados desde la pila, pero también se pueden producir en cualquier región de memoria asignada. De manera más visible, los desbordamientos del búfer basados en el montón presentan el mismo peligro. Por estos motivos, es muy importante usar otras técnicas de mitigación que ofrecen Visual C++ y Windows, como las de protección de flujo de control (CFG), selección aleatoria del diseño del espacio de direcciones (ASLR), Prevención de ejecución de datos (DEP), control de excepciones estructurado seguro (SAFESEH) y protección contra sobrescrituras de control de excepciones estructurado (SEHOP). Todas estas técnicas funcionan de manera sinérgica para reforzar su aplicación. Para obtener más información sobre estas y otras técnicas, consulte bit.ly/2iLG9rq.
Hadi Brais es profesor doctorado en el Instituto Indio de Tecnología de Delhi, donde investiga sobre las optimizaciones de los compiladores, la arquitectura informática, y las herramientas y tecnologías relacionadas. Publica en el blog en hadibrais.wordpress.com y puede ponerse en contacto con él en hadi.b@live.com.
Gracias a los siguientes expertos técnicos por revisar este artículo: Shayne Hiet-Block (Microsoft), Mateusz Jurczyk (Google), Preeti Ranjan Panda (IITD) y Andrew Pardoe (Microsoft)