Tras el pequeño rollo teórico que vimos ayer, hoy toca entrar de lleno en las cuestiones prácticas, que espero que aclaren todas las dudas que ayer pudieran surgir. Veamos el siguiente trozo de código en C de 4 líneas. La primera instrucción asigna el valor 9 a la variable X, la segunda llama a una función, la tercera le asigna a X el valor 1 y la siguiente línea mostrará el valor de X:
x = 9;
funct(5,6,7);
x = 1;
printf("El valor de X es: %d\n", x);
La función lo que va a hacer es obtener la dirección donde se guarda el EIP (dirección de la siguiente
línea a ejecutar), de tal forma que modificaremos su contenido para que en vez de saltar a la
siguiente línea que sería “x = 1″, salte al “printf” mostrando que x es igual a 9, y no 1 que sería lo esperado.
Antes de mostrar el código completo recordar un par de cosas:
- En relación con los punteros, recordar que si yo declaro un puntero “int *p;”, para modificar la dirección donde apunta debo hacer “p = dirección;”, y a su vez, para modificar el valor (el contenido) del dato donde apunta el puntero se realiza “*p = dato;”.
- La segunda es que la memoria se numera por bytes (8 bits) y está agrupada por palabras, siendo una palabra 32 bits es decir, 4 bytes. Por tanto, un registro que guarde una dirección de memoria ocupa una palabra, es decir 4 bytes (32 bits). Por último, recordar que un char ocupa 1 byte, y en una palabra caben 4 chars.
Con esto, pasemos al código:
#include <stdio.h>
void funct(int a, int b, int c){
int i, j;
char buffer[4];
char *ret;
buffer[0] = 'A';
buffer[1] = 'B';
buffer[2] = 'C';
buffer[3] = 'D';
for(i = 0; i < (9*4); i += 4){
ret = buffer + i;
if( i == 0){
printf("Vector:\n");
for( j=0; j < 4; j++){
printf("Valor: 0x%X PosMem: 0x%X\n", *ret, ret);
ret += 1;
}
printf("-----------\n");
}
else{
printf("Valor: 0x%X PosMem: 0x%X\n", *ret, ret);
}
}
// Aquí es donde esta la fiesta
// Retrocedemos hasta donde esta el EIP
ret -= 12;
// Modificamos la dirección de vuelta y le sumamos 7
// que es la siguiente instrucción
*ret += 7;
printf("Premio: Valor: 0x%X PosMem: 0x%X\n", *ret, ret);
}
int main(void){
int x;
x = 9;
funct(4,5,6);
x = 1;
printf("El valor de X es: %d\n", x);
return 0;
}
Recapitulemos. Como vemos, asignamos 9 a la variable X. La siguiente instrucción llama a la función pasándole los números 4, 5 y 6 como parámetros. Por tanto, apilará el valor 6, 5 y 4. A continuación apila el valor del EIP que es el valor de la siguiente instrucción a ejecutar, que en nuestro caso es la instrucción “x = 1″. Encima del EIP apilara el valor del EBP de la pila actual, y sobre éste las variables locales de la función.
Dentro de la función apilamos variables locales para los bucles así como un vector de 4 caracteres “ABCD” o lo que es lo mismo en hexadecimal “0×41 0×42 0×43 0×44″, el cual ocupa una palabra (4 x char (1 byte) = 4 bytes = 32 bits).
Una vez realizadas estas operaciones, se muestra el contenido de la pila de la función seguido de la posición de memoria donde se guardan el dato, que corresponde con lo explicado en teoría. Ya por último se mueve el puntero “ret” a la posición donde se guarda el EIP (próxima linea a ejecutar) y se modifica con un valor de tal forma que cambia la posición de la siguiente instrucción a ejecutar, que seria “x = 1″, por la instrucción “printf”. Compilamos y ejecutamos el programa:
user1@base:~/Desktop> gcc -ggdb --no-stack-protector prueba.c -o prueba
user1@base:~/Desktop> ./prueba
Vector:
Valor: 0x41 PosMem: 0xBF869098
Valor: 0x42 PosMem: 0xBF869099
Valor: 0x43 PosMem: 0xBF86909A
Valor: 0x44 PosMem: 0xBF86909B
--------
Valor: 0x4 PosMem: 0xBF86909C
Valor: 0x4 PosMem: 0xBF8690A0
Valor: 0xFFFFFFA4 - PosMem: 0xBF8690A4
Valor: 0xFFFFFFD8 - PosMem: 0xBF8690A8
Valor: 0x22 - PosMem: 0xBF8690AC
Valor: 0x4 PosMem: 0xBF8690B0
Valor: 0x5 PosMem: 0xBF8690B4
Valor: 0x6 PosMem: 0xBF8690B8
Premio: Valor: 0x29 PosMem: 0xBF8690AC
El valor de X es: 9
Analicemos el resultado de la ejecución. Comienza mostrando la cima de la pila que es el vector de caracteres (char) de nombre “buffer”, donde podemos ver que las posiciones son consecutivas (claro, un char ocupa un byte). Posteriormente muestra los datos de 4 en 4 bytes (una palabra), puesto que lo que nos interesa son los registros que guardan direcciones de memoria que ocupan 32 bits (4 bytes, una palabra). Se muestra un
registro sin importancia (0xBF8690A4) que se emplea en la ejecución de los bucles de la función, y luego hay dos registros en la posición 0xBF8690A8 y 0xBF8690AC (nos interesa el último). A continuación vienen 3 registros que son el valor 4, 5 y 6 que son los valores que hemos pasado a la función en su llamada.
Como podemos imaginar, el dato que hay en la posición de memoria 0xBF8690AC cuyo valor es 0×22 corresponde con el EIP (dirección de la siguiente instrucción a ejecutar una vez termine la función). Por tanto lo que hacemos es ir a dicha posición de memoria, y modificarla incrementando su valor en 7 para que salte a la posición de memoria donde nosotros queremos: la instrucción “printf”. ¿Pero, cómo sabía que tenia que sumarle 7? Veamos:
user1@base:~/Desktop> gdb -q prueba Using host libthread_db library "/lib/libthread_db.so.1". (gdb) disassemble main 0x080484ee <main+0>: lea 0x4(%esp),%ecx ... 0x080484ff <main+17>: movl $0x9,0x8(%ebp) ... 0x0804851d <main+47>: call 0x8048404 <funct> 0x08048522 <main+52>: movl $0x1,-0x8(%ebp) 0x08048529 <main+59>: mov -0x8(%ebp),%eax ...
¡No salgais corriendo! Fijaos bien… hay una llamada (main+17) que mueve al registro el valor 9 ($0×9), posteriormente llama a la función (main+47) y la siguiente línea de código es la misma que la de (main+17), pero esta vez le asigna el valor 1 ($0×1). Por tanto, claramente (main+17) es x = 9 y (main+52) es x = 1.
Por lo que si la instrucción “x = 1″ se encuentra en la posición de memoria 0×08048522, la siguiente línea de ejecución 0×08048529 será el printf. Si restamos las posiciones de memoria obtenemos el 7 empleado en el programa. Pero es más, fijaros bien, ¿en qué terminan las posiciones de memoria de estas dos líneas de código? En 22 y en 29 ¿Os suenan? Fijaos lo que mostraba el programa en su ejecución:
... Valor: 0x22 PosMem: 0xBF8690AC ... Premio: Valor: 0x29 PosMem: 0xBF8690AC
En la próxima entrada, una vez vistos los detalles técnicos y teóricos de los buffer overflow, pasaremos a comentar brevemente una de las principales protecciones del kernel de Linux frente a este tipo de ataques. Hasta entonces, buen fin de semana a todos.
Twitter! 