
Port C multiplateforme de l'exploit Copy Fail Linux LPE (CVE-2026-31431). Divulgué le 2026-04-29 par Theori / Xint.
Anglais (en) ∙ 日本語 (ja) ∙ 简体中文 (zh-cn) ∙ 한국어 (ko) ∙ Русский (ru)
Une réimplémentation en C multiplateforme de l'élévation de privilèges Linux Copy Fail (CVE-2026-31431), divulguée le 29 avril 2026 par Theori / Xint. Voir l'article canonique sur copy.fail pour la description complète de la vulnérabilité, la chronologie et le processus de découverte de Theori.
La preuve de concept publiée est un script Python de 732 octets. Ce portage en C démontre que la même exploitation peut être exprimée en C portable compilable vers n'importe quelle architecture supportée par nolibc, sans blocs hexadécimaux par architecture ni assembleur inline dans le code source du projet.
Auteur de ce portage : Tony Gies [email protected]. Découverte et divulgation originale : Theori / Xint.
copy-fail-c/
├── exploit.c le dropper (variante de mutation binaire)
├── exploit-passwd.c le dropper (variante /etc/passwd avec changement d'UID)
├── vulnerable.c vérificateur de vulnérabilité non destructif
├── payload.c le corps qui est déposé (setgid+setuid+execve sh)
├── utils.c, utils.h primitive partagée de mutation du cache de pages via AF_ALG/splice
├── Makefile orchestration de compilation
├── nolibc/ importé depuis torvalds/linux tools/include/nolibc
└── README.md ce fichier
Après make :
├── payload petit ELF statique, intégré dans le dropper sous forme d'octets
├── payload.o payload encapsulé en .o relogable par `ld -r -b binary`
├── exploit dropper, variante de mutation binaire
├── exploit-passwd dropper, variante /etc/passwd avec changement d'UID
└── vulnerable vérificateur de vulnérabilité non destructif
exploit.c ouvre le binaire cible en lecture seule, puis pour chaque fenêtre de 4 octets du
payload intégré, exécute un déchiffrement AEAD factice via AF_ALG dont l'entrée de texte chiffré
est fournie par splice() depuis les pages du cache de pages de la cible.
L'optimisation in-place du template authencesn traite les pages source issues de splice à la fois
comme entrée de texte chiffré et destination du texte clair, de sorte que le déchiffrement
(échoué) a déjà écrasé la page du cache de pages au moment où la vérification d'authentification
rejette la requête. Après 4 * N itérations, l'image mise en cache de la cible a été remplacée
octet par octet par le payload. L'exécution de la cible via execve() charge les pages mutées ;
l'inode sur disque est toujours setuid root, donc le noyau accorde les privilèges root et exécute
le payload.
payload.c est du C portable standard : setgid(0); setuid(0); execve("/bin/sh", ...).
nolibc fournit _start, les mécanismes d'appel système et les manipulations de registres
par architecture.
Une seconde variante, exploit-passwd.c, mute quatre octets du cache de pages de /etc/passwd
au lieu de l'image d'un binaire setuid. Elle n'a pas besoin de payload intégré et fonctionne
sur les systèmes où la mutation d'un binaire est bloquée, mais sa surface de monétisation est
beaucoup plus réduite.
vulnerable.c n'est pas une exploitation. Il crée un testfile local contenant la chaîne
init, puis exécute la même primitive patch_chunk() contre le cache de pages de ce fichier
pour écraser les octets avec vulnerable. Si le contenu relu correspond, le noyau en cours
d'exécution est dans la fenêtre de CVE-2026-31431. L'inode sur disque n'est jamais modifié ;
testfile est supprimé à la sortie ; la mutation du cache de pages s'évapore avec lui.
S'exécute sans privilèges. Renvoie 100 si vulnérable, 0 si la primitive a fonctionné mais que
la mutation n'a pas pris, 2 si la famille de sockets AF_ALG ou le template authencesn est
indisponible donc l'état du patch ne peut être déterminé, et 1 pour d'autres erreurs d'exécution.
Par défaut (natif pour l'hôte) :
make
Cross-compilation pour aarch64 (ou toute autre architecture Linux pour laquelle une chaîne de cross-compilation est installée) :
make CC=aarch64-linux-gnu-gcc LD=aarch64-linux-gnu-ld
Architectures supportées par nolibc importé (selon l'amont) : x86_64, i386, arm, aarch64,
riscv32/64, mips, ppc, s390x, loongarch, m68k, sh, sparc. nolibc se base sur les macros
d'architecture du compilateur, donc choisir le bon CC/LD est suffisant.
Prérequis pour la compilation :
cc, gcc, ou toute variante cross)ld -r -b binary (binutils ld et lld le font)linux/if_alg.h et <asm/unistd.h>
(Debian/Ubuntu : linux-libc-dev ; variantes cross : généralement incluses par
le paquet de la chaîne cross)Les ensembles d'en-têtes antérieurs à Linux 5.6 n'ont pas __kernel_old_time_t ni
struct __kernel_old_timespec, utilisés par le nolibc importé. compat.h (inclus de force
dans la compilation du payload) les fournit quand ils sont absents, donc un linux-libc-dev
plus ancien compile tout de même. Il est sans effet sur les en-têtes 5.6+.
Il n'y a pas de dépendances à des bibliothèques externes. Le payload est compilé en mode
freestanding avec nolibc ; le dropper est lié à la libc de l'hôte uniquement pour fprintf
et perror.
Quelques petites fonctionnalités de la chaîne de compilation supportent l'essentiel du travail pour garder le code source portable et le payload petit.
nolibc/ est le petit remplacement de libc du noyau, basé uniquement sur des en-têtes,
importé depuis torvalds/linux tools/include/nolibc/. Il fournit _start, une macro
syscall() portable et des wrappers inline d'appels système, avec les conventions de
registres par architecture encodées dans nolibc/arch-*.h. Compiler le payload avec
-nostdlib -static -ffreestanding -Inolibc produit un petit ELF statique qui appelle
directement le noyau sans entraîner le démarrage de glibc, l'initialisation TLS ou la
plomberie de stack canary. Résultat, une fois compressé et décapé (les deux ci-dessous) :
~720 octets sur x86_64, ~1,2 Ko sur aarch64, contre ~17 Ko pour le même payload.c lié
avec musl-static ou ~700 Ko avec glibc-static.
ld -r -b binary pour l'intégrationLe Makefile transforme le payload compilé en payload.o via ld -r -b binary -o payload.o payload.
L'éditeur de liens émet les octets d'entrée verbatim comme la section de données d'un fichier
objet relogable et synthétise trois symboles à partir du nom du fichier d'entrée :
_binary_payload_start adresse du premier octet du payload
_binary_payload_end adresse après le dernier octet du payload
_binary_payload_size symbole absolu dont la valeur est la taille en octets
exploit.c déclare les deux premiers comme extern const unsigned char[] et calcule
la taille par _binary_payload_end - _binary_payload_start.
-Wl,-N plus max-page-size serréLe payload est lié statiquement avec -Wl,-N -Wl,-z,max-page-size=0x10, ce qui fusionne
.text/.rodata/.data en un seul segment LOAD avec un alignement fichier de 16 octets
au lieu de l'alignement par défaut de 4 Ko par segment (aligné sur la page noyau). Cela
produit un avertissement "RWX permissions" de ld, qui est seulement informatif – la
protection mémoire au moment de l'exécution du payload n'a pas d'importance pour son
programme à usage unique. Sans ce drapeau, le même code se lie à ~13 Ko sur x86_64
(surtout du remplissage nul entre segments) ; avec, ~1,3 Ko avant la suppression des
en-têtes de section ci-dessous.