Desbordamiento del búfer de pila - Stack buffer overflow

En el software, se produce un desbordamiento del búfer de pila o un desbordamiento del búfer de pila 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 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 hará 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). Sobrellenar un búfer en la pila es más probable que descarrile la ejecución del programa que sobrellenar un búfer en el montón porque la pila contiene las direcciones de retorno para todas las llamadas a funciones activas.

Un desbordamiento del búfer de pila se puede provocar deliberadamente como parte de un ataque conocido como rotura de pila . 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 la pila está lleno de datos proporcionados por un usuario que no es de confianza, 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.

Explotación de desbordamientos de búfer de pila

El método canónico para explotar un desbordamiento de búfer basado en la pila es sobrescribir la dirección de retorno de la función con un puntero a los datos controlados por el atacante (generalmente en la propia pila). Esto se ilustra strcpy()en el siguiente ejemplo:

#include <string.h>

void foo(char *bar)
{
   char c[12];

   strcpy(c, bar);  // no bounds checking
}

int main(int argc, char **argv)
{
   foo(argv[1]);
   return 0;
}

Este código toma un argumento de la línea de comando y lo copia en una variable de pila local c. Esto funciona bien para argumentos de línea de comandos de menos de 12 caracteres (como puede ver en la figura B a continuación). Cualquier argumento de más de 11 caracteres provocará daños en 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 almacenar, la entrada sigue por el byte centinela cero. El byte cero termina sobrescribiendo una ubicación de memoria que está un byte más allá del final del búfer).

El programa se apila foo()con varias entradas:

A. - Antes de que se copien los datos.
B. - "hola" es el primer argumento de la línea de comandos.
C. - "AAAAAAAAAAAAAAAAAAAA \ x08 \ x35 \ xC0 \ x80" es el primer argumento de la línea de comandos.

Observe en la figura C anterior, cuando se proporciona un argumento de más de 11 bytes en la línea de comando, se foo()sobrescriben los datos de la pila local, el puntero del marco guardado y, lo que es más importante, la dirección de retorno. Cuando foo()retorna, saca 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 una explotación real de desbordamiento del búfer de pila, la cadena de "A" sería en cambio un código de 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 ), el atacante podría usar esta vulnerabilidad para obtener privilegios de superusuario en la máquina afectada.

El atacante también puede modificar los valores de las variables internas para aprovechar algunos errores. Con este ejemplo:

#include <string.h>
#include <stdio.h>

void foo(char *bar)
{
   float My_Float = 10.5; // Addr = 0x0023FF4C
   char  c[28];           // Addr = 0x0023FF30

   // Will print 10.500000
   printf("My Float value = %f\n", My_Float);

    /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       Memory map:
       @ : c allocated memory
       # : My_Float allocated memory

           *c                      *My_Float
       0x0023FF30                  0x0023FF4C
           |                           |
           @@@@@@@@@@@@@@@@@@@@@@@@@@@@#####
      foo("my string is too long !!!!! XXXXX");

   memcpy will put 0x1010C042 (little endian) in My_Float value.
   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/

   memcpy(c, bar, strlen(bar));  // no bounds checking...

   // Will print 96.031372
   printf("My Float value = %f\n", My_Float);
}

int main(int argc, char **argv)
{
   foo("my string is too long !!!!! \x10\x10\xc0\x42");
   return 0;
}

Diferencias relacionadas con la plataforma

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 del 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 no se utilizará ninguna dirección de retorno sobrescrita hasta que se desenrolle posteriormente la pila de llamadas. Otro ejemplo de un detalle específico de la máquina que puede afectar la elección de las técnicas de explotación es el hecho de que la mayoría de las arquitecturas de máquina de estilo RISC no permitirán el acceso no alineado a la memoria. 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 el salto a la técnica ESP 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 pila).

Pilas que crecen

Dentro del tema de los desbordamientos del búfer de pila, una arquitectura que se comenta 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 de 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. Una investigación más profunda de esta protección reclamada concluye que, en el mejor de los casos, es una solución ingenua. Cualquier desbordamiento que se produzca en un búfer de un marco de pila anterior aún sobrescribirá un puntero de retorno y permitirá la explotación malintencionada del error. Por ejemplo, en el ejemplo anterior, el puntero de retorno para foono se sobrescribirá porque el desbordamiento ocurre realmente dentro del marco de pila para memcpy. Sin embargo, debido a que el búfer que se desborda durante la llamada a memcpyreside en un marco de pila anterior, el puntero de retorno para memcpytendrá una dirección de memoria numéricamente más alta que el búfer. Esto significa que en lugar de foosobrescribir el puntero de retorno para memcpy, se sobrescribirá el puntero de retorno para . A lo sumo, 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 la pila, pero no reducirá significativamente la cantidad de errores explotables.

Esquemas de protección

A lo largo de los años, se han desarrollado varios esquemas de integridad del flujo de control para inhibir la explotación malintencionada del desbordamiento del búfer de pila. Por lo general, estos pueden clasificarse en tres categorías:

  • Detecte que se ha producido un desbordamiento del búfer de pila y así evitar la redirección del puntero de instrucción a código malicioso.
  • Evite la ejecución de código malicioso de la pila sin detectar directamente el desbordamiento del búfer de la pila.
  • Aleatorice el espacio de la memoria de modo que la búsqueda de código ejecutable no sea confiable.

Apilar canarios

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 del 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 la pila. La mayoría de los desbordamientos de búfer sobrescriben la memoria de direcciones de memoria inferiores a superiores, por lo que para sobrescribir el puntero de retorno (y así tomar el control del proceso), el valor canario también debe sobrescribirse. Este valor se verifica para asegurarse de que no haya cambiado antes de que una rutina use el puntero de retorno en la pila. Esta técnica puede aumentar en gran medida la dificultad de explotar un desbordamiento del búfer de pila porque obliga al atacante a obtener el control del puntero de instrucción por medios no tradicionales, como corromper otras variables importantes en la pila.

Pila no ejecutable

Otro enfoque para prevenir la explotación del desbordamiento del búfer de pila es hacer cumplir una política de memoria en la región de memoria de la pila que no permite la ejecución desde la pila ( W ^ X , "Write XOR Execute"). Esto significa que para ejecutar shellcode desde la pila, un atacante debe encontrar una manera de deshabilitar la protección de ejecución desde la memoria o encontrar una manera de poner su carga útil de shellcode en una región de memoria no protegida. 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 definitivamente hace que el enfoque canónico para la explotación del desbordamiento del búfer de pila falle, no está exento de problemas. Primero, es común encontrar formas de almacenar shellcode en regiones de memoria desprotegidas como el montón, por lo que es muy poco necesario cambiar la forma de explotación.

Incluso si esto no fuera así, hay otras formas. El más condenatorio es el llamado método return to libc para la creación de shellcode. En este ataque, la carga útil maliciosa cargará la pila no con shellcode, sino con una pila de llamadas adecuada para que la ejecución sea vectorizada a una cadena de llamadas de biblioteca estándar, generalmente con el efecto de deshabilitar las protecciones de ejecución de memoria y permitir que shellcode se ejecute normalmente. Esto funciona porque la ejecución nunca se dirige a la propia pila.

Una variante de return-to-libc es la programación orientada al retorno (ROP), que configura una serie de direcciones de retorno, cada una de las cuales ejecuta una pequeña secuencia de instrucciones de máquina seleccionadas dentro del código de programa existente o las bibliotecas del sistema, secuencia que termina con una devolución. Estos llamados gadgets logran cada uno una simple manipulación de registros o una ejecución similar antes de regresar, y al encadenarlos se logra el objetivo del atacante. Incluso es posible utilizar programación orientada a retorno "sin retorno" explotando instrucciones o grupos de instrucciones que se comportan de manera muy similar a una instrucción de retorno.

Aleatorización

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 usar, se proporciona una carga útil ejecutable (con una pila ejecutable) o se construye una usando la reutilización de código, como en ret2libc o en la programación orientada al retorno (ROP). La distribución aleatoria del diseño de la memoria, como concepto, evitará que el atacante sepa dónde está el código. Sin embargo, las implementaciones normalmente no aleatorizarán todo; por lo general, el ejecutable en sí mismo se carga en una dirección fija y, por lo tanto, incluso cuando ASLR (distribución aleatoria del diseño del espacio de direcciones) se combina con una pila no ejecutable, el atacante puede usar esta región fija de memoria. Por lo tanto, todos los programas deben compilarse con PIE (ejecutables independientes de la posición) de manera que incluso esta región de la memoria sea aleatoria. La entropía de la aleatorización es diferente de una implementación a otra y una entropía lo suficientemente baja puede ser en sí misma un problema en términos de forzar el espacio de memoria que se aleatoriza.

Ejemplos notables

  • El gusano Morris en 1988 se propagó en parte explotando un desbordamiento del búfer de pila en el servidor finger de Unix . [1]
  • El gusano Slammer en 2003 se extendió al explotar un desbordamiento de pila en Microsoft servidor 's SQL. [2]
  • El gusano Blaster en 2003 se propagó explotando un desbordamiento del búfer de pila en el servicio Microsoft DCOM .
  • El gusano Witty en 2004 se propagó explotando un desbordamiento del búfer de pila en el Agente de escritorio BlackICE de Internet Security Systems . [3]
  • Hay un par de ejemplos de Wii que permiten ejecutar código arbitrario en un sistema sin modificar. El "truco de Crepúsculo", que consiste en dar un nombre largo al caballo del personaje principal en The Legend of Zelda: Twilight Princess , y "Smash Stack" para Super Smash Bros. Brawl, que implica el uso de una tarjeta SD para cargar un archivo especialmente preparado en el editor de niveles en el juego. Aunque ambos pueden usarse para ejecutar cualquier código arbitrario, el último se usa a menudo para simplemente recargar Brawl con las modificaciones aplicadas.

Ver también

Referencias