REVISTA .SEGURIDAD | 1 251 478, 1 251 477 | REVISTA BIMESTRAL

Uno de los clásicos: Buffer overflow

Revista .Seguridad 23, revisión de la vulnerabilidad Buffer Overflow - Seguridad informática

Buffer overflow es una de las vulnerabilidades persistentes a pesar de la evolución y complejidad de los mecanismos de seguridad. Se encuentra presente en diversas aplicaciones por lo que aparece constantemente en las listas de vulnerabilidades críticas publicadas por instituciones enfocadas a la notificación de nuevas amenazas de seguridad.

¿Qué es buffer overflow?

Buffer overflow es una vulnerabilidad causada por la inserción de datos con tamaño superior al esperado por una aplicación, lo que provoca la sobrescritura de espacios adyacentes en la memoria. Es común hacer una la analogía con un contenedor de agua (que representa el buffer) con capacidad para almacenar un litro (que representan los datos de la aplicación). Si en este recipiente se introducen tres litros, lo que ocurrirá es que un litro será adecuadamente vertido en el contenedor pero dos litros serán desbordados del recipiente. Esta es una descripción general de la vulnerabilidad, pero aquí pueden surgir diversas dudas, ¿qué es un buffer?, ¿qué pasa sí se sobrescriben espacios adyacentes de memoria?, ¿cuáles son las consecuencias de esta vulnerabilidad?

Para tener una concepción más adecuada de lo que es un buffer, es necesario recordar algunos conceptos sobre la memoria.

Estructura de la memoria       

Cuando un programa es ejecutado, el sistema operativo reserva una zona de memoria para que la aplicación realice correctamente sus instrucciones, este espacio se divide en zonas de acuerdo a los distintos tipos de datos. Primero es necesario cargar el código ejecutable del programa, es decir, las instrucciones. La zona etiquetada como data es utilizada por lenguajes de programación que permiten la creación de variables globales y variables estáticas.

Asimismo, se reservan por lo menos dos espacios para los datos requeridos en la ejecución, estos espacios son stack  y heap.

El stack  almacena los argumentos de las funciones, las variables locales y las direcciones de retorno de las llamadas a funciones.

El heap se encarga de gestionar la memoria dinámica, es decir la memoria solicitada durante el tiempo de ejecución.     

Estructura de la memoria
Figura 1. Estructura de la memoria.

Existen regiones reservadas para datos de entrada del programa, pueden ser localizadas tanto en el stack como en el heap dependiendo del tipo de datos que van a almacenar. Estas regiones son llamadas buffer, por lo que se puede definir el buffer como un espacio en memoria que sirve como almacenamiento temporal de datos de entrada en un programa.

Tipos de buffer overflow

Básicamente pueden distinguirse dos tipos primarios de buffer overflow que se ligan directamente con la explicación previa sobre las regiones de memoria ya que su nombre se deriva del espacio en memoria sobre el cual es localizada la vulnerabilidad:

  • Stack overflow
  • Heap overflow

En este artículo se hablará sobre el tipo stack overflow

Stack overflow

Para comprender esta vulnerabilidad, es necesario conocer primero el funcionamiento del stack. La región de memoria reservada para el stack tiene una estructura de datos tipo LIFO (Last In First Out) donde el último dato que ingresa es el primero en salir. Existen dos operaciones que pueden realizarse en el stack, la operación “PUSH” para añadir elementos y “POP” para extraerlos, los datos almacenados se extraen uno a uno. Para entenderlo mejor puedes pensar en un conjunto de libros, colocando en primer lugar el de geometría (operación PUSH para cada uno), en segundo lugar el de cálculo y en tercer lugar el de álgebra. Para poder retirar el de cálculo, es necesario primero quitar el de álgebra (operación POP sobre el libro de algebra) ya que fue el último en ser colocado.

Es posible describirlo de una forma más abstracta, introduciendo letras a un stack:

  1. PUSH A
  2. PUSH B
  3. PUSH C

                    Inicio de stack

3. C

2. B

1. A

                     Fin de stack

Otro elemento importante para analizar un buffer overflow es la forma en la que se maneja una llamada a procedimiento o función. Al invocar una función se realiza un salto dentro de la memoria para ceder el control a las instrucciones que conforman dicha función. Una vez que termina de ejecutarse la llamada a procedimiento o función, es necesario que el programa sepa a dónde regresar para continuar ejecutando las siguientes instrucciones, por lo que es necesario almacenar un indicador en el stack, éste es conocido como dirección de retorno, pieza clave para la ejecución de un buffer overflow.  

Para entenderlo mejor, pensemos en un mesero al levantar una comanda en un restaurante, él atiende su mesa y pregunta por la orden, pasa esta orden a la cocina y ahí se realizan las instrucciones adecuadas para satisfacer el pedido, similar a como lo haría una función. Al terminar la preparación, es muy importante avisar al mesero que la orden está lista (dirección de retorno) y se le cede el control para que la haga llegar a la mesa.


La figura 2. Llamada a procedimiento o función.

Para observar cómo se manejan las variables y las llamadas a función, es posible recurrir al siguiente ejemplo, donde se inserta en el stack cada elemento del programa.

void funcion_vulnerable(int var1, var2, var3)

{

   char buffer1[5];

   char buffer2[10];

}


void main()

{

  funcion_vulnerable(a,b,c);

}

La memoria quedaría de la siguiente forma (tomando en cuenta los elementos descritos en la sección previa):


Figura 3. Estructura de stack

Se realizan las operaciones PUSH sobre cada variable y al llegar a funcion vulnerable, se almacena en el stack la dirección de retorno para que el programa tenga la dirección a la cual deberá regresar cuando termine de ejecutar la función. Finalmente, una vez que se ejecutan las instrucciones de funcion_vulnerable, se almacena en el stack el contenido de cada buffer.

push var3

push var2

push var1

llamada a función  = introducir dirección de retorno en stack

push buffer1

push buffer2

Figura 4. Operaciones sobre stack.

Nota: La descripción sobre la memoria es muy general y tiene como finalidad la comprensión del funcionamiento de la vulnerabilidad.

Finalmente, ya que se tiene una visión más clara sobre las operaciones realizadas sobre el stack, se puede ejemplificar un stack overflow con el siguiente código en lenguaje C.  

El código valida con una condición, que se le haya pasado por lo menos un argumento al programa (argc=argument count) para llamar a “funcion” pasándole los parámetros insertados por el usuario (argv=argument vector) “cadena”, la función genera un buffer “buffer1” de un tamaño de 8 caracteres y posteriormente copia “cadena” en “buffer1”. El punto crítico se encuentra en esta instrucción, donde se copia la cadena enviada por el usuario en el buffer, finalmente se manda a imprimir en pantalla un texto.  

#include <stdio.h> 
void funcion(char *cadena); 

int main(int argc, char *argv[]) 
{ 
   if(argc > 1)             //Validar paso de parámetros del usuario 
      funcion(argv[1]);     //Llama a funcion y le envía parámetros del usuario

   printf("Entrada estándar menor o igual a 8 caracteres\n"); 
} 

void funcion(char *cadena) 
{ 
   char buffer1[8];         //Genera buffer con espacio para 8 caracteres
   strcpy(buffer1, cadena); //Copia al buffer la entrada de usuario  
} 

La ejecución del programa con el parámetro “AAAAAAAA” (8 caracteres), desplegará el siguiente resultado: 

$ ./Codigo2 AAAAAAAA 

Entrada estándar menor o igual a 8 caracteres 

Todo se ejecuta de la manera esperada debido a que se han insertado en el buffer la cantidad de caracteres adecuados con los cuales se ha delimitado.

¿Qué pasaría si se ingresaran más caracteres de los que soporta el buffer, por ejemplo la cadena “AAAAAAAAA” (9 caracteres)? 

$ ./Codigo2 AAAAAAAAA 
*** stack smashing detected ***: ./Codigo2 terminated 
… 
0208c000-0504c000 rw-p 00000000 00:00 0          [heap] 
… 
decac000-d0000000 rw-p 00000000 00:00 0          [stack] 
Aborted (core dumped) 

La inclusión de una cantidad mayor a la permitida por los límites del buffer provoca un error. Se debe a que el buffer se ha desbordado y escribe los caracteres restantes en direcciones contiguas de memoria. Es ahí donde inicia el problema de un stack overflow.

Consecuencias del stack overflow

Descarga la Revista .Seguridad en PDF¿Qué pasa sí se sobrescribe la dirección de retorno? Se debe recordar que la dirección de retorno indica qué es lo que se debe ejecutar una vez que termina la ejecución de una función o procedimiento, en este caso, pueden ocurrir las siguientes opciones:

  • Sí se llena con caracteres que no representen una dirección válida de memoria, generará un error de tipo “segmentation fault”. Por ejemplo, “AAAAAA” no es una dirección reconocible de memoria.
  • Se puede apuntar a una dirección dentro del programa, provocando que se ejecute de forma incorrecta, por ejemplo, ir al inicio del programa o al final de éste, ir a una instrucción de impresión en pantalla, etc. 
  • Y el peor escenario y por el cual esta vulnerabilidad es muy explotada, es posible ejecutar código arbitrario:
  • Escrito por un individuo malicioso apuntando a una dirección en memoria donde inicie el código malicioso.
  • Código que ya se encuentra en el espacio de direcciones del sistema, haciendo referencia a la dirección donde se encuentre localizado, por ejemplo la ejecución de un Shell (/bin/sh).
Finalmente este artículo ha descrito sin un lenguaje mayormente técnico lo que es un buffer overflow, en particular el de tipo stack overflow, se describieron los elementos principales para su comprensión y finalmente se resumieron sus consecuencias. Un punto importante para explotar esta vulnerabilidad, es encontrar la dirección de retorno y poder sobrescribirla.  
 
Si quieres saber más consulta:

UNAM

[ CONTACTO ]

Se prohíbe la reproducción total o parcial
de los artículos sin la autorización por escrito de los autores

 

Hecho en México, Universidad Nacional Autónoma de México (UNAM) © Todos los derechos reservados 2018.