lunes, 17 de marzo de 2008

Buffer Overflow

Gorums [gorum@lab.matcom.uh.cu]

Tratare de desglosar muy brevemente de que se trata el BufferOverflow sin entrar en profundidad ya que el tema tiene tela por donde cortar. Para la comprensión de esto, explicare tres conceptos que se construyen uno sobre el otro y que ayudaran un poco al entendimiento de este tema.

Concepto de Pila:

En programación no es más que una estructura de dato, que tiene ciertas características, hagamos una analogía con los arreglos que es la estructura de dato que todo programador debe al menos conocer. El arreglo tiene un tamaño fijo (Estático), la pila en teoría no (Dinámico), en el arreglo tu puedes insertar o eliminar un objeto (no me refiero directamente a la POO) en cualquier posición mientras este esté dentro de sus limites establecidos, en la pila el ultimo que entra es el primero que sale, o sea el control de la inserción y la eliminación no lo tenemos nosotros sino la propia estructura y esta es la característica esencial que la distingue de las demás estructuras de datos.

Concepto de Proceso relacionado con la pila:

Cada aplicación que es cargada en memoria para ejecutarse se le nombra proceso, o sea, un programa en ejecución no es mas que un proceso y según la arquitectura PC que es la que mayormente todos usamos, un proceso se divide en varios segmentos, entre ellos se encuentra el segmento de dato que se encarga de almacenar toda la información estática de nuestra aplicación, como las variables globales y estáticas; el segmento de código que almacena las intrusiones de nuestro programa y el segmento de pila que el se encarga del almacenamiento de la información que tiene un carácter dinámico o volátil.

Concepto de Función relacionado con el segmento de pila de los procesos:

Cuando una función en términos de programación llama a otro, esta necesita poder almacenar el puntero a la siguiente instrucción, después de la llamada a la función, ya que cuando la función llamada termina, esta tiene que retornar a la función llamadora para continuar su ejecución por el punto donde se quedo. Esto es posible gracias al segmento de pila de los procesos y a los lenguajes que hacen uso de este recurso, permitiendo que mecanismo como la recursividad sean posible. Pero no solo la pila sirve para almacenar el puntero a la próxima instrucción llamado IP, también se almacena los argumentos que le son pasados a la función y las variables locales que estas declaran en el cuerpo de las funciones.

A la hora de la función llamada llegar al finar de su bloque de instrucciones, se tiene que sacar todo los elementos introducidos en la pila según el mecanismo de esta(el ultimo en entrar es el primero en salir), y así no dejar información que seria basura para la función llamadora, quedando el IP de la próxima instrucción de la función llamadora en el tope de la pila, y así actualizando el registro IP con el valor de la pila, se ejecutaría la siguiente instrucción después de haber echo el llamado a la función.

Expliquemos este proceso en forma algorítmica y seguro nos entenderemos mucho mejor, usaremos para esto con un poco de seudo código.

Un último punto a aclarar antes de entrar en el seudo código: Hablemos de dos registros del CPU que juegan un papel importante en el trabajo con el segmento de pila, estos son el EBP y el ESP, este último es el que apunta al tope de la pila y es incrementado o decrementando dependiendo de si se introduce o se elimina objetos en la pila, en cuanto al EBP su funcionalidad es el de acceder a los parámetros y a las variables locales mediante un offsets fijo para cada una de las variables.
La función f() llama a la función g():

Pseudocódigo

f()
{
int a=1;
int * b = &a;
....
g(a,b);
//<- aqui call g()
a=4; //siguiente instrucción después de llamar a g()
...
}

1-introducir en la pila la dirección o el valor de los argumentos:

f.push(a); // valor de a
f.puch(b); //valor de dirección de apuntador de b
2- Al llamar a g(), introducir en la pila la dirección de la siguiente instrucción a ejecutar después de la llamada y tomar la nueva dirección a ejecutar para saltar a ella:

f.push(ip); //dirección de la instrucción a=4
ip = &g; // dirección de g()

3- Almacena en la pila el EBP para a si poder modificar a este con el ESP:

f.push(ebp); //guardo el valor de bp ya que este va ha ser modificado
mov ebp,esp; // el registro pb toma el valor del registro sp

4-g() introduce en la pila sus variables locales

g.push(_varLocal1); //valor de _varLocal1
g.puch( _varLocal2); //valor de _varLocal2
....................
g.push( _varLocalN); //valor de _varLocalN

Nota: Hasta aquí esto es gracia a la instrucción en ensamblador CALL que es la que se encarga de todo este proceso.

5- Luego g() al terminar saca todo las cosas que le corresponde de la pila, de atrás hacia delante según el mecanismo de la pila.

g.pop( _varLocalN); //valor de _varLocalN
....................
g.pop(_varLocal1); //valor de _varLocal1
ebp = g.pop(ebp) // restauro el valor de ebp

6. El registro IP toma el valor que anterior mente había dejado en la pila, "asegurando" que la proxima instrucción sea la siguiente a la llamada de g()

ip = g.pop(ip); //ip apunta a la instrucción a = 4 de f()

Nota: Esta última parte es responsabilidad de las intrusiones en ensamblador RET o IRET en el caso de las interrupciones.

7. Continua la ejecución después de la llamada a g()

a=4;
......

Bien, mi intención es que se lleven al menos la idea de como es el proceso en el interior de una llamada a función en el lenguaje de programación C, y esto es valido para casi todo los lenguajes, o sea que esta es su forma de proceder.

Pasemos en este momento al plato fuerte:
!!El bufferOverflow EN VIVO!!
Voy a colocar el código y será explicado como comentarios de forma que se pueda entender por lo menos la idea.

//---------------------------------------------------------------------------

#pragma hdrstop

//---------------------------------------------------------------------------

#pragma argsused

/*
Nota Importante:
Para poder comprender las instrucciones y los mecanismos que se aplican en este ejemplo es indispensable haber entendidito todo lo expuesto arriba y tener a manos una buena base sobre el lenguaje C, principalmente con el trabajo con los punteros.
------------------------------------------------------
Compilado sobre C/C++ Builder 6.0
*/

void bufferOverflow()
{

//buffer es un array de 5 elementos del tipo char, lo cual tiene un tamaño (sizeof) de
//10 byte en total, ya que el char ocupa dos byte por cada posición del arreglo
  char buffer[5];

  int *ret;

//los 10 byte de buffer mas los 2 byte del EBP que ocupan en la pila
//suman 12 que es el desplazamiento que tiene que hacer ret para
//apuntar a la posición de retorno
  ret = buffer + 12; //Aritmética de puntero, ret ahora apunta a la posición de retorno

//sobrescribo el valor de la posición de retorno sumándole 26 para así
//poder saltarme las líneas x = 2 y el primer printf ()
  (*ret) += 26;
}

int main(int argc, char* argv[])
{
  int x;

  x = 1;
  bufferOverflow();
  x = 2;
//esto no se ejecuta
//ni este primer printf tampoco
  printf("Esto no se muestra porque yo no lo deseo %d",x);

  printf("Bienvenido a su primer Buffer Overflows [x = %d]",x);
  
return 0;
}

//---------------------------------------------------------------------------

Este código como bien esta expuesto en el comentario de encabezado esta diseñado para ser compilado y ejecutado sobre el IDE C/C++ Builder 6.0, recalcando que puede ser que sobre otros compiladores incluso sobre compiladores de la misma línea de los Builder este ejemplo no realiza su cometido, producto a que cada compilador traduce a código maquina a su forma, generando offsets diferentes para programas con idénticas instrucciones, lo que provoca que las direcciones de desplazamientos de estas instrucciones varíen de un compilador a otro.

Si usted entendió a lo sumo la parte teórica y observo algo del código, sin mucho rodeo le diré que el programa lo único que realiza es modificar la dirección de retorno asiendo que la misma se salte unas líneas, y eso es en teoría lo que consiste el bufferOverflow, ósea no es mas que sobrescribir la dirección de retorno, haciendo que el programa salte a donde nosotros queramos, observe que esto es posible gracias a la inseguridad que nos proporcionan los punteros y los arreglos, estos últimos como casos particulares de los punteros.

Como dato curioso el gusano de Morris, según muchos el primer gusano de Internet, pudo expandirse gracias a un bug del demonio finger, usando como exploit un bufferOverflos sobre la función gets() de la biblioteca estándar de entra/salida, gracias a que esta función no limita la cantidad de byte que debían ser introducidos en el buffer que después era enviado al servicio finger de la maquina victima. Existen infinidades de ejemplos de bufferOverflow que afectan mayormente a los sistemas operativos de la familia de los Unix y cada día aparecen muchos otros y nuevas forma de explotarlos.

Si se te quedo un poco corto este articulo por favor referirse a la phrack 48-0x14, referencia obligatoria para los mas exigentes en el tema.

Saludos Gorums

“No sabe mas aquel que lo sabe todo, sino el que sabe donde buscar”.



Artículos relacionados


No hay comentarios: