0x414141
e 0x424242
che vengono inseriti nei libri a scopo didattico perché si tratta semplicemente delle stringhe "AAAAAAAA" e "BBBBBBB" in notazione esadecimale. Assegnarli a numeri interi non ha nulla a che fare con i buffer overflow.
Un esempio molto migliore può essere ottenuto utilizzando un programma completo e una chiamata di funzione che ci permetterà di sapere quando avviene l'overflow. Ad esempio, cambiamo il programma in:
#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;
}
Dato che stai usando read
e write
stiamo assumendo un sistema UNIX, quindi compiliamolo ed esegui un paio di volte. Supponendo che la sorgente del programma sia in 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 è utile perché possiamo controllare esattamente il numero di byte che forniamo al programma. Poiché STDIN è collegato a una pipe, possiamo fornire una quantità variabile di byte, fino ai 60 byte codificati nell'origine del programma.
La prima esecuzione dà quello che ci aspettavamo, 10 byte sono posti nel buffer lungo 10 byte. Nella seconda esecuzione il buffer da 10 byte dovrebbe avere un overflow con 20 byte di input. Ma nulla sembra accadere. Bene, il buffer ha avuto un overflow, ma non ha esagerato abbastanza da ostacolare l'esecuzione del processo. Facciamo di più:
[grochmal@haps tmp]$ perl -e 'print "A"x30' | ./prog
So long and thanks for all the fish.
Segmentation fault (core dumped)
Bene, questo è ciò che ci aspettiamo di vedere da un eccesso. Ora che abbiamo un buon input, lo salviamo e vediamo come funziona il programma in 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.
Da quando abbiamo aggiunto -g
alla riga del compilatore abbiamo tutti i simboli di debug (ad esempio il codice del programma) che potremmo volere. Ciò rende molto facile impostare i punti di interruzione in cui avviene la parte interessante dell'esecuzione. Eseguiamo il programma con l'input che abbiamo appena salvato e controlliamo come appare il buffer:
(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
Il buffer ha una lunghezza di soli 10 byte e abbiamo stampato 60 byte. Ma questo lascia fuori 0xffffe820
, che è la posizione in cui il programma deve tornare (in main) dalla chiamata fun()
. Quando il buffer viene sovraccaricato, i byte lo sovrascrivono. Vediamolo continuando con la chiamata 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
E ora il programma proverà a tornare a 0x41414141
, il che causerà il segfault.
(gdb) cont
Continuing.
So long and thanks for all the fish.
Program received signal SIGSEGV, Segmentation fault.
0x0000414141414141 in ?? ()
E lo ha fatto! Come bonus abbiamo la stringa 0x41414141
che stai utilizzando come assegnazione int nel tuo programma, e ora puoi dedurre da dove proviene.
La sicurezza implicita di questo è che so esattamente quale parte dell'ingresso sovrascrive quel valore di ritorno sullo stack. E grazie a ciò, posso guidare il programma a qualsiasi istruzione all'interno della sua allocazione di memoria.
(In pratica è molto più difficile oggi perché ci sono parecchie misure per impedire a tali overflow di trovare luoghi utili a cui tornare. Ma il concetto è lo stesso.)