
Preuve de concept d'exploitation pour CVE-2026-31431, une élévation de privilèges locale du noyau Linux via splice() et des options de socket malveillantes, permettant une écriture arbitraire dans la mémoire du noyau afin d'écraser les binaires setuid.
copyFail.py est un exploit de preuve de concept (PoC) qui démontre une vulnérabilité d'élévation de privilèges locale dans le noyau Linux. En abusant de l'appel système splice() combiné à des options de socket savamment conçues, l'exploit obtient la capacité de corrompre la mémoire arbitraire du noyau. Il utilise cette capacité pour écraser n'importe quel binaire setuid sur le disque avec un exécutable ELF personnalisé, puis exécute le binaire falsifié pour obtenir des privilèges élevés.
Type d'attaque : Élévation de privilèges locale (LPE) Impact : Accès complet à un shell root (en ciblant les binaires setuid-root) Complexité : Faible (script unique, aucune dépendance externe)
Utilisation :
./copyFail_cleaned.py [binaire_cible] # par défaut : /usr/bin/su
Le PoC original codait en dur /usr/bin/su, mais la vulnérabilité sous-jacente du noyau s'applique à n'importe quel fichier — l'attaquant peut écraser n'importe quel binaire setuid de son choix. La version nettoyée accepte un argument de ligne de commande optionnel pour spécifier la cible, rendant ce comportement explicite.
Le noyau Linux fournit un appel système splice() qui copie des données entre deux descripteurs de fichiers entièrement dans le noyau — sans copier les données dans l'espace utilisateur au préalable. C'est ce qu'on appelle les « E/S zéro-copie » et cela est utilisé pour les performances dans les applications à haut débit comme les serveurs web et les proxys inverses.
La vulnérabilité provient du fait que le noyau ne valide pas correctement l'état interne de certaines structures de socket lorsque splice() est utilisé sur des sockets PACKET configurées avec des options de socket de niveau protocole non prises en charge. En concevant soigneusement ces options, un attaquant peut corrompre des pointeurs du noyau, puis utiliser splice() pour rediriger les écritures mémoire du noyau vers des emplacements arbitraires.
En termes simples : le noyau dispose d'un raccourci pour déplacer des données en interne, et cet exploit trompe ce raccourci pour écrire des données au mauvais endroit — plus précisément, dans la copie en mémoire d'un binaire système.
L'exploit suit une séquence claire d'étapes :
L'exploit crée un socket PACKET brut (AF_PACKET / SOCK_RAW) et le lie à des adresses intentionnellement malformées. Il appelle ensuite setsockopt() en utilisant SOL_PNIO (niveau de protocole 279), une constante de niveau Solaris que Linux ne reconnaît pas.
Pourquoi c'est important : Lorsque le noyau rencontre un niveau de protocole non pris en charge dans setsockopt(), il retombe sur un gestionnaire générique qui ne valide pas correctement les données. Cela laisse la mémoire du noyau dans un état incohérent et exploitable.
Après avoir établi une connexion socket via accept(), l'exploit envoie des messages de contrôle (ancillaires) conçus avec soin en utilisant sendmsg(). Ces messages ont des en-têtes de longueur volontairement incohérents — certains prétendent être plus courts qu'ils ne le sont, d'autres plus longs.
Pourquoi c'est important : L'analyseur de messages de contrôle du noyau utilise ces longueurs pour l'arithmétique des pointeurs. Des longueurs incohérentes amènent l'analyseur à lire ou écrire au-delà des limites prévues, corrompant les structures adjacentes du noyau — en particulier, les pointeurs que splice() suivra ensuite.
L'exploit crée un tube et appelle splice() deux fois :
/usr/bin/su dans le tube.Pourquoi c'est important : L'appel splice() suit les pointeurs du noyau corrompus implantés à l'étape 2, ce qui l'amène à écrire des données vers une adresse mémoire du noyau choisie par l'attaquant au lieu du tampon du socket. Cela donne à l'exploit une primitive d'écriture mémoire arbitraire dans le noyau.
La charge utile de l'exploit est un exécutable ELF de 160 octets (voir ci-dessous). Chaque appel à la fonction d'exploit écrit exactement 4 octets. La boucle principale s'exécute donc 40 fois (160 / 4 = 40), à chaque fois :
Après les 40 itérations, les 160 premiers octets de /usr/bin/su ont été écrasés.
Enfin, l'exploit exécute os.system("su"). Le noyau charge le /usr/bin/su modifié depuis son cache de pages (qui contient désormais le shellcode de l'attaquant au lieu du vrai binaire su), et le shellcode s'exécute.
La charge utile intégrée se décompresse en un exécutable ELF x86-64 de 160 octets contenant ce shellcode :
; Tentative de l'appel système 105 (execveat) — peut ne pas être disponible sur les noyaux plus anciens
xor eax, eax
xor edi, edi
mov al, 0x69 ; appel système 105
syscall
; Repli : appel système 59 (execve) — le chemin fiable
lea rdi, [rip+0xf] ; RDI = pointeur vers "/bin/sh"
xor esi, esi ; RSI = NULL (envp)
push 0x3b ; numéro d'appel système 59
pop eax
cdq ; RDX = NULL (argv)
syscall
; Sortie propre
xor edi, edi
push 0x3c ; appel système 60 (exit)
pop eax
syscall
; Section de données : "/bin/sh\0\0\0"
Ce qu'il fait : Lance /bin/sh sans arguments, héritant des privilèges du processus qui a exécuté su. Si l'exploit s'exécute en tant que root (ou avec des capacités qui accordent à su un accès root), le shell résultant est un shell root.
Ce qu'il ne fait PAS :
Il s'agit d'une élévation de privilèges directe en un seul coup, cohérente avec un PoC de recherche.
| Constante | Valeur | Objectif dans l'exploit |
|---|---|---|
AF_PACKET | 17 | Famille de sockets de paquets bruts |
SOCK_RAW | 3 | Type de socket brut |
SOL_PNIO | 279 | Niveau de protocole non pris en charge (Solaris) |
MSG_DONTWAIT | 0x400 | Indicateur sendmsg non bloquant |
copyFail.py
├── hex_to_bytes() — décodeur de chaîne hexadécimale
├── exploit_splice() — exploit principal (configuration socket + corruption + splice)
│ ├── Phase 1 : Créer un socket PACKET, lier, setsockopt (SOL_PNIO)
│ ├── Phase 2 : accept() de la connexion
│ ├── Phase 3 : sendmsg() avec des messages ancillaires conçus
│ ├── Phase 4 : pipe() + splice() pour corrompre la mémoire du noyau
│ └── Phase 5 : tentative de recv() (consolide la corruption)
└── Boucle principale :
├── Ouvrir /usr/bin/su (lecture seule)
├── Décompresser la charge utile intégrée (zlib → ELF de 160 octets)
├── Boucle : injecter 4 octets par itération (40 itérations au total)
└── Exécuter su falsifié → shell root
L'exploit ouvre /usr/bin/su avec O_RDONLY (lecture seule). Il n'a pas besoin d'accès en écriture car la corruption se produit via le cache de pages du noyau — la copie en mémoire du fichier que le noyau utilise avant de vider les modifications sur le disque. L'écriture basée sur splice() contourne entièrement les permissions de fichiers normales, allant directement à la mémoire du noyau.
/usr/bin/su modifié (comparer le hash avec le gestionnaire de paquets)setsockopt() inattendus avec des niveaux de protocole inconnussplice() sur des descripteurs de fichiers non régulierssysctl pour restreindre la création de sockets AF_PACKET par les utilisateurs non privilégiéscopyFail.py (tel que distribué)copyFail_cleaned.py (même répertoire)net/packet/, fs/splice.c, net/core/sock.cCe rapport est destiné uniquement à la recherche en sécurité et à l'analyse défensive.