Alignment. Il compilatore può posizionare variabili locali nello stack in modo che l'allineamento alle parole della CPU sia migliore. In altre parole, non c'è nulla che costringa il compilatore a posizionare le variabili locali una dopo l'altra sullo stack. Posizionare le variabili locali una dopo l'altra significa salvare un paio di byte nella dimensione del programma per una penalità considerevole nel tempo di elaborazione.
Consentitemi di usare la C normale anziché il C ++ per spiegare perché è molto più facile rilasciarlo in GDB. Programma convertito in semplice C, chiamiamo è 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;
}
Si noti che la stringa che ho usato è della stessa lunghezza:
"12345678901"
"biiiiiiiyee"
Compiliamolo senza ottimizzazioni, nel caso in cui:
gcc -O0 -g -o pp pp.c
E ora diamo un'occhiata al programma in esecuzione:
$ 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 siamo appena prima che si verifichi l'overflow, vediamo dove si trova lo stack e dove sono le variabili nello stack:
(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
Molto buono, var
è in cima alla pila ( 0x7fffffffe860
) ma i
non è più 4 byte più tardi. i
è a 0x7fffffffe86c
, 12 byte dopo ( i
è 20, cioè 0x00000014
). Vediamo cosa succede dopo:
(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
Il buffer fa overflow! Ma non abbastanza, abbiamo bisogno di 1 byte extra (ricordiamo che 0x00
è il terminatore null) per raggiungere il posto in memoria dove si trova i
. Se modifichiamo strcpy
in:
strcpy(var, "AAAAAAAAAAAA\x39\x00");
Si arriva a sovrascrivere i
:
$ gcc -O0 -g -o pp pp.c
$ ./pp
var is : AAAAAAAAAAAA9
i : 1337
Non è facile indovinare l'allineamento, dipende dalla CPU e dal compilatore. La maggior parte dei cappelli (bianchi / grigi / neri) lo fanno per tentativi ed errori, o compilando il programma nello stesso ambiente in cui viene eseguito e poi guardandolo in un debugger (come abbiamo fatto sopra).