
Proof-of-concept exploit per CVE-2026-31431, una escalatione locale dei privilegi nel kernel Linux tramite splice() e opzioni socket appositamente predisposte, che consente la scrittura arbitraria nella memoria del kernel per sovrascrivere i binari setuid.
copyFail.py è un exploit proof-of-concept (PoC) che dimostra una vulnerabilità di privilege escalation locale nel kernel Linux. Abusando della chiamata di sistema splice() in combinazione con opzioni socket appositamente configurate, l'exploit ottiene la capacità di corrompere memoria arbitraria del kernel. Utilizza questa capacità per sovrascrivere qualsiasi binario setuid su disco con un eseguibile ELF personalizzato, quindi esegue il binario manomesso per ottenere privilegi elevati.
Tipo di attacco: Privilege escalation locale (LPE) Impatto: Accesso completo a una shell root (quando si prende di mira binari setuid-root) Complessità: Bassa (singolo script, nessuna dipendenza esterna)
Utilizzo:
./copyFail_cleaned.py [binario_target] # predefinito: /usr/bin/su
Il PoC originale aveva /usr/bin/su hardcoded, ma la vulnerabilità sottostante del kernel si applica a qualsiasi file — l'attaccante può sovrascrivere qualsiasi binario setuid scelga. La versione ripulita accetta un argomento opzionale da riga di comando per specificare il target, rendendo esplicito questo comportamento.
Il kernel Linux fornisce una chiamata di sistema splice() che copia dati tra due descrittori di file interamente all'interno del kernel — senza copiare prima i dati nello spazio utente. Questa è chiamata "I/O a copia zero" e viene utilizzata per le prestazioni in applicazioni ad alto throughput come server web e proxy inversi.
La vulnerabilità nasce dal fatto che il kernel non valida correttamente lo stato interno di alcune strutture socket quando splice() viene utilizzato su socket PACKET configurati con opzioni socket a livello di protocollo non supportate. Configurando attentamente queste opzioni, un attaccante può corrompere i puntatori del kernel e quindi utilizzare splice() per reindirizzare le scritture di memoria del kernel verso posizioni arbitrarie.
In termini semplici: il kernel ha una scorciatoia per spostare dati internamente, e questo exploit inganna quella scorciatoia facendole scrivere dati nel posto sbagliato — in particolare, nella copia in memoria di un binario di sistema.
L'exploit segue una sequenza chiara di passaggi:
L'exploit crea un socket PACKET raw (AF_PACKET / SOCK_RAW) e lo associa a indirizzi intenzionalmente malformati. Successivamente chiama setsockopt() utilizzando SOL_PNIO (livello di protocollo 279), che è una costante a livello Solaris che Linux non riconosce.
Perché è importante: Quando il kernel incontra un livello di protocollo non supportato in setsockopt(), ricade su un gestore generico che non valida correttamente i dati. Questo lascia la memoria del kernel in uno stato incoerente e sfruttabile.
Dopo aver stabilito una connessione socket tramite accept(), l'exploit invia messaggi ancillari (di controllo) appositamente configurati utilizzando sendmsg(). Questi messaggi hanno header di lunghezza deliberatamente non corrispondenti — alcuni dichiarano di essere più corti di quanto siano, altri più lunghi.
Perché è importante: Il parser dei messaggi di controllo del kernel utilizza queste lunghezze per l'aritmetica dei puntatori. Le lunghezze non corrispondenti causano al parser di leggere o scrivere oltre i limiti previsti, corrompendo le strutture adiacenti del kernel — in particolare, i puntatori che splice() seguirà successivamente.
L'exploit crea una pipe e chiama splice() due volte:
/usr/bin/su nella pipe.Perché è importante: La chiamata splice() segue i puntatori del kernel corrotti piantati nel Passaggio 2, causando la scrittura di dati in un indirizzo di memoria del kernel scelto dall'attaccante invece che nel buffer del socket. Questo fornisce all'exploit una primitiva di scrittura arbitraria nella memoria del kernel.
Il payload dell'exploit è un eseguibile ELF di 160 byte (vedi sotto). Ogni chiamata alla funzione exploit scrive esattamente 4 byte. Quindi il ciclo principale viene eseguito 40 volte (160 / 4 = 40), ogni volta:
Dopo tutte le 40 iterazioni, i primi 160 byte di /usr/bin/su sono stati sovrascritti.
Infine, l'exploit esegue os.system("su"). Il kernel carica il /usr/bin/su modificato dalla sua cache di pagina (che ora contiene lo shellcode dell'attaccante invece del vero binario su), e lo shellcode viene eseguito.
Il payload incorporato si decompatta in un eseguibile ELF x86-64 di 160 byte contenente questo shellcode:
; Tentativo syscall 105 (execveat) — potrebbe non essere disponibile su kernel più vecchi
xor eax, eax
xor edi, edi
mov al, 0x69 ; syscall 105
syscall
; Fallback: syscall 59 (execve) — il percorso affidabile
lea rdi, [rip+0xf] ; RDI = puntatore a "/bin/sh"
xor esi, esi ; RSI = NULL (envp)
push 0x3b ; numero syscall 59
pop eax
cdq ; RDX = NULL (argv)
syscall
; Uscita pulita
xor edi, edi
push 0x3c ; syscall 60 (exit)
pop eax
syscall
; Sezione dati: "/bin/sh\0\0\0"
Cosa fa: Avvia /bin/sh senza argomenti, ereditando i privilegi del processo che ha eseguito su. Se l'exploit viene eseguito come root (o con capacità che concedono a su accesso root), la shell risultante è una shell root.
Cosa NON fa:
Questa è una semplice escalation di privilegi one-shot, coerente con un PoC di ricerca.
| Costante | Valore | Scopo nell'Exploit |
|---|---|---|
AF_PACKET | 17 | Famiglia socket packet raw |
SOCK_RAW | 3 | Tipo socket raw |
SOL_PNIO | 279 | Livello di protocollo non supportato (Solaris) |
MSG_DONTWAIT | 0x400 | Flag sendmsg non bloccante |
copyFail.py
├── hex_to_bytes() — decodificatore di stringhe esadecimali
├── exploit_splice() — exploit principale (configurazione socket + corruzione + splice)
│ ├── Fase 1: Creazione socket PACKET, bind, setsockopt (SOL_PNIO)
│ ├── Fase 2: connessione accept()
│ ├── Fase 3: sendmsg() con messaggi ancillari appositamente configurati
│ ├── Fase 4: pipe() + splice() per corrompere la memoria del kernel
│ └── Fase 5: tentativo recv() (consolida la corruzione)
└── Ciclo principale:
├── Apertura /usr/bin/su (sola lettura)
├── Decompattazione del payload incorporato (zlib → ELF di 160 byte)
├── Ciclo: iniezione di 4 byte per iterazione (40 iterazioni totali)
└── Esecuzione di su manomesso → shell root
L'exploit apre /usr/bin/su con O_RDONLY (sola lettura). Non ha bisogno di accesso in scrittura perché la corruzione avviene attraverso la cache di pagina del kernel — la copia in memoria del file che il kernel utilizza prima di scrivere le modifiche su disco. La scrittura basata su splice() bypassa completamente i permessi normali dei file, andando direttamente alla memoria del kernel.