
Variante sintetica dell'overflow del buffer di stack CWE-120 di CVE-2020-8597 (pppd EAP) come target di analisi statica CodeQL
Un programma C deliberatamente vulnerabile di ~170 righe usato come target di
analisi statica CodeQL. Riproduce la classe di bug di CVE-2020-8597 — il
buffer overflow sullo stack di rhostname in pppd EAP (CWE-120) — in un
programma che non condivide nessuno dei nomi di funzione, della profondità di
chiamata o della struttura di dispatch di pppd.
L'obiettivo è un test di generalità: una query CodeQL scritta per catturare il bug di pppd deve attivarsi anche su questo programma, senza modifiche. Se lo fa, la query esprime la classe di bug piuttosto che la forma del codice originale.
Questo programma è intenzionalmente non sicuro ed esiste solo per l'analisi. Non distribuirlo. Il bug che rispecchia è pubblico (CVE-2020-8597, divulgato nel 2020).
Una lunghezza derivata dall'attaccante viene copiata in un buffer di dimensione fissa, senza alcuna guardia che metta in relazione quella lunghezza con la dimensione del buffer.
In ogni caso è presente un controllo dei limiti — semplicemente non riesce a mettere in relazione le due quantità che contano. Ci sono esattamente due modi per sbagliarlo, e il programma ne contiene uno per ciascuno:
sizeof(dest).
Previene una over-read, non fa nulla contro la over-write. (handle_hello)sizeof(dest) —
sembra esattamente un limite di buffer — ma vincola una variabile diversa da
quella usata come lunghezza della copia. (handle_stat)Il secondo è quello più difficile, ed è ciò che è il controllo morto di pppd
vallen >= len + sizeof(rhostname): un confronto che menziona la dimensione
della destinazione mentre vincola qualcosa che non è la lunghezza della copia.
Una query che si limita a chiedere "esiste qui qualche confronto che menziona
sizeof(dest)?" viene messa a tacere da esso.
| pppd / CVE-2020-8597 | questo progetto | |
|---|---|---|
| Sorgente | read() sul fd PPP | recvfrom() su un socket UDP |
| Dispatch | struct protent *protocols[] globale, match lineare sul numero di protocollo | const struct frame_op ops[] locale al file, match lineare sul tag di 1 byte |
| Profondità fino al sink | get_input → (*input) → eap_input → eap_request | dispatch_frame → (*handle) → handle_hello |
| Destinazione | char rhostname[256] | char name[64] |
| Guardia sbagliata | vallen limitato dal len del pacchetto | vlen limitato dal plen del frame |
Entrambi mantengono l'unica proprietà che rende questo un data-flow, non un grep: una chiamata indiretta attraverso una tabella di puntatori a funzione tra la sorgente e il sink.
| Handler | Riga | Controllo presente | Verdetto |
|---|---|---|---|
handle_hello() | sink a :80 | vlen > plen - 2 — valore giusto, limite sbagliato | deve attivarsi |
handle_echo() | copia a :106 | vlen >= sizeof(buf) — entrambi giusti | deve restare silenzioso — controllo negativo |
handle_stat() | sink a :145 | hlen >= sizeof(report) — limite giusto, valore sbagliato | deve attivarsi |
handle_stat è il caso discriminante. Il suo controllo nomina sizeof(report),
quindi una query che accetta qualsiasi confronto che menziona la dimensione
della destinazione lo tratta come protetto e manca il bug. Catturarlo richiede
confrontare il valore che viene controllato con il valore usato come
lunghezza della copia — global value numbering. Elimina questo dalla query e
questo handler diventa un falso negativo mentre ogni altro sito mantiene il suo
verdetto.
Un datagramma UDP = un frame:
[ type : 1 ] [ length : 2, big-endian ] [ value : length bytes ]
type 0x01 → hello, 0x02 → echo, 0x03 → stat. Un frame hello con una lunghezza
dichiarata tra 65 e ~2045 causa overflow di name[64]. Un frame stat trasporta
invece due lunghezze di un byte — una lunghezza di header e una lunghezza di
body — e qualsiasi lunghezza di body superiore a 32 causa overflow di
report[32], qualunque cosa dica la lunghezza dell'header.
make # gcc -Wall -Wextra -O0 -g -o tlv_server tlv_server.c
Linux/POSIX (socket BSD). Compila pulito senza warning.
CodeQL traccia una compilazione reale, quindi compila da pulito:
make clean
codeql database create db --language=cpp --command="make"
# oppure, senza lo step di clean:
codeql database create db --language=cpp --command="make -B"
Poi esegui la query della Parte 3 contro db; dovrebbe segnalare la memcpy in
handle_hello e quella in handle_stat, e restare silenziosa su handle_echo.
La query e le sue istruzioni di esecuzione si trovano in codeql/.
Il sorgente cambia ogni volta che viene aggiunto un handler, quindi ricostruisci
il database — CodeQL cattura un'istantanea del codice al momento di
database create e un db/ esistente non vedrà il nuovo codice.