Per capire cosa sta succedendo, il punto importante è che in C, le funzioni non "sanno" quanti argomenti gli ha dato il chiamante. Per le normali funzioni, il compilatore rileva usi non validi. Ad esempio, se scrivi questo:
#include <string.h>
/* ... */
memcpy(a, b);
allora il compilatore si lamenterà rumorosamente e rifiuterà di completare la compilazione: l'intestazione inclusa <string.h>
dichiara la funzione memcpy()
come prendendo tre parametri (due puntatori e un intero di tipo size_t
). Se il compilatore vede una chiamata a memcpy()
con solo due parametri, la mancata corrispondenza lo farà urlare.
Tuttavia, alcune funzioni richiedono un numero variabile di parametri, ad es. %codice%. Per queste funzioni, il compilatore non può fare molto controllo. printf()
usa come primo argomento una "stringa di formato" il cui contenuto indica printf()
quanti altri argomenti dovrebbe trovare e il loro tipo previsto. Quando la stringa di formato è una costante letterale, i compilatori intelligenti possono eseguire alcuni controlli aggiuntivi (con printf()
, ciò avviene con " formato "attributo , un'estensione specifica per gcc). Ma non può fare nulla quando si ottiene la stringa di formato in fase di esecuzione. Immagina, ad esempio, il seguente codice:
printf(s);
dove gcc
è una stringa che l'utente malintenzionato può scegliere (ad esempio, alcuni dati inviati tramite la rete o un parametro di chiamata). Il programmatore ha appena assunto che la stringa fosse "semplice" (solo caratteri alfanumerici) ma l'autore dell'attacco può inserire segni " s
" al suo interno, che %
interpreta osservando parametri aggiuntivi. Non ci sono parametri extra, quindi printf()
guarderà effettivamente agli slot di memoria dove i parametri extra sarebbero stati , se ce ne fossero stati. Questi slot sono "più in basso nella pila" (tecnicamente, a indirizzi più alti, dal momento che gli stack "crescono" nella maggior parte delle architetture); le variabili locali, l'indirizzo di ritorno della funzione e altri elementi dello stack per la funzione corrente (e il relativo chiamante e il chiamante del chiamante e così via) sono trovati lì.
Usando printf()
specificatori, l'attaccante può leggere tutti questi slot ( %u
stamperà diligentemente il loro contenuto), e questo è già cattivo. Con il parametro printf()
, i poteri dell'attaccante sono addirittura migliorati: %n
prende il parametro successivo, interpreta come valore di puntatore, lo segue e scrive nell'indice a cui è indirizzato il numero corrente di caratteri finora (il numero di caratteri stampati da %n
fino a printf()
). Se la chiamata %n
errata si verifica in una funzione in cui è presente una variabile locale che è anch'essa sotto il controllo dell'utente malintenzionato (ad esempio un valore intero semplice), l'utente malintenzionato può creare printf()
per utilizzare tale variabile locale come puntatore e scrivi i dati a cui punta. In parole più brevi, questo dà all'aggressore la possibilità di scrivere qualunque byte desideri, ovunque voglia nella RAM. A quel punto, sei praticamente condannato.
Il codice corretto sarebbe stato:
printf("%s", s);
che ha forzato %n
a stampare la stringa controllata dall'attore "così com'è", senza fare nulla di speciale con i caratteri " printf()
".
Questa vulnerabilità %
mette in evidenza il fatto che C è un linguaggio piuttosto pericoloso: nessun controllo sui limiti, nessun controllo sui tipi forti ... puoi fare in modo che il programma guardi gli slot di memoria come se fossero parametri, mentre non lo sono, e anche interpretare i modelli di byte come se fossero dei puntatori, che non sono. I programmatori C competenti scriveranno il loro codice in modo da fornire al compilatore il maggior numero possibile di informazioni, in modo da rilevare gli errori (i buchi di sicurezza sono solo errori di programmazione con un effetto che può essere distorto a vantaggio dell'attaccante). Ma competenza è una risorsa scarsa, e anche i migliori programmatori a volte commettono errori.