Lo stack dell'architettura IA 32
...

  • 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
    

Attacchi di buffer overflow
...

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.

Riscrittura indirizzo di ritorno
...

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.

Riscrittura indirizzo di ritorno ed esecuzione di codice arbitrario
...

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.

NOTE
...

Si noti che se vi fossero variabili sopra la definizione del buffer, tali variabili potrebbero essere modificate.

Contromisure
...

  • Attivare ASLR, Address Space Layout Randomization, questo randomizza lo spazio di indirizzamento di alcune aree di memoria importanti del programma (stack per esempio) rendendo più difficile per l'attaccante stabilire in quale punto della memoria si trova lo stack.
  • Rendere lo stack non eseguibile, questa funzione è resa disponibile dalle moderne CPU, il SO può marcare alcune aree di memoria come non eseguibili, non rendendo possibile l'esecuzione di codice arbitrario nello stack.
  • Canary: i compilatori si occupano di tradurre il codice, il che dà un certo controllo sullo stack. Quando viene allocata una funzione, il compilatore inserisce dei valori casuali di controllo (canary) sotto il return address, il valore originale di esso viene salvato in un posto sicuro, non soggetto a manipolazioni. Nel momento in cui si verifica buffer overflow, i valori di canary vengono anch'essi sovrascritti, un controllo sul canary, prima di ritornare, consente di capire se lo stack è stato compromesso o no.
  • Utilizzo di funzioni non vulnerabili.
  • Attenzione dei programmatori.

Organizzazione memoria IA
...

La memoria è organizzata in:

  • stack: contiene tutto ciò che serve per gestire le funzioni come il return address, gli argomenti e le variabili definite all'interno di esse;
  • heap: utilizzato per allocazioni dinamiche della memoria;
  • segmento BSS: contiene variabili non inizializzate dal programmatore:
  • segmento dei dati: contiene variabili inizializzate dal programmatore
  • segmento del testo: contiene il codice da eseguire;