Los 0x414141
y 0x424242
que se colocan en libros con fines educativos porque son simplemente las cadenas "AAAAAAAA" y "BBBBBBB" en notación hexadecimal. Asignarlos a enteros no tiene nada que ver con los desbordamientos de búfer.
Se puede obtener un ejemplo mucho mejor utilizando un programa completo y una llamada a la función que nos permitirá saber cuándo ocurre el desbordamiento. Por ejemplo, alteremos el programa para:
#include <stdio.h>
#include <unistd.h>
void
fun(void)
{
char buffer[10];
if (read(STDIN_FILENO, &buffer, 60) < 0) {
perror("read");
}
write(STDOUT_FILENO, "So long and thanks for all the fish.\n", 37);
return;
}
int
main(void)
{
fun();
write(STDOUT_FILENO, "WILL NOT PRINT.\n", 16);
return 0;
}
Ya que estás usando read
y write
estamos asumiendo un sistema UNIX, así que vamos a compilarlo y ejecutarlo un par de veces. Suponiendo que la fuente del programa está en buf.c
:
[~]$ gcc -g -o prog buf.c
[~]$ perl -e 'print "A"x10' | ./prog
So long and thanks for all the fish.
WILL NOT PRINT.
[~]$ perl -e 'print "A"x20' | ./prog
So long and thanks for all the fish.
WILL NOT PRINT.
Perl es útil porque podemos controlar exactamente la cantidad de bytes que le damos al programa. Como STDIN está conectado a una tubería, podemos proporcionar una cantidad variable de bytes, hasta los 60 bytes codificados en la fuente del programa.
La primera ejecución da lo que esperábamos, 10 bytes son lugares en el búfer de 10 bytes de longitud. En la segunda ejecución, el búfer de 10 bytes debe tener un desbordamiento con 20 bytes de entrada. Pero nada parece suceder. Bueno, el búfer se desbordó, pero no se desbordó lo suficiente como para dificultar la ejecución del proceso. Vamos a hacer más:
[grochmal@haps tmp]$ perl -e 'print "A"x30' | ./prog
So long and thanks for all the fish.
Segmentation fault (core dumped)
Bien, eso es lo que esperamos ver de un desbordamiento. Ahora que tenemos una buena información, la guardamos y vemos cómo funciona el programa en GDB:
[grochmal@haps tmp]$ perl -e 'print "A"x30' > input
[grochmal@haps tmp]$ gdb -q prog
Reading symbols from prog...done.
(gdb) list
5 fun(void)
6 {
7 char buffer[10];
8
9 if (read(STDIN_FILENO, &buffer, 30) < 0) {
10 perror("read");
11 }
12 write(STDOUT_FILENO, "So long and thanks for all the fish.\n", 37);
13 return;
14 }
(gdb) list
15
16 int
17 main(void)
18 {
19 fun();
20 write(STDOUT_FILENO, "WILL NOT PRINT.\n", 16);
21 return 0;
22 }
23
(gdb) break 9
Breakpoint 1 at 0x40058e: file buf.c, line 9.
(gdb) break 12
Breakpoint 2 at 0x4005b3: file buf.c, line 12.
Como agregamos -g
a la línea del compilador, tenemos todos los símbolos de depuración (por ejemplo, el código del programa) que podamos desear. Esto hace que sea muy fácil configurar los puntos de ruptura donde ocurre la parte interesante de la ejecución. Ejecutamos el programa con la entrada que acabamos de guardar y verificamos el aspecto del búfer:
(gdb) run <input
Starting program: /home/grochmal/tmp/prog <input
Breakpoint 1, fun () at buf.c:9
9 if (read(STDIN_FILENO, &buffer, 30) < 0) {
(gdb) p &buffer
$1 = (char (*)[10]) 0x7fffffffe800
(gdb) x/15wx 0x7fffffffe800
0x7fffffffe800: 0x004005f0 0x00000000 0x00400490 0x00000000
0x7fffffffe810: 0xffffe820 0x00007fff 0x004005d3 0x00000000
0x7fffffffe820: 0x004005f0 0x00000000 0xf7a5c291 0x00007fff
0x7fffffffe830: 0xf7dd0798 0x00007fff 0xffffe908
El búfer tiene solo 10 bytes de longitud, e imprimimos 60 bytes. Pero eso regala 0xffffe820
, que es la ubicación a la que el programa debe regresar (en main) desde la llamada fun()
. Cuando el búfer se desborda, los bytes sobrescriben eso. Vamos a verlo al continuar con la llamada read
.
(gdb) cont
Continuing.
Breakpoint 2, fun () at buf.c:12
12 write(STDOUT_FILENO, "So long and thanks for all the fish.\n", 37);
(gdb) x/15wx 0x7fffffffe800
0x7fffffffe800: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffe810: 0x41414141 0x41414141 0x41414141 0x00004141
0x7fffffffe820: 0x004005f0 0x00000000 0xf7a5c291 0x00007fff
0x7fffffffe830: 0xf7dd0798 0x00007fff 0xffffe908
Y ahora el programa intentará volver a 0x41414141
, lo que provocará la falta de seguridad.
(gdb) cont
Continuing.
So long and thanks for all the fish.
Program received signal SIGSEGV, Segmentation fault.
0x0000414141414141 in ?? ()
Y lo hizo! Como bono, tenemos la cadena 0x41414141
que está utilizando como una asignación int en su programa, y ahora puede deducir de dónde viene.
La implicación de seguridad de esto es que sé exactamente qué parte de la entrada sobrescribe ese valor de retorno en la pila. Y, gracias a eso, puedo dirigir el programa a cualquier instrucción dentro de su asignación de memoria.
(En la práctica, eso es mucho más difícil hoy en día porque existen varias medidas para evitar que esos desbordamientos encuentren lugares útiles a los que volver. Pero el concepto es el mismo.)