Alineación. Se permite al compilador colocar variables locales en la pila de manera que la alineación con las palabras de la CPU sea mejor. En otras palabras, no hay nada que obligue al compilador a colocar variables locales una tras otra en la pila. En realidad, la colocación de variables locales una tras otra estaría ahorrando un par de bytes en el tamaño del programa para una penalización considerablemente grande en el tiempo de procesamiento.
Permítame usar C simple en lugar de C ++ para explicar porque es mucho más fácil colocarlo en GDB. Programa convertido a C simple, llamemos pp.c
:
#include <stdio.h>
#include <string.h>
int main() {
int i = 20;
char var[4];
strcpy(var, "12345678901");
printf(" var is : %s\n", var);
printf(" i : %d\n", i);
return 0;
}
Tenga en cuenta que la cadena que usé es de la misma longitud:
"12345678901"
"biiiiiiiyee"
Vamos a compilarlo sin optimizaciones, por si acaso:
gcc -O0 -g -o pp pp.c
Y ahora veamos el programa en ejecución:
$ gdb -q pp
Reading symbols from pp...done.
(gdb) list
1 #include <stdio.h>
2 #include <string.h>
3
4 int main() {
5 int i = 20;
6 char var[4];
7
8 strcpy(var, "12345678901");
9 printf(" var is : %s\n", var);
10 printf(" i : %d\n", i);
(gdb) break 8
Breakpoint 1 at 0x4004fa: file pp.c, line 8.
(gdb) break 9
Breakpoint 2 at 0x400510: file pp.c, line 9.
(gdb) run
Starting program: /home/grochmal/tmp/pp
Breakpoint 1, main () at pp.c:8
8 strcpy(var, "12345678901");
OK, justo antes de que ocurra el desbordamiento, veamos dónde está la pila y dónde están las variables en la pila:
(gdb) p $rsp
$1 = (void *) 0x7fffffffe860
(gdb) x/16x 0x7fffffffe860
0x7fffffffe860: 0xffffe950 0x00007fff 0x00000000 0x00000014
0x7fffffffe870: 0x00400550 0x00000000 0xf7a5c291 0x00007fff
0x7fffffffe880: 0xf7dd0798 0x00007fff 0xffffe958 0x00007fff
0x7fffffffe890: 0xf7b9cc48 0x00000001 0x004004f6 0x00000000
(gdb) p &var
$2 = (char (*)[4]) 0x7fffffffe860
Muy bien, var
está realmente en la parte superior de la pila ( 0x7fffffffe860
) pero i
no es 4 bytes más tarde. i
está en 0x7fffffffe86c
, 12 bytes más tarde ( i
es 20, es decir, 0x00000014
). Veamos que pasa después:
(gdb) cont
Continuing.
Breakpoint 2, main () at pp.c:9
9 printf(" var is : %s\n", var);
(gdb) x/16x 0x7fffffffe860
0x7fffffffe860: 0x34333231 0x38373635 0x00313039 0x00000014
0x7fffffffe870: 0x00400550 0x00000000 0xf7a5c291 0x00007fff
0x7fffffffe880: 0xf7dd0798 0x00007fff 0xffffe958 0x00007fff
0x7fffffffe890: 0xf7b9cc48 0x00000001 0x004004f6 0x00000000
¡El búfer se desborda! Pero no lo suficiente, necesitamos 1 byte adicional (recuerde que 0x00
es el terminador nulo) para llegar al lugar en la memoria donde se encuentra i
. Si cambiamos nuestro strcpy
a:
strcpy(var, "AAAAAAAAAAAA\x39\x00");
Tienes que sobrescribir i
:
$ gcc -O0 -g -o pp pp.c
$ ./pp
var is : AAAAAAAAAAAA9
i : 1337
No es fácil adivinar la alineación, depende de la CPU y el compilador. La mayoría de los sombreros (blanco / gris / negro) lo hacen por prueba y error, o compilando el programa en el mismo entorno en el que se ejecuta y luego mirándolo en un depurador (como hicimos anteriormente).