
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.
Dopo il linking, objcopy --strip-section-headers rimuove la tabella delle intestazioni di sezione
del payload e .shstrtab. Il caricatore ELF del kernel mappa un programma dai suoi soli
program headers, quindi quei byte non vengono mai caricati a runtime, e poiché
payload è incorporato verbatim, essi gonfiano anche il drop e il
conteggio delle iterazioni di patch_chunk. Rimuoverli porta il payload x86_64 da
~1.3 KB a 720 byte (322 iterazioni di quattro byte ridotte a 180). Due flag
al momento del link rasano il resto: -Wl,--build-id=none elimina la nota build-id, e
-fcf-protection=none elimina la nota CET x86 dove il compilatore la supporta.
Lo strip necessita di binutils >= 2.40. Le build cross passano l'objcopy del target tramite
OBJCOPY= (es. OBJCOPY=aarch64-linux-gnu-objcopy); quando objcopy non può farlo,
la build stampa una nota e mantiene un payload valido più grande.
Questo repository fornisce due varianti dell'exploit che condividono la primitiva di mutazione della cache di pagina AF_ALG/splice ma incassano in esecuzione root in modo diverso. I loro profili di affidabilità non sono equivalenti, e la differenza conta quando si ragiona su modelli di minaccia reali.
exploit)Muta la cache di pagina di un binario setuid target con i byte del payload incorporato, quindi esegue il binario. Il kernel concede i privilegi di root dal bit setuid intatto su disco del binario, carica l'immagine corrotta in memoria, ed esegue il payload.
Funziona ovunque l'attaccante possa open(target, O_RDONLY) per qualsiasi binario
setuid di root sul sistema. Più o meno sconfitto da ambienti che bloccano i binari setuid
dietro directory a lettura ristretta e da progetti di sistema senza setuid.
exploit-passwd)Muta quattro byte della cache di pagina di /etc/passwd per impostare il campo UID dell'utente corrente a "0000". /etc/passwd è leggibile da tutti su ogni sistema Linux standard, quindi la mutazione è universale. Tradurla in esecuzione root dipende da qualche processo lato root che risolve l'utente tramite getpwnam/getpwuid e agisce sull'uid risolto senza cross-validazione. Molti consumatori di questo tipo esistono; molti di loro controllano in modo difensivo la coerenza con la vista del kernel dell'uid chiamante o con la proprietà del file su disco, rompendo l'incasso.
suexploit-passwd esegue su <user> dopo la mutazione, come incasso più semplice
possibile. Funziona contro su di util-linux ma fallisce contro su di shadow-utils
con "Impossibile determinare il tuo nome utente." La mutazione della cache di pagina è ancora
in atto a quel punto, e il pivot verso qualsiasi altro incasso (es. utilizzare un demone
che risolve gli utenti tramite getpwnam senza cross-validazione) è possibile a quel punto.
Esegui echo 3 > /proc/sys/vm/drop_caches come root per pulire la cache di pagina corrotta
quando hai finito di testare.
pavimento: torvalds/linux 72548b093ee3 Agosto 2017, v4.14
(Rielaborazione iov_iter di AF_ALG che
ha introdotto la primitiva di scrittura
sulle pagine file tramite splice nello
scatterlist AEAD)
soffitto: torvalds/linux a664bf3d603d Aprile 2026, mainline
(Ripristina l'ottimizzazione in-place
algif_aead del 2017; separa gli scatterlist
di origine e destinazione in modo che le
pagine della cache di pagina non possano più
essere una destinazione crypto scrivibile)
In mezzo: ogni kernel di distribuzione principale che non ha backportato la correzione.
Ubuntu, RHEL, SUSE, Amazon Linux e Debian sono stati tutti confermati vulnerabili
nei loro kernel di immagini cloud standard al momento della divulgazione. I backport
a livello di distribuzione hanno iniziato a essere rilasciati intorno al 29-04-2026
insieme alla divulgazione pubblica. Per verificare se un kernel target è nella finestra,
controlla se a664bf3d603d (o il suo backport specifico per distribuzione) è presente
nel git log del kernel o nel changelog della distribuzione.
Per revisione di sicurezza a pagamento, lavoro di porting personalizzato o avvisi di sicurezza privati, contatta l'autore tramite la sua società di consulenza, Crash United, LLC.
Contatto: [email protected] · https://crashunited.com
GitHub: @tgies · X/Twitter: @me_irl
Scoperta e divulgazione originale di CVE-2026-31431: Theori / Xint. Writeup pubblico: https://copy.fail/.
Questo porting C: Tony Gies [email protected]
nolibc/: venduto dall'albero del kernel Linux, con doppia licenza
LGPL-2.1-or-later O MIT (vedi nolibc/nolibc.h e le intestazioni SPDX
dei singoli file).
Il dropper e le fonti del payload in questo repository sono rilasciati sotto gli stessi termini di doppia licenza LGPL-2.1-or-later O MIT dell'albero nolibc da cui dipendono, per mantenere la compatibilità di licenza in modo banale per chiunque venda questa intera directory nel proprio lavoro.
L'exploit e il payload sono pubblicati per scopi di ricerca sulla sicurezza e rilevamento difensivo. L'uso contro sistemi che non possiedi o per cui non hai esplicita autorizzazione al test è un problema tuo, non dell'autore.
| Incasso | Setup pre-root necessario | Note |
|---|
| Lancio sessione WSL2 | No | Il setuid(getpwnam(default_user)->pw_uid) per sessione di WSL non esegue alcuna validazione. Funziona pulitamente. |
su di util-linux | No | Gestione permissiva dell'identità del chiamante. |
su di shadow-utils | Sì | Il controllo getpwuid(getuid()) dell'identità del chiamante fallisce perché la mutazione rimuove l'uid reale. |
sshd (StrictModes yes predefinito) | Sì (disabilitare StrictModes) | StrictModes richiede che la home directory sia di proprietà di root o di pw->pw_uid. La mutazione imposta pw_uid=0; il proprietario su disco rimane l'uid originale; la discrepanza rifiuta l'autenticazione. |
| Consegna locale MTA (postfix, exim, ecc.) | Variabile | Dipende dalla validazione dei permessi home dell'MDA. Testare per MTA specifico. |