
Échec de copie : 732 octets vers root sur chaque grande distribution Linux.
732 octets. Toute distribution. Root.
Une faille logique linéaire dans le modèle cryptographique
authencesndu noyau Linux permet à un utilisateur local non privilégié d'effectuer une écriture précise et contrôlée de 4 octets dans le cache de pages de n'importe quel fichier lisible — y compris les binaires setuid. Aucune course. Aucune nouvelle tentative. Aucune recompilation. Root sur toutes les principales distributions Linux publiées depuis 2017.
📄 Analyse technique · 🔗 Correctif du noyau · 🛡️ CVSS : Critique
| Distribution | Version du noyau |
|---|---|
| Ubuntu 24.04 LTS | 6.17.0-1007-aws |
| Amazon Linux 2023 | 6.18.8-9.213.amzn2023 |
| RHEL 10.1 | 6.12.0-124.45.1.el10_1 |
| SUSE 16 | 6.12.0-160000.9-default |
Les quatre ont été compromises avec le même script Python de 732 octets, sans modification.
| Propriété | Détail |
|---|---|
| Déterministe | Faille logique linéaire — aucune condition de course, aucune fenêtre temporelle, aucune nouvelle tentative |
| Portable | Même script, mêmes octets, fonctionne sur toutes les distributions et architectures testées |
| Minuscule | Script Python de 732 octets utilisant uniquement la bibliothèque standard (os, socket, zlib). Nécessite Python 3.10+ pour os.splice |
| Furtive | La page corrompue n'est jamais marquée comme sale. Les sommes de contrôle sur disque sont inchangées ; seule la mémoire cache de pages est modifiée |
| Trans-conteneur | Le cache de pages est partagé à l'échelle du système au-delà des frontières des conteneurs — c'est également une primitive d'évasion de nœud Kubernetes (voir Partie 2) |
AF_ALG expose le sous-système cryptographique du noyau à l'espace utilisateur non privilégié. splice() transfère les données d'un fichier dans un tube par référence — en transmettant directement les pages du cache de pages, sans copie. Lorsqu'un utilisateur épisse un fichier dans une socket AEAD AF_ALG, la liste scatterlist d'entrée de la socket contient des références vivantes aux pages mises en cache par le noyau de ce fichier.
Dans algif_aead.c, l'optimisation en place de 2017 copiait l'AAD et le texte chiffré de la liste scatterlist TX vers le tampon RX, mais chaînait les pages du tag d'authentification par référence à l'aide de sg_chain(), puis définissait req->src = req->dst :
SGL d'entrée : [ AAD | CT | Tag ]
^
└─ sg_chain() → pointe toujours vers les pages du cache de pages
SGL de sortie : [ AAD | CT ] ──→ [ Tag (pages du cache de pages) ]
(tampon RX) (chaîné depuis le SGL TX)
req->src ──┐
├──→ même liste scatterlist combinée
req->dst ──┘
Les pages du cache de pages issues de splice() se retrouvaient désormais dans une liste scatterlist de destination inscriptible, séparées de la zone d'écriture légitime par une simple limite d'offset. Rien dans l'API n'imposait que les algorithmes restent dans les limites.
authencesnauthencesn est un wrapper AEAD utilisé par IPsec pour la prise en charge des Extended Sequence Numbers (ESN) 64 bits. Pour réorganiser les octets ESN pour le calcul HMAC, il utilise le tampon de destination de l'appelant comme espace de travail — y compris une écriture à l'offset assoclen + cryptlen, qui se situe au-delà de la limite du tag d'authentification :
scatterwalk_map_and_copy(tmp, dst, 0, 8, 0); // lit AAD[0..7]
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1); // écrase dst[4..7]
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); // ← écrit au-delà du tag
Le troisième appel écrit 4 octets (seqno_lo) à dst[assoclen + cryptlen]. Dans le chemin AF_ALG en place, le scatterwalk traverse le tampon RX pour atteindre les pages du tag du cache de pages chaînées. Le noyau mappe la page du cache de pages via kmap_local_page et écrit directement dans la copie mise en cache du fichier cible.
Le HMAC échoue ensuite (le texte chiffré est fabriqué), recvmsg() renvoie une erreur — mais l'écriture de 4 octets persiste définitivement.
| Variable | Contrôlée via |
|---|---|
| Fichier cible | Tout fichier lisible par l'utilisateur actuel |
| Offset d'écriture | assoclen, offset de splice et longueur de splice |
| Valeur d'écriture | Octets 4–7 de l'AAD fourni dans sendmsg() (seqno_lo) |
| Année | Événement |
|---|---|
| 2011 | authencesn ajouté au noyau (a5079d084f8b) pour la prise en charge ESN d'IPsec. L'écriture de travail existait mais était inoffensive — seule la couche interne xfrm l'appelait, et l'AAD se trouvait dans une liste scatterlist séparée. |
| 2015 | AF_ALG gagne la prise en charge AEAD. authencesn converti à la nouvelle interface AEAD (104880a6b470), introduisant l'offset d'écriture assoclen + cryptlen. Toujours hors place : les pages du cache de pages étaient dans src (lecture seule). Pas encore exploitable. |
| 2017 | Optimisation en place ajoutée à algif_aead.c (72548b093ee3). req->src = req->dst. Les pages du tag du cache de pages chaînées dans la destination inscriptible. Vulnérabilité formée. |
| 2026-03-23 | Signalée à l'équipe de sécurité du noyau Linux. |
| 2026-04-01 | Correctif fusionné dans la branche principale. |
| 2026-04-22 | CVE-2026-31431 attribuée. |
| 2026-04-29 | Divulgation publique. |
Aucune modification individuelle n'était erronée. La vulnérabilité réside à l'intersection des trois.
La cible par défaut est /usr/bin/su, un binaire setuid-root présent sur toutes les distributions testées.
Étape 1 — Configuration de la socket
Ouvrir une socket AF_ALG, lier à authencesn(hmac(sha256),cbc(aes))
Définir la clé. Accepter la socket de requête. (Aucun privilège requis.)
Étape 2 — Boucle d'écriture (une fois par bloc de shellcode de 4 octets)
sendmsg() → les octets AAD [4:8] transportent les 4 octets à écrire (seqno_lo)
splice() → les pages du cache de pages du fichier cible dans la socket AF_ALG
recv() → déclenche le déchiffrement → authencesn écrit seqno_lo dans le cache de pages
(recvmsg renvoie une erreur ; l'écriture persiste)
Étape 3 — Exécution
execve("/usr/bin/su")
Le noyau charge le binaire depuis le cache de pages (désormais corrompu)
Le binaire setuid-root exécute le shellcode injecté → UID 0
a = socket.socket(38, 5, 0) # AF_ALG, SOCK_SEQPACKET
a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
# ... définir la clé, accepter la socket de requête u ...
u.sendmsg([b"A"*4 + payload_chunk], [cmsg_headers], MSG_MORE)
os.splice(target_fd, pipe_wr, offset)
os.splice(pipe_rd, alg_fd, offset)
u.recv(...) # déclenche l'écriture dans le cache de pages