
Porting in C multipiattaforma del Copy Fail Linux LPE (CVE-2026-31431). Divulgato il 2026-04-29 da Theori / Xint.
Inglese (en) ∙ Giapponese (ja) ∙ Cinese semplificato (zh-cn) ∙ Coreano (ko) ∙ Russo (ru)
Una reimplementazione C multipiattaforma dell'LPE Linux Copy Fail (CVE-2026-31431), divulgato il 29-04-2026 da Theori / Xint. Consulta il writeup canonico su copy.fail per la descrizione completa della vulnerabilità, la cronologia e il processo di scoperta di Theori.
Il proof-of-concept rilasciato pubblicamente è uno script Python di 732 byte. Questo porting C dimostra che lo stesso exploit può essere espresso come C portabile compilabile per qualsiasi architettura supportata da nolibc, senza blob hex per architettura o assembly inline nel codice sorgente del progetto stesso.
Autore di questo porting: Tony Gies [email protected]. Scoperta e divulgazione originale: Theori / Xint.
copy-fail-c/
├── exploit.c il dropper (variante mutazione binaria)
├── exploit-passwd.c il dropper (variante /etc/passwd UID-flip)
├── vulnerable.c verificatore di vulnerabilità non distruttivo
├── payload.c il corpo che viene rilasciato (setgid+setuid+execve sh)
├── utils.c, utils.h primitiva condivisa di mutazione della cache di pagina AF_ALG/splice
├── Makefile orchestrazione della build
├── nolibc/ venduto da torvalds/linux tools/include/nolibc
└── README.md questo file
Dopo make:
├── payload piccolo ELF statico, incorporato nel dropper come byte
├── payload.o payload racchiuso come .o rilocabile da `ld -r -b binary`
├── exploit dropper, variante mutazione binaria
├── exploit-passwd dropper, variante /etc/passwd UID-flip
└── vulnerable verificatore di vulnerabilità non distruttivo
exploit.c apre il binario target in sola lettura, quindi per ogni finestra di 4 byte del
payload incorporato esegue un fittizio decifratura AEAD attraverso AF_ALG il cui
input ciphertext è fornito tramite splice() dalle pagine della cache di pagina del target.
L'ottimizzazione in-place del template authencesn tratta le pagine splice'd sia come
input ciphertext che come destinazione plaintext, quindi la decifratura (fallita) ha già
sovrascritto la pagina della cache di pagina quando la verifica di autenticazione rifiuta la
richiesta. Dopo 4 * N iterazioni l'immagine cache del target è stata sostituita byte per byte
con il payload. esecve() del target carica le pagine mutate; l'inode su disco è ancora
setuid root, quindi il kernel concede i privilegi di root ed esegue il payload.
payload.c è C portabile semplice: setgid(0); setuid(0); execve("/bin/sh", ...). nolibc fornisce _start, il meccanismo delle syscall e la gestione dei registri
per-arch.
Una seconda variante, exploit-passwd.c, muta quattro byte della cache di pagina di
/etc/passwd invece dell'immagine di un binario setuid. Non necessita di payload incorporato e
funziona su sistemi in cui la via di mutazione binaria è bloccata, ma la sua superficie di
incasso è molto più limitata.
vulnerable.c non è un exploit. Crea un testfile locale contenente la stringa
init, quindi esegue la stessa primitiva patch_chunk() contro la cache di pagina di quel file
per sovrascrivere i byte con vulnerable. Se i contenuti letti corrispondono, il kernel in esecuzione
è nella finestra per CVE-2026-31431. L'inode su disco non viene mai modificato; testfile viene rimosso
all'uscita; la mutazione della cache di pagina svanisce con esso. Viene eseguito senza privilegi. Esce con 100 se
vulnerabile, 0 se la primitiva è stata eseguita ma la mutazione non ha avuto effetto, 2 se la famiglia
di socket AF_ALG o il template authencesn non sono disponibili quindi lo stato della patch non può
essere determinato, e 1 per altri errori di runtime.
Predefinita (nativa host-arch):
make
Cross-compila per aarch64 (o qualsiasi altra architettura Linux per cui è installato un cross-toolchain):
make CC=aarch64-linux-gnu-gcc LD=aarch64-linux-gnu-ld
Architetture supportate dal nolibc venduto (secondo upstream): x86_64, i386,
arm, aarch64, riscv32/64, mips, ppc, s390x, loongarch, m68k, sh, sparc.
nolibc dispaccia sulle macro arch del compilatore, quindi scegliere il
CC/LD corretto è sufficiente.
Necessario per compilare:
cc, gcc, o qualsiasi variante cross)ld -r -b binary (binutils ld e lld lo fanno entrambi)linux/if_alg.h e <asm/unistd.h>
(Debian/Ubuntu: linux-libc-dev; varianti cross: solitamente incluse dal
pacchetto cross-toolchain)Set di intestazioni precedenti a Linux 5.6 sono antecedenti a __kernel_old_time_t e
struct __kernel_old_timespec, che il nolibc venduto utilizza. compat.h
(force-incluso nella build del payload) li fornisce quando assenti, quindi un
linux-libc-dev più vecchio compila comunque. È un no-op su intestazioni 5.6+.
Non ci sono dipendenze da librerie esterne. Il payload è compilato freestanding
contro nolibc; il dropper si collega alla libc host solo per fprintf
e perror.
Alcune piccole funzionalità del toolchain supportano la maggior parte del peso nel mantenere il codice portabile e il payload piccolo.
nolibc/ è la minuscola sostituzione header-only della libc del kernel, venduta da
torvalds/linux tools/include/nolibc/. Fornisce _start, una macro syscall()
portabile e wrapper di syscall inline, con le convenzioni dei registri per-arch
codificate in nolibc/arch-*.h. Compilare il payload con
-nostdlib -static -ffreestanding -Inolibc produce un piccolo ELF statico che
chiama direttamente il kernel senza trascinare l'avvio di glibc, l'inizializzazione TLS
o la strumentazione di stack-canary. Risultato una volta impacchettato e privato delle sezioni (entrambi
sotto): ~720 byte su x86_64, ~1.2 KB su aarch64, contro ~17 KB per lo stesso
payload.c linkato contro musl-static o ~700 KB contro glibc-static.
ld -r -b binary per l'incorporamentoIl Makefile trasforma il payload ELF costruito in payload.o tramite ld -r -b binary -o payload.o payload. Il linker emette i byte di input verbatim come
la sezione dati di un file oggetto rilocabile e sintetizza tre simboli
dal nome del file di input:
_binary_payload_start indirizzo del primo byte del payload
_binary_payload_end indirizzo dopo l'ultimo byte del payload
_binary_payload_size simbolo assoluto il cui valore è la dimensione in byte
exploit.c dichiara i primi due come extern const unsigned char[] e
calcola la dimensione come _binary_payload_end - _binary_payload_start.
-Wl,-N più max-page-size strettoIl payload è linkato staticamente con -Wl,-N -Wl,-z,max-page-size=0x10,
che collassa .text/.rodata/.data in un unico segmento LOAD con
allineamento file di 16 byte invece dell'impostazione predefinita di 4 KB per segmento
allineato alla pagina del kernel. Questo produce un avviso "RWX permissions" da
ld, che è solo informativo - la protezione della memoria runtime del payload non ha
importanza per il suo programma monouso. Senza questo flag, lo stesso codice si linka a ~13 KB
su x86_64 (principalmente padding zero inter-segmento); con esso, ~1.3 KB prima della
rimozione dell'intestazione di sezione sottostante.