En software, un desbordamiento del búfer de pila o 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 . [1] [2] 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 lo que realmente está asignado 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 del búfer (o desbordamiento del búfer). [1] Es más probable que llenar demasiado un búfer en la pila descarrile la ejecución del programa que llenar demasiado un búfer en el montón porque la pila contiene las direcciones de retorno de todas las llamadas de función activas.
Un desbordamiento del búfer de pila puede ser causado deliberadamente como parte de un ataque conocido como stack smashing . Si el programa afectado se ejecuta con privilegios especiales o acepta datos de hosts de red que no son de confianza (por ejemplo, un servidor web ), entonces el error es una vulnerabilidad de seguridad potencial . Si el búfer de pila se llena con datos suministrados por un usuario que no es de confianza, entonces ese usuario puede corromper la pila de tal manera que inyecte código ejecutable en el programa en ejecución y tome 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. [3] [4] [5]
El método canónico para explotar un desbordamiento de búfer basado en pila es sobrescribir la dirección de retorno de la función con un puntero a datos controlados por el atacante (generalmente en la propia pila). [3] [6] Esto se ilustra strcpy()
en el siguiente ejemplo:
#include <cadena.h> void foo ( char * bar ) { char c [ 12 ]; strcpy ( c , bar ); // sin comprobación de límites } int principal ( int argc , char ** argv ) { foo ( argv [ 1 ]); devolver 0 ; }
Este código toma un argumento de la línea de comandos y lo copia a una variable de pila local c
. Esto funciona bien para argumentos de línea de comandos menores a 12 caracteres (como se puede ver en la figura B a continuación). Cualquier argumento mayor a 11 caracteres provocará la corrupción de la pila. (El número máximo de caracteres que es seguro es uno menos que el tamaño del búfer aquí porque en el lenguaje de programación C, las cadenas terminan con un carácter de byte nulo. Por lo tanto, una entrada de doce caracteres requiere trece bytes para almacenarse, la entrada seguida por el byte cero centinela. El byte cero luego termina sobrescribiendo una ubicación de memoria que está un byte más allá del final del búfer).
El programa se acumula foo()
con varias entradas:
En la figura C anterior, cuando se proporciona un argumento mayor a 11 bytes en la línea de comandos, foo()
se sobrescriben los datos de la pila local, el puntero de marco guardado y, lo más importante, la dirección de retorno. Cuando foo()
retorna, extrae la dirección de retorno de la pila y salta a esa dirección (es decir, comienza a ejecutar instrucciones desde esa dirección). Por lo tanto, el atacante ha sobrescrito la dirección de retorno con un puntero al búfer de pila char c[12]
, que ahora contiene datos proporcionados por el atacante. En un exploit de desbordamiento de búfer de pila real, la cadena de "A" sería en cambio un código shell adecuado para la plataforma y la función deseada. Si este programa tuviera privilegios especiales (por ejemplo, el bit SUID configurado para ejecutarse como superusuario ), entonces el atacante podría usar esta vulnerabilidad para obtener privilegios de superusuario en la máquina afectada. [3]
El atacante también puede modificar los valores de las variables internas para aprovechar algunos errores. Con este ejemplo:
#include <cadena.h> #include <stdio.h> void foo ( char * bar ) { float Mi_Float = 10.5 ; // Direccion = 0x0023FF4C char c [ 28 ]; // Direccion = 0x0023FF30 // Imprimirá 10.500000 printf ( "Mi valor flotante = %f \n " , My_Float ); /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Mapa de memoria: @ : c memoria asignada # : My_Float memoria asignada *c *My_Float 0x0023FF30 0x0023FF4C | | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@##### foo("¡¡¡mi cadena es demasiado larga!!!!! XXXXX"); memcpy colocará 0x1010C042 (little endian) en el valor My_Float. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ memcpy ( c , bar , strlen ( bar )); // sin verificación de límites... // Imprimirá 96.031372 printf ( "Mi valor flotante = %f \n " , My_Float ); } int main ( int argc , char ** argv ) { foo ( "mi cadena es demasiado larga !!!!! \x10\x10\xc0\x42 " ); return 0 ; }
Normalmente se utilizan dos métodos para alterar la dirección almacenada en la pila: directo e indirecto. Los atacantes comenzaron a desarrollar ataques indirectos, que tienen menos dependencias, para eludir las medidas de protección que se crearon para reducir los ataques directos. [7]
Varias plataformas tienen diferencias sutiles en su implementación de la pila de llamadas que pueden afectar la forma en que funcionará un exploit de desbordamiento de búfer de pila. Algunas arquitecturas de máquinas almacenan la dirección de retorno de nivel superior de la pila de llamadas en un registro. Esto significa que cualquier dirección de retorno sobrescrita no se utilizará hasta que se desenrolle más tarde la pila de llamadas. Otro ejemplo de un detalle específico de la máquina que puede afectar la elección de técnicas de explotación es el hecho de que la mayoría de las arquitecturas de máquinas de estilo RISC no permitirán el acceso no alineado a la memoria. [8] Combinado con una longitud fija para los códigos de operación de la máquina, esta limitación de la máquina puede hacer que la técnica de saltar a la pila sea casi imposible de implementar (con la única excepción de cuando el programa realmente contiene el código improbable para saltar explícitamente al registro de la pila). [9] [10]
Dentro del tema de los desbordamientos del búfer de pila, una arquitectura que se discute a menudo pero que rara vez se ve es aquella en la que la pila crece en la dirección opuesta. Este cambio en la arquitectura se sugiere con frecuencia como una solución al problema del desbordamiento del búfer de pila porque cualquier desbordamiento de un búfer de pila que ocurra dentro del mismo marco de pila no puede sobrescribir el puntero de retorno. Sin embargo, cualquier desbordamiento que ocurra en un búfer de un marco de pila anterior sobrescribirá un puntero de retorno y permitirá la explotación maliciosa del error. [11] Por ejemplo, en el ejemplo anterior, el puntero de retorno para foo
no se sobrescribirá porque el desbordamiento ocurre realmente dentro del marco de pila for memcpy
. Sin embargo, debido a que el búfer que se desborda durante la llamada a memcpy
reside en un marco de pila anterior, el puntero de retorno para memcpy
tendrá una dirección de memoria numéricamente mayor que el búfer. Esto significa que en lugar de foo
sobrescribirse el puntero de retorno para, memcpy
se sobrescribirá el puntero de retorno para . Como máximo, esto significa que hacer crecer la pila en la dirección opuesta cambiará algunos detalles de cómo se pueden explotar los desbordamientos del búfer de pila, pero no reducirá significativamente la cantidad de errores explotables. [ cita requerida ]
A lo largo de los años, se han desarrollado varios esquemas de integridad del flujo de control para inhibir la explotación maliciosa del desbordamiento del búfer de pila. Estos esquemas se pueden clasificar en tres categorías:
Los canarios de pila, llamados así por su analogía con un canario en una mina de carbón , se utilizan para detectar un desbordamiento de búfer de pila antes de que pueda ocurrir la ejecución de código malicioso. Este método funciona colocando un pequeño entero, cuyo valor se elige aleatoriamente al inicio del programa, en la memoria justo antes del puntero de retorno de pila. La mayoría de los desbordamientos de búfer sobrescriben la memoria desde direcciones de memoria inferiores a superiores, por lo que para sobrescribir el puntero de retorno (y así tomar el control del proceso) también se debe sobrescribir el valor del canario. Este valor se verifica para asegurarse de que no haya cambiado antes de que una rutina use el puntero de retorno en la pila. [2] Esta técnica puede aumentar en gran medida la dificultad de explotar un desbordamiento de búfer de pila porque obliga al atacante a obtener el control del puntero de instrucción por algún medio no tradicional, como corromper otras variables importantes en la pila. [2]
Otro enfoque para evitar la explotación del desbordamiento del búfer de pila es aplicar una política de memoria en la región de memoria de la pila que no permita la ejecución desde la pila ( W^X , "Write XOR Execute"). Esto significa que para ejecutar el código shell desde la pila, un atacante debe encontrar una forma de desactivar la protección de ejecución desde la memoria o encontrar una forma de colocar su carga útil de código shell en una región no protegida de la memoria. Este método se está volviendo más popular ahora que el soporte de hardware para el indicador de no ejecución está disponible en la mayoría de los procesadores de escritorio.
Si bien este método evita el exploit de destrucción de pila canónica, los desbordamientos de pila se pueden explotar de otras maneras. En primer lugar, es común encontrar formas de almacenar shellcode en regiones de memoria desprotegidas como el montón, por lo que es necesario realizar muy pocos cambios en la forma de explotación. [12]
Otro ataque es el llamado método de retorno a libc para la creación de shellcode. En este ataque, la carga maliciosa cargará la pila no con shellcode, sino con una pila de llamadas adecuada, de modo que la ejecución se vectorice a una cadena de llamadas a la biblioteca estándar, generalmente con el efecto de deshabilitar las protecciones de ejecución de memoria y permitir que el shellcode se ejecute con normalidad. [13] Esto funciona porque la ejecución en realidad nunca se vectoriza a la pila en sí.
Una variante de retorno a libc es la programación orientada al retorno (ROP), que establece una serie de direcciones de retorno, cada una de las cuales ejecuta una pequeña secuencia de instrucciones de máquina seleccionadas cuidadosamente dentro del código de programa existente o bibliotecas del sistema, secuencia que termina con un retorno. Cada uno de estos llamados gadgets realiza alguna manipulación de registro simple o ejecución similar antes de regresar, y al encadenarlos se logran los fines del atacante. Incluso es posible utilizar la programación orientada al retorno "sin retorno" explotando instrucciones o grupos de instrucciones que se comportan de manera muy similar a una instrucción de retorno. [14]
En lugar de separar el código de los datos, otra técnica de mitigación es introducir aleatorización en el espacio de memoria del programa en ejecución. Dado que el atacante necesita determinar dónde reside el código ejecutable que se puede utilizar, se proporciona una carga útil ejecutable (con una pila ejecutable) o se construye una utilizando la reutilización de código, como en ret2libc o la programación orientada al retorno (ROP). La aleatorización del diseño de la memoria evitará, como concepto, que el atacante sepa dónde se encuentra el código. Sin embargo, las implementaciones normalmente no aleatorizarán todo; por lo general, el ejecutable en sí se carga en una dirección fija y, por lo tanto, incluso cuando ASLR (aleatorización del diseño del espacio de direcciones) se combina con una pila no ejecutable, el atacante puede utilizar esta región fija de memoria. Por lo tanto, todos los programas deben compilarse con PIE (ejecutables independientes de la posición) de modo que incluso esta región de memoria sea aleatorizada. La entropía de la aleatorización es diferente de una implementación a otra y una entropía suficientemente baja puede en sí misma ser un problema en términos de fuerza bruta en el espacio de memoria que se aleatoriza.
Las mitigaciones anteriores hacen que los pasos de explotación sean más difíciles, pero aún es posible explotar un desbordamiento de búfer de pila si existen algunas vulnerabilidades o si se cumplen ciertas condiciones. [15]
Un atacante puede explotar la vulnerabilidad de la cadena de formato para revelar las ubicaciones de memoria en el programa vulnerable. [16]
Cuando se activa la prevención de ejecución de datos para prohibir cualquier acceso de ejecución a la pila, el atacante puede seguir utilizando la dirección de retorno sobrescrita (el puntero de instrucción) para señalar datos en un segmento de código ( .text en Linux) o en cualquier otra sección ejecutable del programa. El objetivo es reutilizar el código existente. [17]
Consiste en sobrescribir el puntero de retorno un bit antes de una instrucción de retorno (ret en x86) del programa. Las instrucciones entre el nuevo puntero de retorno y la instrucción de retorno se ejecutarán y la instrucción de retorno volverá a la carga útil controlada por el explotador. [17] [ aclaración necesaria ]
La programación orientada a saltos es una técnica que utiliza instrucciones de salto para reutilizar el código en lugar de la instrucción ret. [18]
Una limitación de la implementación de ASLR en sistemas de 64 bits es que es vulnerable a ataques de divulgación de memoria y fuga de información. El atacante puede iniciar el ROP revelando una única dirección de función mediante un ataque de fuga de información. La siguiente sección describe la estrategia existente similar para romper la protección ASLR. [19]
{{cite web}}
: CS1 maint: nombres numéricos: lista de autores ( enlace )