
Variante synthétique de débordement de tampon de pile CWE-120 de CVE-2020-8597 (pppd EAP) comme cible d'analyse statique CodeQL
Un programme C délibérément vulnérable d'environ 170 lignes, utilisé comme cible
d'analyse statique CodeQL. Il reproduit la classe de bug de CVE-2020-8597 —
le débordement de tampon sur la pile de rhostname dans l'EAP de pppd (CWE-120) —
dans un programme qui ne partage aucun nom de fonction, aucune profondeur d'appel,
ni aucune structure de dispatch avec pppd.
L'objectif est un test de généralité : une requête CodeQL écrite pour détecter le bug de pppd doit également se déclencher sur ce programme, sans modification. Si c'est le cas, la requête exprime la classe de bug plutôt que la forme du code d'origine.
Ce programme est intentionnellement dangereux et n'existe que pour l'analyse. Ne le déployez pas. Le bug qu'il reproduit est public (CVE-2020-8597, divulgué en 2020).
Une longueur dérivée d'une entrée attaquante est copiée dans un tampon de taille fixe, sans garde reliant cette longueur à la taille du tampon.
Dans chaque cas, une vérification de bornes est présente — elle échoue simplement à relier les deux quantités qui comptent. Il existe exactement deux façons de se tromper, et le programme contient un exemple de chacune :
sizeof(dest). Empêche une lecture hors
limites, ne fait rien contre l'écriture hors limites. (handle_hello)sizeof(dest) — elle
ressemble exactement à une borne de tampon — mais contraint une variable
différente de celle utilisée comme longueur de copie. (handle_stat)La seconde est la plus difficile, et c'est ce qu'est la vérification morte de pppd
vallen >= len + sizeof(rhostname) : une comparaison qui mentionne la taille de
destination tout en contraignant quelque chose qui n'est pas la longueur de copie.
Une requête qui demande seulement « une comparaison ici mentionne-t-elle
sizeof(dest) ? » est réduite au silence par elle.
| pppd / CVE-2020-8597 | ce projet | |
|---|---|---|
| Source | read() sur le fd PPP | recvfrom() sur un socket UDP |
| Dispatch | struct protent *protocols[] global, correspondance linéaire sur le n° de protocole | const struct frame_op ops[] local au fichier, correspondance linéaire sur un tag de 1 octet |
| Profondeur jusqu'au sink | get_input → (*input) → eap_input → eap_request | dispatch_frame → (*handle) → handle_hello |
| Destination | char rhostname[256] | char name[64] |
| Mauvaise garde | vallen borné par le len du paquet | vlen borné par le plen de la trame |
Les deux conservent la propriété qui en fait un flux de données, et non un grep : un appel indirect via une table de pointeurs de fonctions entre la source et le sink.
| Handler | Ligne | Vérification présente | Verdict |
|---|---|---|---|
handle_hello() | sink à :80 | vlen > plen - 2 — bonne valeur, mauvaise borne | doit se déclencher |
handle_echo() | copie à :106 | vlen >= sizeof(buf) — les deux corrects | doit rester silencieux — contrôle négatif |
handle_stat() | sink à :145 | hlen >= sizeof(report) — bonne borne, mauvaise valeur | doit se déclencher |
handle_stat est le cas discriminant. Sa vérification nomme sizeof(report), donc
une requête qui accepte toute comparaison mentionnant la taille de destination le
traite comme protégé et manque le bug. Pour l'attraper, il faut comparer la valeur
vérifiée à la valeur utilisée comme longueur de copie — la numérotation globale
des valeurs. Supprimez cela de la requête et ce handler devient un faux négatif
tandis que tous les autres sites conservent leur verdict.
Un datagramme UDP = une trame :
[ type : 1 ] [ length : 2, big-endian ] [ value : length bytes ]
type 0x01 → hello, 0x02 → echo, 0x03 → stat. Une trame hello avec une longueur
déclarée entre 65 et ~2045 déborde name[64]. Une trame stat transporte à la place
deux longueurs d'un octet — une longueur d'en-tête et une longueur de corps — et
toute longueur de corps supérieure à 32 déborde report[32], quelle que soit la
longueur d'en-tête.
make # gcc -Wall -Wextra -O0 -g -o tlv_server tlv_server.c
Linux/POSIX (sockets BSD). Compile proprement sans avertissement.
CodeQL trace une compilation réelle, donc compilez à partir d'un état propre :
make clean
codeql database create db --language=cpp --command="make"
# ou, sans l'étape de nettoyage :
codeql database create db --language=cpp --command="make -B"
Exécutez ensuite la requête de la Partie 3 contre db ; elle doit signaler le
memcpy dans handle_hello et celui dans handle_stat, et rester silencieuse sur
handle_echo. La requête et ses instructions d'exécution se trouvent dans
codeql/.
Le source change à chaque ajout d'un handler, donc reconstruisez la base de
données — CodeQL capture un instantané du code au moment de database create et
un db/ existant ne verra pas le nouveau code.