La protección contra desbordamiento de búfer es una de las diversas técnicas que se utilizan durante el desarrollo de software para mejorar la seguridad de los programas ejecutables detectando desbordamientos de búfer en las variables asignadas a la pila y evitando que provoquen un mal funcionamiento del programa o se conviertan en vulnerabilidades de seguridad graves . Un desbordamiento de búfer de pila se produce cuando un programa escribe en una dirección de memoria de la pila de llamadas del programa fuera de la estructura de datos prevista, que suele ser un búfer de longitud fija. Los errores de desbordamiento de búfer de pila se producen cuando un programa escribe más datos en un búfer ubicado en la pila de los que realmente están asignados para ese búfer. Esto casi siempre da como resultado la corrupción de los datos adyacentes en la pila, lo que podría provocar fallos del programa, un funcionamiento incorrecto o problemas de seguridad.
Por lo general, la protección contra desbordamiento de búfer modifica la organización de los datos asignados a la pila para que incluya un valor canario que, cuando se destruye por un desbordamiento de búfer de pila, muestra que se ha desbordado un búfer que lo precede en la memoria. Al verificar el valor canario, se puede terminar la ejecución del programa afectado, lo que evita que se comporte mal o que permita que un atacante tome el control sobre él. Otras técnicas de protección contra desbordamiento de búfer incluyen la comprobación de límites , que verifica los accesos a cada bloque de memoria asignado para que no puedan ir más allá del espacio realmente asignado, y el etiquetado , que garantiza que la memoria asignada para almacenar datos no pueda contener código ejecutable.
Es más probable que el llenado excesivo de un búfer asignado en la pila influya en la ejecución del programa que el llenado excesivo de un búfer en el montón, porque la pila contiene las direcciones de retorno de todas las llamadas de función activas. Sin embargo, también existen protecciones específicas de la implementación similares contra los desbordamientos basados en el montón.
Existen varias implementaciones de protección contra desbordamiento de búfer, incluidas aquellas para GNU Compiler Collection , LLVM , Microsoft Visual Studio y otros compiladores.
Un desbordamiento del búfer de pila ocurre cuando un programa escribe en una dirección de memoria en la pila de llamadas del programa fuera de la estructura de datos prevista, que suele ser un búfer de longitud fija. Los errores de desbordamiento del búfer de pila se producen cuando un programa escribe más datos en un búfer ubicado en la pila de los que realmente están asignados para ese búfer. Esto casi siempre da como resultado la corrupción de los datos adyacentes en la pila y, en los casos en que el desbordamiento se desencadenó por error, a menudo provocará que el programa se bloquee o funcione incorrectamente. El desbordamiento del búfer de pila es un tipo de mal funcionamiento de programación más general conocido como desbordamiento de búfer (o desbordamiento de búfer). Es más probable que llenar en exceso un búfer en la pila descarrile la ejecución del programa que llenar en exceso un búfer en el montón porque la pila contiene las direcciones de retorno de todas las llamadas de función activas. [1]
El desbordamiento del búfer de pila puede ser provocado deliberadamente como parte de un ataque conocido como stack smashing . Si el programa afectado se ejecuta con privilegios especiales o si acepta datos de hosts de red que no son de confianza (por ejemplo, un servidor web público ), entonces el error es una vulnerabilidad de seguridad potencial que permite a un atacante inyectar código ejecutable en el programa en ejecución y tomar el control del proceso. Este es uno de los métodos más antiguos y confiables para que los atacantes obtengan acceso no autorizado a una computadora. [2]
Por lo general, la protección contra desbordamiento de búfer modifica la organización de los datos en el marco de pila de una llamada de función para incluir un valor "canario" que, cuando se destruye, muestra que se ha desbordado un búfer que lo precede en la memoria. Esto proporciona el beneficio de prevenir toda una clase de ataques. Según algunos investigadores, [3] el impacto en el rendimiento de estas técnicas es insignificante.
La protección contra la destrucción de la pila no puede proteger contra ciertas formas de ataque. Por ejemplo, no puede proteger contra desbordamientos de búfer en el montón. No existe una forma sensata de alterar la disposición de los datos dentro de una estructura ; se espera que las estructuras sean las mismas entre módulos, especialmente con bibliotecas compartidas. Es imposible proteger con canarios cualquier dato en una estructura después de un búfer; por lo tanto, los programadores deben ser muy cuidadosos con la forma en que organizan sus variables y utilizan sus estructuras.
Los canarios , las palabras canarias o las cookies de pila son valores conocidos que se colocan entre un búfer y los datos de control en la pila para supervisar los desbordamientos del búfer. Cuando el búfer se desborda, los primeros datos que se corrompen normalmente serán los canarios y, por tanto, una verificación fallida de los datos canarios alertará de un desbordamiento, que puede gestionarse, por ejemplo, invalidando los datos dañados. Un valor canario no debe confundirse con un valor centinela .
La terminología es una referencia a la práctica histórica de utilizar canarios en las minas de carbón , ya que se veían afectados por los gases tóxicos antes que los mineros, lo que proporcionaba un sistema de alerta biológica. Los canarios también se conocen como galletas de pila , lo que pretende evocar la imagen de una "galleta rota" cuando el valor está alterado.
Hay tres tipos de canarios en uso: terminator , random y random XOR . Las versiones actuales de StackGuard admiten los tres, mientras que ProPolice admite canarios terminator y random .
Los canarios de terminación utilizan la observación de que la mayoría de los ataques de desbordamiento de búfer se basan en ciertas operaciones de cadena que terminan en terminadores de cadena. La reacción a esta observación es que los canarios se construyen con terminadores nulos , CR , LF y FF . Como resultado, el atacante debe escribir un carácter nulo antes de escribir la dirección de retorno para evitar alterar el canario. Esto evita ataques que utilizan strcpy()
y otros métodos que regresan al copiar un carácter nulo, mientras que el resultado indeseable es que el canario es conocido. Incluso con la protección, un atacante podría sobrescribir potencialmente el canario con su valor conocido y la información de control con valores no coincidentes, pasando así el código de verificación del canario, que se ejecuta poco antes de la instrucción de retorno de llamada del procesador específico.
Los canarios aleatorios se generan aleatoriamente, generalmente a partir de un demonio que recolecta entropía , para evitar que un atacante conozca su valor. Por lo general, no es lógicamente posible ni plausible leer el canario para explotarlo; el canario es un valor seguro que solo conocen quienes necesitan conocerlo (en este caso, el código de protección contra desbordamiento de búfer).
Normalmente, se genera un canario aleatorio al inicializar el programa y se almacena en una variable global. Esta variable suele estar rellena con páginas no asignadas, de modo que intentar leerla mediante cualquier tipo de trucos que aprovechen errores para leer la RAM puede provocar un error de segmentación y terminar el programa. Aún puede ser posible leer el canario si el atacante sabe dónde está o puede hacer que el programa lo lea desde la pila.
Los canarios XOR aleatorios son canarios aleatorios que se mezclan mediante XOR utilizando todos o parte de los datos de control. De esta manera, una vez que se modifica el canario o los datos de control, el valor del canario es incorrecto.
Los canarios XOR aleatorios tienen las mismas vulnerabilidades que los canarios aleatorios, excepto que el método de "leer desde la pila" para obtener el canario es un poco más complicado. El atacante debe obtener el canario, el algoritmo y los datos de control para poder volver a generar el canario original necesario para burlar la protección.
Además, los canarios XOR aleatorios pueden brindar protección contra un cierto tipo de ataque que implica desbordar un búfer en una estructura en un puntero para cambiar el puntero para que apunte a un fragmento de datos de control. Debido a la codificación XOR, el canario será incorrecto si se modifican los datos de control o el valor de retorno. Debido al puntero, los datos de control o el valor de retorno se pueden modificar sin desbordar el canario.
Aunque estos canarios protegen los datos de control de ser alterados por punteros modificados, no protegen ningún otro dato ni los punteros mismos. Los punteros de función en particular son un problema aquí, ya que pueden desbordarse y ejecutar código shell cuando se los llama.
La comprobación de límites es una técnica basada en compiladores que agrega información de límites en tiempo de ejecución para cada bloque de memoria asignado y verifica todos los punteros con respecto a los de tiempo de ejecución. Para C y C++, la comprobación de límites se puede realizar en el momento del cálculo del puntero [4] o en el momento de la desreferencia. [5] [6] [7]
Las implementaciones de este enfoque utilizan un repositorio central, que describe cada bloque de memoria asignado, [4] [5] [6] o punteros fat , [7] que contienen tanto el puntero como datos adicionales, que describen la región a la que apuntan.
El etiquetado [8] es una técnica basada en compilador o en hardware (que requiere una arquitectura etiquetada ) para etiquetar el tipo de un fragmento de datos en la memoria, que se utiliza principalmente para la comprobación de tipos. Al marcar ciertas áreas de la memoria como no ejecutables, se evita de forma eficaz que la memoria asignada para almacenar datos contenga código ejecutable. Además, ciertas áreas de la memoria se pueden marcar como no asignadas, lo que evita desbordamientos del búfer.
Históricamente, el etiquetado se ha utilizado para implementar lenguajes de programación de alto nivel; [9] con el soporte adecuado del sistema operativo , el etiquetado también se puede utilizar para detectar desbordamientos de búfer. [10] Un ejemplo es la característica de hardware de bit NX , compatible con los procesadores Intel , AMD y ARM .
La protección contra la destrucción de pilas fue implementada por primera vez por StackGuard en 1997 y publicada en el Simposio de Seguridad USENIX de 1998. [11] StackGuard fue presentado como un conjunto de parches para el backend x86 de Intel de GCC 2.7 . StackGuard se mantuvo para la distribución Linux Immunix desde 1998 hasta 2003 y se amplió con implementaciones para terminadores, canarios aleatorios y XOR aleatorios. Se sugirió la inclusión de StackGuard en GCC 3.x en las Actas de la Cumbre GCC 2003, [12] pero esto nunca se logró.
Entre 2001 y 2005, IBM desarrolló parches GCC para la protección contra ataques de pila, conocidos como ProPolice . [13] Mejoró la idea de StackGuard al colocar buffers después de los punteros locales y los argumentos de función en el marco de la pila. Esto ayudó a evitar la corrupción de punteros, lo que impidió el acceso a ubicaciones de memoria arbitrarias.
Sin embargo, los ingenieros de Red Hat identificaron problemas con ProPolice y en 2005 volvieron a implementar la protección contra destrucción de pila para incluirla en GCC 4.1. [14] [15] Este trabajo introdujo la -fstack-protectorbandera, que protege solo algunas funciones vulnerables, y la -fstack-protector-allbandera, que protege todas las funciones, ya sea que la necesiten o no. [16]
En 2012, los ingenieros de Google implementaron la -fstack-protector-strongbandera para lograr un mejor equilibrio entre seguridad y rendimiento. [17] Esta bandera protege más tipos de funciones vulnerables que -fstack-protector, pero no todas las funciones, lo que proporciona un mejor rendimiento que -fstack-protector-all. Está disponible en GCC desde su versión 4.9. [18]
Todos los paquetes de Fedora se compilan con -fstack-protectordesde Fedora Core 5 y -fstack-protector-strongdesde Fedora 20. [19] [20] La mayoría de los paquetes en Ubuntu se compilan con -fstack-protectordesde 6.10. [21] Cada paquete de Arch Linux se compila con -fstack-protectordesde 2011. [22] Todos los paquetes de Arch Linux creados desde el 4 de mayo de 2014 usan -fstack-protector-strong. [23] La protección de pila solo se usa para algunos paquetes en Debian , [24] y solo para el sistema base FreeBSD desde 8.0. [25] La protección de pila es estándar en ciertos sistemas operativos, incluidos OpenBSD , [26] Hardened Gentoo [27] y DragonFly BSD . [ cita requerida ]
StackGuard y ProPolice no pueden brindar protección contra desbordamientos en estructuras asignadas automáticamente que se desbordan en punteros de función. ProPolice al menos reorganizará el orden de asignación para que dichas estructuras se asignen antes que los punteros de función. En PointGuard [28] se propuso un mecanismo independiente para la protección de punteros y está disponible en Microsoft Windows. [29]
La suite de compiladores de Microsoft implementa protección contra desbordamiento de búfer desde la versión 2003 a través del parámetro de línea de comandos /GS , que está habilitado de manera predeterminada desde la versión 2005. [30] El uso de /GS- deshabilita la protección.
La protección contra la destrucción de pila se puede activar mediante el indicador del compilador -qstackprotect
. [31]
Clang admite las mismas -fstack-protectoropciones que GCC [32] y un sistema de "pila segura" más fuerte ( -fsanitize=safe-stack ) con un impacto de rendimiento igualmente bajo. [33] Clang también tiene tres detectores de desbordamiento de búfer, a saber, AddressSanitizer ( ), [6] UBSan ( ), [34]
y el SafeCode no oficial (última actualización para LLVM 3.0). [35]-fsanitize=address
-fsanitize=bounds
Estos sistemas tienen diferentes ventajas y desventajas en términos de pérdida de rendimiento, sobrecarga de memoria y clases de errores detectados. La protección de la pila es estándar en ciertos sistemas operativos, incluido OpenBSD . [36]
El compilador C y C++ de Intel admite protección contra destrucción de pila con opciones similares a las proporcionadas por GCC y Microsoft Visual Studio. [37]
Fail-Safe C [7] es un compilador ANSI C de código abierto y seguro para memoria que realiza comprobaciones de límites basadas en punteros fat y acceso a memoria orientado a objetos. [38]
Inventado por Mike Frantzen, StackGhost es un simple ajuste a las rutinas de desbordamiento/relleno de la ventana de registro que hace que los desbordamientos de búfer sean mucho más difíciles de explotar. Utiliza una característica de hardware única de la arquitectura SPARC de Sun Microsystems (que es: desbordamiento/relleno diferido de la ventana de registro en la pila) para detectar modificaciones de punteros de retorno (una forma común para que un exploit secuestre rutas de ejecución) de forma transparente, protegiendo automáticamente todas las aplicaciones sin requerir modificaciones binarias o de código fuente. El impacto en el rendimiento es insignificante, menos del uno por ciento. Los problemas de gdb resultantes fueron resueltos por Mark Kettenis dos años después, lo que permitió habilitar la característica. Después de este evento, el código de StackGhost se integró (y optimizó) en OpenBSD /SPARC.
{{cite web}}
: CS1 maint: bot: estado de URL original desconocido ( enlace )Se ha abierto camino en GCC 4.9
gcc viene con la extensión de protección de pila
ProPolice
, que está habilitada de forma predeterminada.
El GCC reforzado de Gentoo activa el protector de pila de forma predeterminada a menos que se solicite explícitamente lo contrario.
clang viene con la protección de pila habilitada de forma predeterminada, equivalente a la opción
-fstack-protector-strong
en otros sistemas.