
CVE-2026-31431-killed page-cache exploit — ejecución de código en contenedores que comparten la misma capa de imagen
Exploit de vulnerabilidad aead AF_ALG entre contenedores -- pivotar desde un contenedor comprometido a todos los contenedores hermanos que comparten la misma capa de imagen libc.so.6.
Esta es una primitiva de escape: se ejecuta desde dentro de un contenedor no privilegiado que el atacante ya ha comprometido, y utiliza el bug de escritura arbitraria de 4 bytes en la rotación ESN de authencesn de AF_ALG (CVE-2026-31431) para plantar un hook persistente de read() en las páginas de caché de página de libc.so.6. Debido a que Docker / containerd respaldan los archivos de la capa inferior de overlayfs con inodos compartidos, esas páginas son visibles para todos los contenedores hermanos instanciados desde la misma imagen -- el hook se activa también en sus procesos, y el atacante obtiene ejecución de comandos dentro de cada uno.
victim) en un host que ejecuta otros contenedores (siblings) desde la misma imagen que victim.victim se ejecuta con la configuración predeterminada de Docker/k8s: uid no privilegiado dentro del namespace de usuario del contenedor, perfil seccomp predeterminado, perfil AppArmor predeterminado, sin capacidades especiales, sin bind mounts del host.victim tiene solo:
/usr/lib/x86_64-linux-gnu/libc.so.6 o donde la distribución la instale)socket(AF_ALG, ...)splice / vmsplicechmod +x (por ejemplo, /tmp)algif_aead + authencesn anterior a la corrección de reversión upstream).Eso es todo. Sin CAP_* especial, sin acceso al sistema de archivos del host. El atacante coloca un binario autocontenido enlazado estáticamente dentro del contenedor, lo ejecuta, y la corrupción de la caché de página -- y por lo tanto el hook -- se vuelve visible para todos los hermanos.
Identidad de página de caché de página. Dentro de un contenedor overlayfs, /usr/lib/.../libc.so.6 es servido por el inodo ext4 de la capa inferior de la imagen. Cada contenedor iniciado desde la misma imagen comparte ese inodo subyacente, y la caché de página del kernel se clavea por el inodo subyacente -- no por el overlay ni por el namespace. Así, una sola escritura de 4 bytes en una página de caché de página es visible para todos los procesos de los contenedores hermanos que tengan esa página mapeada.
La vulnerabilidad aead de AF_ALG convierte una escritura de este tipo en muchas. algif_aead encadena el iovec RX del usuario con los bytes authsize finales del SGL TX empalmado, y la rotación ESN de authencesn coloca 4 bytes del campo seq_high del AAD en dst[assoclen + cryptlen] -- que es el primer byte de esa cola externa encadenada. La página empalmada es una página de caché de página de un archivo al que el atacante solo tiene acceso de lectura, pero el cifrador copia bytes en ella de todos modos, sin contabilidad dirty. (Vea crypto/algif_aead.c y crypto/authencesn.c para la mecánica subyacente.)
Inicialización de una primitiva invocable. Lo primero que hace page_inject es inicializar Zone A -- una reimplementación codificada en asm del mismo baile AF_ALG (write_cache.asm), colocada dentro de la cueva .text de libc. Esto hace que la escritura de 4 bytes sea un call regular desde dentro de cualquier payload de hook futuro, sin necesidad de configuración de socket por llamada.
Instalación del hook. El inyector luego escribe Zone C (zone_c.asm) en la cueva .text de libc y parchea los primeros 7-12 bytes de read() con un salto E9 disp32 hacia ella. Los bytes desplazados del prólogo se emulan fielmente en la ruta rápida de Zone C (se reconocen tres prólogos diferentes de glibc -- vea "Manejo de prólogos" más abajo). El hook ahora está activo en la caché de página de libc.
Propagación del hook. Cada contenedor hermano ejecuta procesos que llaman a read() constantemente (demonios de registro, healthchecks, cat /etc/hostname, cualquier cosa). En la primera llamada de este tipo dentro de un contenedor hermano, el prólogo secuestrado salta a Zone C, que:
stat("/") del inodo raíz del contenedor (un ID estable por namespace), lo usa como clave del slot del contenedor,fork() de un hijo de bucle de comandos de larga duración que sondea el área CMD en busca de órdenes,read()+N para que el llamante no se dé cuenta.
El proceso hermano original sigue ejecutándose. A partir de ahora, el atacante tiene un demonio dentro de ese contenedor.Canal de comandos. El atacante usa el mismo binario page_inject en modo --shell para escribir comandos en el área CMD de la región de slots. El hijo del hook de cada contenedor hermano registrado sondea, hace fork de /bin/sh -c <cmd>, captura stdout/stderr en el área OUTPUT, señala finalización, y vuelve a sondear. El shell muestra la salida. Debido a que cada escritura CMD/OUTPUT también pasa por la primitiva de la vulnerabilidad, no se necesita ningún privilegio especial.
Deshacer hook. Cuando se termina, unhook restaura los bytes originales del prólogo de read() y pone a cero la tabla de slots; los hijos del hook ven un slot vacío en su próxima iteración y se autofinalizan. Las modificaciones de la caché de página en sí son limpias (el kernel nunca marcó las páginas modificadas como dirty), así que una vez que se detienen todos los contenedores que tienen libc mapeada, un drop_caches revierte completamente la caché -- no queda ningún artefacto en disco.
El inyector se compila fuera del contenedor víctima -- típicamente en la máquina de desarrollo del atacante -- porque la mayoría de las imágenes de contenedor de producción no incluyen un compilador. Un entorno de desarrollo Linux x86_64 estándar con gcc (con soporte de enlace -static) y nasm es 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
La salida es un único ELF enlazado estáticamente (./page_inject) que se ejecuta en cualquier kernel Linux x86_64 moderno.
Una vez que el atacante tiene shell en victim, sube el binario a un directorio escribible (típicamente /tmp):
# inside the compromised container, attacker session
victim$ ./page_inject
Sin argumentos, page_inject usa por defecto /usr/lib/x86_64-linux-gnu/libc.so.6 (la ubicación posterior a la fusión en Debian/Ubuntu). Para otras distribuciones, libc está en una ruta diferente; pásala explícitamente o usa --root / para escanear la tabla de búsqueda incorporada desde la raíz del contenedor:
# 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 /
Cualquiera de las invocaciones hace lo mismo: analizar ELF de libc dentro del contenedor, instalar el hook en su caché de página, monitorear la tabla de slots durante ~30 s mientras los hermanos se registran, y ejecutar un id de una sola vez contra el primer hermano que se registró como verificación de cordura.
Después de la inicialización, ingresa al shell de comandos para manejar cualquier hermano registrado: