@ La risposta di binarym è abbastanza buona. Spiega già le ragioni di un overflow del buffer, come puoi trovare un semplice overflow e come possiamo guardare allo stack usando un core file e / o GDB. Voglio solo aggiungere due dettagli extra:
- Un esempio di test della scatola nera più approfondito, cioè:
a description of how to consistently detect buffer overflows (black-box testing)
- Il quirk del compilatore, ovvero quando il testing della black-box fallisce (più o meno, è più come se un payload generato dalla black box potrebbe fallire).
Il codice che useremo è un po 'più complesso:
#include <stdio.h>
#include <string.h>
void do_post(void)
{
char curr = 0, message[128] = {};
int i = 0;
while (EOF != (curr = getchar())) {
if ('\n' == curr) {
message[i] = 0;
break;
} else {
message[i] = curr;
}
i++;
}
printf("I got your message, it is: %s\n", message);
return;
}
int main(void)
{
char curr = 0, request[8] = {};
int i = 0;
while (EOF != (curr = getchar())) {
request[i] = curr;
if (!strcmp(request, "GET\n")) {
printf("It's a GET!\n");
return 0;
} else if (!strcmp(request, "POST\n")) {
printf("It's a POST, get the message\n");
do_post();
return 0;
} else if (5 < strlen(request)) {
printf("Some rubbish\n");
return 1;
} /* else keep reading */
i++;
}
printf("Assertion error, THIS IS A BUG please report it\n");
return 0;
}
Mi sto prendendo gioco di HTTP con le richieste POST e GET. E sto usando getchar()
per leggere STDIN carattere per carattere (che è una cattiva implementazione ma è educativo). Il codice distingue tra GET, POST e "spazzatura" (qualsiasi altra cosa), e lo fa usando un ciclo più o meno correttamente scritto (senza overflow).
Tuttavia, durante l'analisi del messaggio POST c'è un overflow, nel buffer message[128]
. Sfortunatamente quel buffer è in profondità all'interno del programma (beh, non proprio così profondo ma una semplice lunga discussione non lo troverà). Facciamo la compilazione e proviamo le stringhe lunghe:
[~]$ gcc -O2 -o over over.c
[~]$ perl -e 'print "A"x2000' | ./over
Some rubbish
Sì, non funziona. Poiché conosciamo il codice, sappiamo che se aggiungiamo "POST \ n" all'inizio causeremo l'overflow. Ma cosa succede se non conosciamo il codice? O il codice è troppo complesso? Entra nel test black-box.
Test della scatola nera
La tecnica di collaudo della scatola nera più popolare è la fuzzing. Quasi tutte le altre tecniche (scatola nera) sono una variante di esso. Il fuzzing sta semplicemente alimentando l'input casuale del programma finché non troviamo qualcosa di interessante. Ho scritto un semplice script fuzzing per controllare questo programma, diamo un'occhiata:
#!/usr/bin/env python3
from itertools import product
from subprocess import Popen, PIPE, DEVNULL
prog = './over'
valid_returns = [ 0, 1 ]
all_chars = list(map(chr, range(256)))
# This assumes that we may find something with an input as small as 1024 bytes,
# which isn't realistic. In the real world several megabytes of need to be
# tried.
for input_size in range(1,1024):
input = [p for p in product(all_chars, repeat=input_size)]
for single_input in input:
child = Popen(prog, stdin=PIPE, stdout=DEVNULL)
byte_input = (''.join(single_input)).encode("utf-8")
child.communicate(input=byte_input)
child.stdin.close()
ret = child.wait()
if not ret in valid_returns:
print("INPUT", repr(byte_input), "RETURN", ret)
exit(0)
# The exit(0) is not realistic either, in the real world I'd like to have a
# full log of the entire search space.
Semplicemente: alimenta input casuali sempre più grandi al programma. (ATTENZIONE: lo script richiede una buona quantità di RAM) Eseguo questo e dopo alcune ore ottengo un risultato interessante:
INPUT b"POST\nXl_/.\xc3\x93\xc3\x90\xc2\x87\xc3\xa6dh\xc3\xaeH\xc2\xa0\xc2\x836\x16.\xc3\xb7\x1be\x1e,\xc3\x98\xc3\xa4\xc2\x81\xc2\x83 su\xc2\xb1\xc3\xb2\xc3\x8d^\xc2\xbc\xc2\xa11/\xc2\x9f\x12vY\x12[0\x0c]\xc3\xb6\x19zI\xc2\xb8\xc2\xb5\xc3\xbb\xc2\x9e\xc3\xab>^\xc2\x85\xc2\x91\xc2\xb5\xc2\xb5\xc3\xb6u\xc3\x8e).\xc3\xbcn\x1aM\xc3\xbb+{\x1c\xc3\x9a\xc3\x8b&\xc2\x93\xc2\xa1D\xc3\xad\xc3\xad\xc3\x81\xc2\xbd\xc2\x8d\xc2\xa3 \xc3\x87_\xc2\x82\xc3\x9asv\xc3\x92\xc2\x85IP\xc2\xb8\x1bS\xc3\xbe\xc3\x9e\\xc2\x8e\xc3\x9f\xc2\xb1\xc3\xa4\xc2\xbe\x1fue\xc3\x81\xc3\x8a\xc2\x8b'\xc3\xaf\xc2\xa1\xc3\x95'\xc2\xaa\xc3\xa8P\xc2\xa7\xc2\x8f\xc3\x99\xc2\x94S5\xc2\x83\xc3\x85U" RETURN -11
Il processo è scaduto -11, è un segfault? Vediamo:
kill -l | grep SIGSEGV
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
Va bene un errore di segmentazione (vedi questa risposta per chiarimento ). Ora ho un campione di input che posso usare per simulare questo segfault e scoprire (con GDB) dove è l'overflow.
I quirk del compilatore
Hai visto qualcosa di strano sopra? C'è un pezzo di informazione che ho omesso, ho usato un tag spoiler qui sotto in modo da poter tornare indietro e provare a capire. La risposta è qui:
Why the hell I used gcc -O2 -o over over.c
? Why a plain gcc -o over over.c
is not enough? What is so special about compiler optimisation (-O2
) in this context?
Per essere onesti, io stesso ho trovato sorprendente il fatto che ho potuto trovare questo comportamento in un programma così semplice. I compilatori riscrivono una buona parte del codice durante la compilazione, per motivi di prestazioni. Anche i compilatori cercano di attenuare diversi rischi (ad esempio, overflow chiaramente visibili). Spesso lo stesso codice può sembrare molto diverso con e senza l'ottimizzazione abilitata.
Diamo un'occhiata a questo specifico quirk, ma torniamo a perl dato che conosciamo già la vulnerabilità:
[~]$ gcc -O2 -o over over.c
[~]$ perl -e 'print "POST\n" . "A"x2000' | ./over
It's a POST, get the message
I got your message, it is: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAins
Segmentation fault (core dumped)
Sì, è esattamente quello che ci aspettavamo. Ma ora disabilitiamo l'ottimizzazione:
[~]$ gcc -o over over.c
[~]$ perl -e 'print "POST\n" . "A"x2000' | ./over
It's a POST, get the message
I got your message, it is: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAÿ}
$ echo $?
0
Che diavolo! Il compilatore è riuscito a correggere la vulnerabilità che ho creato con così tanto amore. Se osservi la lunghezza di quel messaggio, vedrai che è lunga 141 byte. Il buffer ha avuto un overflow, ma il compilatore ha aggiunto una sorta di assembly per fermare le scritture nel caso in cui l'overflow raggiungesse qualcosa di importante.
Per gli scettici, ecco la versione del compilatore che sto usando per ottenere il comportamento sopra:
[~]$ gcc --version
gcc (GCC) 6.2.1 20160830
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
La morale della storia è che la maggior parte delle vulnerabilità di buffer overflow funzionano solo con lo stesso carico utile se compilate dallo stesso compilatore e con la stessa ottimizzazione (o anche altri parametri). I compilatori fanno cose cattive al tuo codice per farlo funzionare più velocemente, e anche se ci sono buone probabilità che un payload funzioni sullo stesso programma compilato da due compilatori, non è sempre vero.
Postscript
Ho fatto questa risposta per divertirmi e per tenere un registro per me stesso. Non merito la ricompensa perché non rispondo completamente alla tua domanda, rispondo solo alla domanda aggiuntiva aggiunta nella definizione di generosità. La risposta di bynarym merita la ricompensa perché risponde a più parti della domanda originale.