Lo stack cresce verso il basso: da intirizzi più alti a indirizzi più bassi.
Lo stack utilizza alcuni registri: stack pointer (esp) che punta sempre alla cima dello stack, base pointer (ebp), che punta sempre alla base dello stack. ebp è noto anche come frame pointer.
Quando si effettua una chiamata di funzione, si mettono sullo stack eventuali parametri da passare alla nuova funzione, si salva l'indirizzo di ritorno della funzione e si salva sullo stack il vecchio valore del frame pointer, poi si fa puntare il frame pointer allo stack pointer. A questo punto si estende lo stack pointer se la funzione ha bisogno di salvare altri dati nello stack. Per chiudere il frame di attivazione, si fa puntare lo stack pointer al frame pointer (chiudendo il frame), si ripristina il vecchio valore del frame pointer e si ritorna. In codice abbiamo
# apertura frame di attivazione
push ebp
mv ebp, esp
sub esp, n
# chiusura frame di attivazione
mv esp, ebp
pop ebp
ret
Gli attacchi di buffer overflow si verificano quando si alloca dello spazio nello stack (buffer) che viene riempito da chi utilizza il programma con lo scopo di accedere a zone contigue al buffer all'interno dello stack per modificare per esempio, il normale flusso del programma.
Alcune funzioni (per esempio in C: gets) potrebbero essere vulnerabili a buffer overflow, tuttavia anche pratiche scorrette dei programmatori potrebbero rendere vulnerabile il programma a buffer overflow.
int login(){
char[10] secret;
gets(secret);
if(strcmp(secret, "password00")){
return 1;
}else{ return -1; }
}
void loginOk(){...}
void loginFail(){...}
int main(){
if(login() == 1){
loginOk();
}else{ loginFail(); }
}
C
In questo esempio un malintenzionato potrebbe inserire tutti i caratteri che vuole, potrebbe riempire il buffer e accedere a zone della memoria contigue al buffer, per esempio l'indirizzo di ritorno, modificandolo e facendolo puntare ad un altro indirizzo, per esempio quello della funzione loginOk()
, riuscendo ad effettuare comunque l'accesso.
Se il buffer è abbastanza grande un attaccante potrebbe iniettare del codice nel buffer, riscrivere il return address per farlo puntare verso il suo codice, riuscendo ad eseguirlo.
int login(){
char[10] secret;
gets(secret);
if(strcmp(secret, "password00")){
return 1;
}else{ return -1; }
}
void loginOk(){...}
void loginFail(){...}
int main(){
/* CAMBIAMENTO */
char[400] buffer;
if(login() == 1){
loginOk();
}else{ loginFail(); }
}
C
Un attaccante potrebbe preparare del codice in linguaggio macchina (shellcode) arbitrario, modificare il return address, aggiungere delle operazioni NOP che farebbero scorrere l'instruction pointer fino all'istruzione successiva, fino a che non giunge all'inizio del codice malevolo.
Una variante di questo attacco (utile quando il buffer non è molto grande) è quella di copiare del codice arbitrario in linguaggio macchina in una variabile di ambiente, ottenere l'indirizzo di tale variabile e far puntare il return address ad essa.
Si noti che se vi fossero variabili sopra la definizione del buffer, tali variabili potrebbero essere modificate.
La memoria è organizzata in: