
CVE-2026-31431-killed Page-Cache-Exploit — Code-Ausführung in Container, die dieselbe Image-Ebene teilen
AF_ALG aead-Schwachstellen-Exploit für Containerflucht – pivotiere von einem kompromittierten Container in jeden anderen Container, der dieselbe libc.so.6-Ebene des Images nutzt.
Hierbei handelt es sich um ein Escape-Primitiv: Es wird von innerhalb eines unprivilegierten Containers ausgeführt, den der Angreifer bereits kompromittiert hat, und nutzt den AF_ALG authencesn-ESN-Rotationsfehler (4-Byte-Arbitrary-Write, CVE-2026-31431), um einen persistenten read()-Hook in den Page-Cache-Seiten von libc.so.6 zu platzieren. Da Docker/containerd die unteren Layer von OverlayFS mit gemeinsamen Inodes bereitstellen, sind diese Seiten für jeden anderen Container sichtbar, der aus demselben Image erstellt wurde – der Hook feuert auch in deren Prozessen, und der Angreifer erhält Befehlsausführung in jedem dieser Container.
victim) auf einem Host, der andere Container (siblings) aus dem gleichen Image wie victim ausführt.victim läuft mit der Standard-Docker/k8s-Konfiguration: unprivilegierte UID innerhalb des Container-Benutzernamensraums, Standard-Seccomp-Profil, Standard-AppArmor-Profil, keine speziellen Capabilities, keine Host-Bind-Mounts.victim hat nur:
/usr/lib/x86_64-linux-gnu/libc.so.6 oder dort, wo die Distribution sie installiert)socket(AF_ALG, ...)-Syscall-Familiesplice/vmsplice-Syscallschmod +x versehen kann (z. B. /tmp)algif_aead + authencesn-Builds vor dem Upstream-Revert-Fix).Das ist alles. Kein spezielles CAP_*, kein Zugriff auf das Host-Dateisystem. Der Angreifer lädt eine statisch gelinkte, in sich geschlossene Binärdatei in den Container, führt sie aus, und die Page-Cache-Korruption – und damit der Hook – wird für jeden anderen Container sichtbar.
Page-Cache-Seitenidentität. Innerhalb eines OverlayFS-Containers wird /usr/lib/.../libc.so.6 durch den ext4-Inode der unteren Image-Ebene bereitgestellt. Jeder Container, der aus demselben Image gestartet wird, teilt sich diesen zugrunde liegenden Inode, und der Page-Cache des Kernels ist durch den zugrunde liegenden Inode – nicht durch Overlay oder Namespace – indiziert. Ein einzelner 4-Byte-Schreibvorgang in eine Page-Cache-Seite ist daher für alle Prozesse in anderen Containern sichtbar, die diese Seite per mmap eingebunden haben.
AF_ALG aead-Schwachstelle macht aus einem solchen Schreibvorgang viele. algif_aead verknüpft das RX-iov des Benutzers mit den nachfolgenden authsize-Bytes des gesplickten TX-SGL, und die ESN-Rotation von authencesn legt 4 Bytes des seq_high-Feldes des AAD an dst[assoclen + cryptlen] ab – also das erste Byte dieses verketteten fremden Endstücks. Die gesplickte Seite ist eine Page-Cache-Seite einer Datei, auf die der Angreifer nur Lesezugriff hat, aber die Chiffre kopiert trotzdem Bytes hinein, ohne Dirty-Buchführung. (Siehe crypto/algif_aead.c und crypto/authencesn.c für die zugrunde liegende Mechanik.)
Aufbau eines aufrufbaren Primitivs. Als Erstes bootet page_inject Zone A – eine asm-codierte Neuimplementierung derselben AF_ALG-Übung (write_cache.asm), die in die .text-Höhle von libc eingefügt wird. Dadurch wird der 4-Byte-Schreibvorgang zu einem regulären call innerhalb jedes zukünftigen Hook-Payloads, ohne dass pro Aufruf ein Socket aufgebaut werden muss.
Installation des Hooks. Der Injektor schreibt dann Zone C (zone_c.asm) in die .text-Höhle von libc und patcht die ersten 7–12 Bytes von read() mit einem E9 disp32-Sprung darauf. Die verdrängten Bytes des Prologs werden im Fast-Path von Zone C originalgetreu nachgebildet (drei verschiedene glibc-Prologe werden erkannt – siehe „Prologbehandlung" unten). Der Hook ist jetzt live im Page-Cache von libc.
Ausbreitung des Hooks. Jeder andere Container führt Prozesse aus, die ständig read() aufrufen (Logging-Daemons, Healthchecks, cat /etc/hostname, usw.). Beim ersten solchen Aufruf in einem anderen Container springt der gekaperte Prolog in Zone C, welches:
stat("/") den Root-Inode des Containers (eine stabile, pro-Namespace-ID) und verwendet ihn als Slot-Schlüssel des Containers,read()+N zurückkehrt, damit der Aufrufer nichts merkt.
Der ursprüngliche Prozess im anderen Container läuft weiter. Von nun an hat der Angreifer einen Daemon in diesem Container.Befehlskanal. Der Angreifer verwendet dasselbe page_inject-Binary im --shell-Modus, um Befehle in das CMD-Gebiet des Slotbereichs zu schreiben. Der Hook-Kindprozess jedes registrierten anderen Containers pollt, forkt /bin/sh -c <cmd>, erfasst stdout/stderr im OUTPUT-Gebiet, signalisiert Fertigstellung und kehrt zum Pollen zurück. Die Shell zeigt die Ausgabe. Da jeder CMD/OUTPUT-Schreibvorgang ebenfalls durch das Schwachstellenprimitiv läuft, ist keine besondere Berechtigung erforderlich.
Unhook. Wenn fertig, stellt unhook die ursprünglichen Prolog-Bytes von read() wieder her und setzt die Slottabelle zurück; Hook-Kindprozesse sehen bei ihrem nächsten Durchlauf einen leeren Slot und beenden sich selbst. Die Page-Cache-Modifikationen selbst sind sauber (der Kernel hat die geänderten Seiten nie als dirty markiert), sodass sobald alle Container, die libc per mmap eingebunden haben, gestoppt sind, ein drop_caches den Cache vollständig zurücksetzt – es bleibt kein Artefakt auf der Festplatte zurück.
Der Injektor wird außerhalb des Opfer-Containers gebaut – typischerweise auf dem eigenen Entwicklungsrechner des Angreifers –, da die meisten Produktionscontainer-Images keinen Compiler mitliefern. Eine Standard-Linux-x86_64-Entwicklungsumgebung mit gcc (mit -static-Link-Unterstützung) und nasm reicht aus.
make # assembliert .asm-Quellen via gen_arrays.sh, linkt statisches page_inject
make shellcode # erzeugt auch inspizierbare .bin-Flachbinaries
make clean # entfernt generierte Dateien und das Binary
Die Ausgabe ist ein einzelnes statisch gelinktes ELF (./page_inject), das auf jedem modernen x86_64-Linux-Kernel läuft.
Sobald der Angreifer eine Shell auf victim hat, lädt er das Binary in ein beschreibbares Verzeichnis (üblicherweise /tmp):
# innerhalb des kompromittierten Containers, Angreifer-Session
victim$ ./page_inject
Ohne Argumente verwendet page_inject standardmäßig /usr/lib/x86_64-linux-gnu/libc.so.6 (den Post-Merge-Pfad für Debian/Ubuntu). Bei anderen Distributionen befindet sich libc an einem anderen Pfad; entweder explizit angeben oder --root / verwenden, um die integrierte Lookup-Tabelle von der Container-Root aus zu durchsuchen:
# Fedora / Rocky / CentOS
victim$ ./page_inject /usr/lib64/libc.so.6
# Arch
victim$ ./page_inject /usr/lib/libc.so.6
# Auto-Erkennung, unabhängig von der Distribution:
victim$ ./page_inject --root /
Jeder Aufruf bewirkt dasselbe: ELF-Parsing der libc im Container, Installation des Hooks in ihrem Page-Cache, Überwachung der Slottabelle für ca. 30 Sekunden, während sich andere Container registrieren, und einmaliges Ausführen von id gegen den ersten registrierten anderen Container als Korrektheitsprüfung.
Nach dem Bootstrap kann man mit der Befehlsshell auf jeden registrierten anderen Container zugreifen: