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:
- 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.
- 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.
- 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.