
Outillage d'exploitation pour le débordement d'erreur ICMP PXE UEFI d'EDK II (CVE-2026-97631) : forge des paquets ICMP usurpés, implante un faux EFI_EVENT et vide la mémoire DXE via UART.
Outillage pour le débordement d'erreur ICMP du pilote EDK II PXE Base Code
(NetworkPkg/UefiPxeBcDxe, GHSA-99cj-7hg9-mp4m, CVSS 4.0 9.5).
Lors d'un démarrage PXE, le pilote copie un message d'erreur ICMP dans un tampon
fixe de 504 octets. Un message plus long devient une écriture linéaire au-delà de ce tampon, dans le
pool DXE. Comme la destination de l'écriture est l'adresse du tampon lui-même, la charge utile est
indépendante de la position : l'octet i du datagramme atterrit à &Mode->IcmpError + i.
Le débordement implante un EFI_EVENT contrefait ; le cœur DXE le distribue
via le chemin normal CoreSignalEvent ; la charge utile restaure l'événement à partir d'un
instantané, parcourt des plages mémoire choisies par l'opérateur et renvoie EFI_SUCCESS afin que le
démarrage se poursuive.
Le rayon d'action reste à l'intérieur de la mémoire DXE du firmware lui-même. Aucune écriture en flash, aucun mécanisme de persistance, aucun artefact du système d'exploitation.
include/pxe_layout.h EFI_PXE_BASE_CODE_ICMP_ERROR, IEVENT offsets, wire structs
include/craft.h one packet = one primitive
include/graft.h counterfeit event + payload parameter block
include/layout.h the overflow region (single source of truth)
src/payload.S ring-0 payload: restore, walk ranges, stream out
src/craft.c packet construction, IPv4 + IPv6
src/graft.c IEVENT overlay, parameter block
src/layout.c overflow assembly, shared by sender and simulator
src/main.c pxeinj: sniff xid, probe, send
src/net.c raw L3 transport, DHCP xid sniffer
src/recv.c pxerecv: serial collector, dump + map
sim/sim.c host harness that proves the chain
tools/fw_offsets.py pool offsets from a debug firmware build
Charge utile :
as --64 -o build/payload.o src/payload.S
ld -Ttext=0 -e payload_start --oformat elf64-x86-64 -o build/payload.elf build/payload.o
objcopy -O binary --only-section=.text build/payload.elf build/payload.bin
Le résultat fait 378 octets, indépendant de la position, sans relocations.
L'émetteur et le récepteur sont POSIX (AF_PACKET, IP_HDRINCL, termios) et se compilent
avec make sous Linux. Le simulateur se compile avec MSVC via Makefile.win :
cl /nologo /W3 /O2 /Iinclude /Fe:build\sim.exe sim\sim.c src\craft.c src\graft.c src\layout.c
build/sim.exe mappe une fausse page de pool, écrit le débordement là où le
memcpy vulnérable le ferait, vérifie que l'IEVENT résultant dispatcherait la charge utile, appelle
la charge utile en tant que code machine réel, puis vérifie :
26 vérifications portant sur la disposition, l'ABI, le framing et la construction des paquets. Cela prouve que les parties sont cohérentes entre elles. Cela ne prouve pas l'adresse de pool d'un firmware spécifique, qui est une allocation à l'exécution.
Deux modes, utilisés dans l'ordre.
Probe : chemin légal uniquement, aucune écriture hors limites.
pxeinj --target 10.0.0.50 --spoof 10.0.0.1 --sniff --probe --send
Une erreur ICMP bien formée de 504 octets. Même usurpation, même transaction BOOTP intégrée, tout est identique. Si le client continue de démarrer, l'erreur a été acceptée et la boucle de copie vulnérable a été atteinte. Confirmation non destructive, et elle ne nécessite aucune adresse de pool.
Overflow : refuse de se construire tant que trois éléments n'existent pas : la base du pool
(tools/fw_offsets.py), les octets d'événement intacts (--shadow), et au moins une
plage.
pxeinj --target 10.0.0.50 --spoof 10.0.0.1 --xid 0xdeadbeef \
--pool-base 0x7f12345000 --payload build/payload.bin \
--ranges ranges.txt --shadow shadow.bin --uart 0x3f8 --send
pxerecv -d /dev/ttyUSB0 -o dump.bin -m dump.map
ranges.txt contient une plage START-END par ligne, en hexadécimal sans 0x ; voir
ranges.example.txt pour la taille et le budget de temps. Le chemin de débordement v6 est
désactivé : la sonde fonctionne sur v6, l'écriture non, tant que la borne de copie v6 n'a pas
été redérivée à partir du code source corrigé.
EFI_SUCCESS sur le chemin de notification normal, donc le démarrage se poursuitSignalLink de l'événement est cohérent avec lui-même, donc le parcours de liste ne déclenche pas d'assertionLes traces résiduelles sont inhérentes au bug : l'événement a été signalé à un moment où il ne l'aurait normalement pas été, la page du pool reste sale, et si la plateforme mesure DXE avec un TPM, la valeur PCR est celle qu'elle avait au début du démarrage. Tout ce qui va au-delà n'est pas une propriété de ce bug.
--send est requis ; par défaut, c'est un dry run--pool-base, --payload, --shadow
et une liste de plages non vide. Chacun de ces éléments a une manière spécifique de corrompre
le firmware s'il est deviné, donc aucun n'est optionnel--shadow doit contenir une signature EFI_EVENT à l'offset attendu, ce qui
détecte un fichier rempli de zéros et un mauvais --event-off en une seule vérification plutôt qu'au
moment du dispatchMode->IcmpError est une allocation à l'exécution et diffère selon
la build du firmware. Elle ne peut pas être dérivée d'une link map. tools/fw_offsets.py
donne les offsets ; la base doit être mesurée sur le firmware cible, par
exemple à partir d'une trace DEBUG_POOL, puis mise en cache.sizeof(EFI_PXE_BASE_CODE_MODE) et offsetof(..., IcmpError) changent entre
les versions d'edk2. Les valeurs par défaut dans tools/fw_offsets.py sont des points de départ, pas
des valeurs à prendre pour acquises.DpcHandle
dans l'arborescence cible avant de vous fier à l'offset de débordement.CoreSignalEvent va parcourir. L'outil refuse de se construire
sans --shadow pour cette raison.