
Synthetische Variante eines CWE-120-Stack-Pufferüberlaufs von CVE-2020-8597 (pppd EAP) als Ziel für die statische CodeQL-Analyse
Ein absichtlich verwundbares C-Programm mit ~170 Zeilen, das als CodeQL-Statische-Analyse-Ziel dient. Es reproduziert die Bug-Klasse von CVE-2020-8597 — den pppd-EAP-rhostname-Stack-Buffer-Overflow (CWE-120) — in einem Programm, das keine der Funktionsnamen, Aufruftiefen oder Dispatch-Strukturen von pppd teilt.
Das Ziel ist ein Allgemeinheitstest: Eine CodeQL-Abfrage, die geschrieben wurde, um den pppd-Bug zu erkennen, muss auch bei diesem Programm anschlagen, unverändert. Wenn sie das tut, drückt die Abfrage die Bug-Klasse aus und nicht die Form des ursprünglichen Codes.
Dieses Programm ist absichtlich unsicher und existiert nur zur Analyse. Nicht einsetzen. Der Bug, den es spiegelt, ist öffentlich (CVE-2020-8597, offengelegt 2020).
Eine vom Angreifer stammende Länge wird in einen Puffer fester Größe kopiert, ohne Schutz, der diese Länge mit der Größe des Puffers in Beziehung setzt.
In jedem Fall ist eine Grenzprüfung vorhanden — sie setzt nur die beiden Größen, die zählen, nicht in Beziehung zueinander. Es gibt genau zwei Arten, das falsch zu machen, und das Programm enthält je eine davon:
sizeof(dest). Verhindert ein Über-Lesen, tut nichts gegen das Über-Schreiben. (handle_hello)sizeof(dest) — sie sieht genau wie eine Puffergrenze aus — beschränkt aber eine andere Variable als die, die als Kopierlänge verwendet wird. (handle_stat)Die zweite ist die schwierigere, und sie ist das, was pppds tote vallen >= len + sizeof(rhostname)-Prüfung ist: ein Vergleich, der die Zielgröße erwähnt, während er etwas einschränkt, das nicht die Kopierlänge ist. Eine Abfrage, die nur fragt „erwähnt hier irgendein Vergleich sizeof(dest)?", wird dadurch zum Schweigen gebracht.
| pppd / CVE-2020-8597 | dieses Projekt | |
|---|---|---|
| Quelle | read() auf dem PPP-fd | recvfrom() auf einem UDP-Socket |
| Dispatch | globales struct protent *protocols[], linearer Abgleich auf Protokollnr. | dateilokales const struct frame_op ops[], linearer Abgleich auf 1-Byte-Tag |
| Tiefe bis zur Senke | get_input → (*input) → eap_input → eap_request | dispatch_frame → (*handle) → handle_hello |
| Ziel | char rhostname[256] | char name[64] |
| Falscher Schutz | vallen durch Paket-len beschränkt | vlen durch Frame-plen beschränkt |
Beide behalten die eine Eigenschaft, die dies zu Datenfluss statt grep macht: einen indirekten Aufruf über eine Funktionszeigertabelle zwischen Quelle und Senke.
| Handler | Zeile | Vorhandene Prüfung | Urteil |
|---|---|---|---|
handle_hello() | Senke bei :80 | vlen > plen - 2 — richtiger Wert, falsche Grenze | muss anschlagen |
handle_echo() | Kopie bei :106 | vlen >= sizeof(buf) — beides richtig | muss still bleiben — Negativkontrolle |
handle_stat() | Senke bei :145 | hlen >= sizeof(report) — richtige Grenze, falscher Wert | muss anschlagen |
handle_stat ist der entscheidende Fall. Seine Prüfung nennt sizeof(report), sodass eine Abfrage, die jeden Vergleich akzeptiert, der die Zielgröße erwähnt, ihn als geschützt behandelt und den Bug verfehlt. Um ihn zu erfassen, muss der Wert, der geprüft wird, mit dem Wert, der als Kopierlänge verwendet wird, verglichen werden — globale Wertnummerierung. Streicht man das aus der Abfrage, wird dieser Handler zu einem falsch-negativen Ergebnis, während jede andere Stelle ihr Urteil behält.
Ein UDP-Datagramm = ein Frame:
[ type : 1 ] [ length : 2, big-endian ] [ value : length bytes ]
type 0x01 → hello, 0x02 → echo, 0x03 → stat. Ein Hello-Frame mit einer deklarierten Länge zwischen 65 und ~2045 überläuft name[64]. Ein Stat-Frame trägt stattdessen zwei Ein-Byte-Längen — eine Header-Länge und eine Body-Länge — und jede Body-Länge über 32 überläuft report[32], egal was die Header-Länge sagt.
make # gcc -Wall -Wextra -O0 -g -o tlv_server tlv_server.c
Linux/POSIX (BSD-Sockets). Baut sauber ohne Warnungen.
CodeQL verfolgt einen echten Kompilierungsvorgang, also aus dem sauberen Zustand bauen:
make clean
codeql database create db --language=cpp --command="make"
# oder, ohne den Clean-Schritt:
codeql database create db --language=cpp --command="make -B"
Führen Sie dann die Part-3-Abfrage gegen db aus; sie sollte das memcpy in handle_hello und das in handle_stat melden und bei handle_echo still bleiben. Die Abfrage und ihre Ausführungsanweisungen befinden sich in codeql/.
Der Quellcode ändert sich, wann immer ein Handler hinzugefügt wird, also bauen Sie die Datenbank neu — CodeQL erstellt einen Schnappschuss des Codes zum Zeitpunkt von database create, und eine bestehende db/ wird neuen Code nicht sehen.