
CVE-2026-31431-killed page-cache exploit — esecuzione di codice nei container che condividono lo stesso layer dell'immagine
Exploit cross-container della vulnerabilità aead AF_ALG -- passare da un container compromesso a ogni container fratello che condivide lo stesso layer dell'immagine libc.so.6.
Questa è una primitiva di fuga: viene eseguita
dall'interno di un container non privilegiato che l'attaccante ha già
compromesso, e usa il bug di scrittura arbitraria di 4 byte per rotazione ESN
authencesn di AF_ALG (CVE-2026-31431) per piantare un hook persistente di
read() nelle pagine della page cache di libc.so.6. Poiché Docker /
containerd appoggia i file dei layer inferiori di overlayfs su inode condivisi,
quelle pagine sono visibili a ogni container fratello istanziato dalla stessa
immagine -- l'hook scatta anche nei loro processi, e l'attaccante ottiene
l'esecuzione di comandi dentro ciascuno di essi.
victim)
su un host che esegue altri container (siblings) dalla stessa
immagine di victim.victim gira con la postura Docker/k8s predefinita: uid non privilegiato
dentro il namespace utente del container, profilo seccomp predefinito,
profilo AppArmor predefinito, nessuna capability speciale, nessun bind
mount dell'host.victim ha solo:
/usr/lib/x86_64-linux-gnu/libc.so.6
o dove la distribuzione la installa)socket(AF_ALG, ...)splice / vmsplicechmod +x (es. /tmp)algif_aead + authencesn precedente al fix di revert a monte).Questo è tutto. Nessuna CAP_* speciale, nessun accesso al
filesystem dell'host. L'attaccante inserisce un binario statico autosufficiente
dentro il container, lo esegue, e la corruzione della page cache -- e
quindi l'hook -- diventa visibile a ogni container fratello.
Identità della pagina nella page cache. Dentro un container overlayfs,
/usr/lib/.../libc.so.6 è servito dall'inode ext4 del layer inferiore
dell'immagine. Ogni container avviato dalla stessa immagine condivide
quell'inode di supporto, e la page cache del kernel è indicizzata
in base all'inode sottostante -- non dall'overlay né dal namespace.
Quindi una singola scrittura di 4 byte in una pagina della page cache
è visibile a tutti i processi dei container fratelli che hanno quella
pagina in mmap.
La vulnerabilità aead AF_ALG trasforma una tale scrittura in molte.
algif_aead concatena l'iovec RX dell'utente con i byte finali
authsize del SGL TX spliced, e la rotazione ESN di authencesn
parcheggia 4 byte del campo seq_high dell'AAD in
dst[assoclen + cryptlen] -- che è il primo byte di quella coda
esterna concatenata. La pagina spliced è una pagina della page
cache di un file su cui l'attaccante ha solo accesso in lettura,
ma il cipher vi copia comunque dei byte, senza contabilità dirty.
(Vedi crypto/algif_aead.c e crypto/authencesn.c per i
meccanismi sottostanti.)
Bootstrap di una primitiva invocabile. La prima cosa che fa
page_inject è il bootstrap della Zona A -- una
re-implementazione in assembly dello stesso giro AF_ALG
(write_cache.asm), collocata nella cave .text della libc.
Questo rende la scrittura di 4 byte una call regolare
dall'interno di qualsiasi futuro payload hook, senza bisogno di
setup socket a ogni chiamata.
Installazione dell'hook. L'iniettore poi scrive la Zona C
(zone_c.asm) nella cave .text della libc e applica una patch
ai primi 7-12 byte di read() con un salto E9 disp32 verso di
essa. I byte spostati del prologo vengono emulati fedelmente nel
fast-path della Zona C (vengono riconosciuti tre diversi prologhi
glibc -- vedi "Gestione del prologo" sotto). L'hook ora è attivo
nella page cache della libc.
Propagazione dell'hook. Ogni container fratello esegue processi
che chiamano read() di continuo (demoni di log, healthcheck, cat /etc/hostname, qualsiasi cosa). Alla prima chiamata del genere
dentro un container fratello, il prologo dirottato salta nella
Zona C, che:
stat("/") dell'inode root del container (un ID stabile
per namespace), usandolo come chiave di slot del container,fork() di un longevo
figlio a ciclo di comando che interroga l'area CMD alla ricerca
di ordini,read()+N così il chiamante non sospetta nulla.
Il processo originale del fratello continua a girare. Da questo
momento l'attaccante ha un demone dentro quel container.Canale di comando. L'attaccante usa lo stesso binario
page_inject in modalità --shell per scrivere comandi nell'area
CMD della regione slot. Il figlio hook di ogni container fratello
registrato interroga, fa fork di /bin/sh -c <cmd>, cattura
stdout/stderr nell'area OUTPUT, segnala il completamento e torna
a interrogare. La shell mostra l'output. Poiché ogni scrittura
CMD/OUTPUT passa anche attraverso la primitiva della vulnerabilità,
non serve alcun privilegio speciale.
Rimozione dell'hook. Quando ha finito, unhook ripristina i
byte originali del prologo di read() e azzera la tabella slot;
i figli hook vedono uno slot vuoto alla loro iterazione successiva
e si auto-terminano. Le modifiche alla page cache in sé sono pulite
(il kernel non ha mai marcato dirty le pagine modificate), quindi
una volta fermati tutti i container che hanno libc in mmap, un
drop_caches ripristina completamente la cache -- non rimane
alcun artefatto su disco.
L'iniettore viene compilato fuori dal container vittima --
tipicamente sulla macchina di sviluppo dell'attaccante -- perché la
maggior parte delle immagini container di produzione non include un
compilatore. Un ambiente di sviluppo Linux x86_64 standard con gcc
(con supporto al link -static) e nasm è sufficiente.
make # assembles .asm sources via gen_arrays.sh, links static page_inject
make shellcode # also produces inspectable .bin flat binaries
make clean # removes generated files and the binary
L'output è un singolo ELF staticamente linkato (./page_inject) che
gira su qualsiasi kernel Linux x86_64 moderno.
Una volta che l'attaccante ha shell su victim, carica il binario in
una directory scrivibile (tipicamente /tmp):
# inside the compromised container, attacker session
victim$ ./page_inject
Senza argomenti, page_inject usa come predefinito
/usr/lib/x86_64-linux-gnu/libc.so.6 (la posizione post-merge di
Debian/Ubuntu). Per altre distribuzioni la libc è in un percorso
diverso; passatela esplicitamente oppure usate --root / per
scansionare la tabella di lookup integrata a partire dalla root del
container:
# Fedora / Rocky / CentOS
victim$ ./page_inject /usr/lib64/libc.so.6
# Arch
victim$ ./page_inject /usr/lib/libc.so.6
# Auto-detect, regardless of distro:
victim$ ./page_inject --root /
Ognuna delle due invocazioni fa la stessa cosa: parse ELF della libc
nel container, installa l'hook nella sua page cache, monitora la
tabella slot per ~30 s mentre i fratelli si registrano, ed esegue un
id one-shot contro il primo fratello che si è registrato come
controllo di sanità.
Dopo il bootstrap, si entra nella shell di comando per pilotare qualsiasi fratello registrato:
victim$ ./page_inject --shell --no-bootstrap
=== page-cache shell ===
Containers (3):
[0] 0x0018598d <- target
[1] 0x001859ab
[2] 0x001859cd