Buffers Overflow: Ponte las pilas con esta pila

Buffers Overflow: Ponte las pilas con esta pila

La mayoría de los exploits basados en Buffer Overflow tratan de obtener una cuenta administrativa mediante la ejecución de código malicioso. El principio es muy simple: las instrucciones maliciosas se almacenan en un buffer, donde se desbordan alterando varias secciones de la memoria manipulando el proceso. Existen dos tipos de ataques: Stack Overflow y Heap Overflow. Primero veamos como se organiza la memoria:

Cuando un programa se ejecuta sus elementos se mapean en la memoria de cierta manera, la parte alta contiene los parámetros del proceso, luego siguen dos secciones, stack y heap, que se colocan durante el tiempo de ejecución. La stack o pila es donde se almacenan las variables y argumentos necesarios para llamar a una función, esta funciona con el sistema LIFO (Last In, First Out) y crece hacia la memoria baja. Las variables dinámicas se almacenan en heap o montículo. Generalmente son el resultado de funciones como malloc en C que nos devuelven un puntero a la dirección de memoria reservada. Las secciones .bss y .data están destinadas a las variables globales y se colocan durante la compilación. La sección .data contiene información estatica inicializada, mientras que la que no esta inicializada se encuentra en .bss. La ultima seccion es .text que contiene el código e incluso información de solo lectura.

Algunos ejemplos de como funciona:


char vacia;  // .bss
char valor = 'a';  // .data
int main (){
static int vacia2; // .bss
static int valor2 = 1; // .data
char *punt = malloc(3); // Heap
}


Ahora veamos que sucede en la memoria (o la pila) cuando se usan las funciones. En un sistema Unix una llamada a una función se hace en 3 pasos:

  1.  Se guarda el puntero del marco actual. Un marco (frame) es la unidad de la pila y contiene todos los elementos relacionados con una función. Aquí se reserva la memoria necesaria para la función.
  2. Los parámetros de la función se almacenan en la pila y se guarda el puntero de instrucción (registro asm) para saber a donde regresar cuando acabe la funcion.
  3. Se regresa al marco anterior de la pila.

Un ejemplo de como funciona esto nos ayudará a entender como es que funcionan las técnicas mas comunes de Buffer Overflow.

En el código:

int funcion(int a, int b, int c){
int i=4;
return (a+i);
}

int main(int argc, char **argv){
funcion(0, 1, 2);
return 0;
}

Si lo desensamblamos para conocer la forma real en la que trabaja obtendremos algo así:

1. Dump of assembler code for function main:

0x80483e4 <main>: push %ebp
0x80483e5 <main+1>: mov %esp,%ebp
0x80483e7 <main+3>: sub $0x8,%esp

Vemos que están los registros EBP y ESP, el primero es un puntero al frame actual y el segundo es un puntero a la parte alta de la pila. En esta parte guarda la dirección del marco actual y cambia a otro.

2.-

0x80483ea <main+6>: add $0xfffffffc,%esp
 0x80483ed <main+9>: push $0x2
 0x80483ef <main+11>: push $0x1
 0x80483f1 <main+13>: push $0x0
 0x80483f3 <main+15>: call 0x80483c0 <funcion>

La función es llamada con estas 4 instrucciones. Primero los parámetros se introducen mediante el sistema LIFO, por lo que están en orden inverso, y después se llama a la función.

3.-

0x80483f8 <main+20>: add $0x10,%esp
 0x80483fb <main+23>: xor %eax,%eax
 0x80483fd <main+25>: jmp 0x8048400 <main+28>
 0x80483ff <main+27>: nop
 0x8048400 <main+28>: leave
 0x8048401 <main+29>: ret
 End of assembler dump.

Esta instrucción representa que funcion() regresa a main(), limpiando los registros y regresando al frame anterior, que le pertenecía a main(). Las ultimas 2 instrucciones son el fin de main().

Ahora veamos la función:

Dump of assembler code for function funcion:
 0x80483c0 <funcion>: push %ebp
 0x80483c1 <funcion+1>: mov %esp,%ebp
 0x80483c3 <funcion+3>: sub $0x18,%esp

Aquí en la función EBP apunta hacia el ambiente por lo que manda a la pila y se corre hacia arriba en la memoria para tomar una dirección baja y  reservar espacio para las variables de la función.

0x80483c6 <funcion+6>: movl $0x4,0xfffffffc(%ebp)
 0x80483cd <funcion+13>: mov 0x8(%ebp),%eax
 0x80483d0 <funcion+16>: mov 0xfffffffc(%ebp),%ecx
 0x80483d3 <funcion+19>: lea (%ecx,%eax,1),%edx
 0x80483d6 <funcion+22>: mov %edx,%eax
 0x80483d8 <funcion+24>: jmp 0x80483e0 <funcion+32>
 0x80483da <funcion+26>: lea 0x0(%esi),%esi

Estas son las instrucciones de la función.

0x80483e0 <funcion+32>: leave
 0x80483e1 <funcion+33>: ret
 End of assembler dump.

Y aquí retorna, estas dos instrucciones regresan a EBP y ESP a sus valores anteriores, aunque recordemos que antes de entrar a la función estos ya habían sido modificados, y regresa IP para saber qué posición de la memoria contiene la instrucción a ejecutar. Este ejemplo nos demuestra como se modifica la pila y los punteros cuando una funcion es llamada. Lo importante es observar como se reserva la memoria. Si esto no se hace con cuidado se puede modificar la estructura de la memoria y ejecutar codigo nuevo. Esto podria suceder ya que la dirección donde se encuentra la instruccion siguiente se copia desde la pila al registro EIP. Todo esto sucede en la pila, así que si manipulamos la pila podemos acceder a esa zona y sobrescribirla para que apunte a lo que nosotros queremos.

Deja un comentario

Your email address will not be published. Required fields are marked *

You may use these <abbr title="HyperText Markup Language">HTML</abbr> tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>