Il canary
Le vulnerabilità di tipo buffer overflow sullo stack sono molto comuni, per ciò per evitare di renderle facilmente sfruttabili negli anni si è sviluppata una mitigazione che cerca di limitarne i danni. L'idea di base è che in presenta di buffer overflow, un attaccante per poter corrompere l'indirizzo di ritorno della funzione deve sovrascrivere anche tutti i byte precedenti, quindi possiamo piazzare un valore "sentinella" pseudo-casuale (non conosciuto da un attaccante) prima dell'indirizzo di ritorno della funzione, e al termine della funzione controllare che questo valore non sia stato modificato. Se il programma si accorge che il valore è stato modificato, termina immediatamente l'esecuzione, rendendo vano ogni attacco. Il termine canary deriva dall'analogia di questa tecnica con i canarini utilizzati nelle miniere per l'identificazione di gas tossici.
Prendiamo come esempio il programma vulnerabile a buffer overflow visto nel capitolo precedente.
#include <stdio.h>
int main() {
char name[32];
scanf("%64s", name);
printf("ciao %s\n", name);
}
Se proviamo a compilarlo con il canary attivo (gcc
applica questa mitigazione per default) e proviamo a inserire un input lungo, non otteniamo più segmentation fault, ma otteniamo un altro errore.
$ ./main
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
ciao AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
*** stack smashing detected ***: terminated
[1] 2592424 IOT instruction (core dumped) ./main
In particolare il programma è terminato prima che venisse eseguita l'istruzione ret
con un valore di ritorno controllato dall'utente.
Possiamo andare a controllare il codice assembly del programma, e possiamo notare che sono state inserite nella funzione main
delle istruzioni all'inizio
1161: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28
1168: 00 00
116a: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax
e alla fine della funzione.
11ab: 48 8b 55 f8 mov rdx,QWORD PTR [rbp-0x8]
11af: 64 48 2b 14 25 28 00 sub rdx,QWORD PTR fs:0x28
11b6: 00 00
11b8: 74 05 je 11bf <main+0x66>
11ba: e8 71 fe ff ff call 1030 <__stack_chk_fail@plt>
11bf: c9 leave
11c0: c3 ret
Non è importante capire esattamente il codice, ma possiamo notare che all'inizio viene scritto sullo stack un valore (il canary), e poi alla fine prima di eseguire leave
e ret
viene letto lo stesso valore sullo stack e comparato con quello che è stato scritto all'inizio.
Se il valore del canary non corrisponde a quello che dovrebbe essere, viene chiamata la funzione __stack_chk_fail
, che stampa il messaggio di errore stack smashing detected
e termina il programma.
Questa mitigazione può essere aggirata tramite varie tecniche.
- Questa mitigazione si basa sul fatto che l'attaccante non può conoscere il valore del canary, ma potremmo avere una ulteriore vulnerabilità che ci permette di leggere valori arbitrariamente sullo stack. In questo caso sarebbe possibile leggere il valore del canary e quindi inserirlo nella giusta posizione in modo che il controllo non fallisca.
- Potremmo non essere in presenza di un buffer overflow, ma di una vulnerabilità che permette di scrivere ad offset arbitrari da un punto sullo stack, e se non vengono fatti i controlli adeguati potremmo scrivere direttamente dopo il canary sull'indirizzo di ritorno, lasciandolo invariato.