
CVE-2026-31431-killed page-cache эксплойт — выполнение кода в контейнерах, совместно использующих один и тот же слой образа
Межконтейнерный эксплойт уязвимости AF_ALG aead — переход из одного скомпрометированного контейнера во все соседние контейнеры, использующие тот же слой образа libc.so.6.
Это примитив эскейпа: он запускается изнутри непривилегированного контейнера, который атакующий уже скомпрометировал, и использует 4-байтовую ошибку произвольной записи при ротации ESN в authencesn из AF_ALG (CVE-2026-31431), чтобы внедрить постоянный хук read() в страницы page-cache файла libc.so.6. Поскольку в Docker / containerd файлы нижних слоёв overlayfs опираются на общие inode, эти страницы видны каждому соседнему контейнеру, созданному из того же образа: хук срабатывает и в их процессах, и атакующий получает выполнение команд в каждом из них.
victim) на хосте, где работают другие контейнеры (siblings) из того же образа, что и victim.victim работает в стандартной конфигурации Docker/k8s: непривилегированный uid внутри пользовательского пространства имён контейнера, профиль seccomp по умолчанию, профиль AppArmor по умолчанию, без специальных capabilities, без bind-монтирований хоста.victim имеет только:
/usr/lib/x86_64-linux-gnu/libc.so.6 или туда, куда её устанавливает дистрибутив)socket(AF_ALG, ...)splice / vmsplicechmod +x (например, /tmp)algif_aead + authencesn до исправления-отката в апстриме).На этом всё. Никаких специальных CAP_*, никакого доступа к файловой системе хоста. Атакующий помещает самодостаточный статически слинкованный бинарник в контейнер, запускает его — и повреждение page-cache, а следовательно и хук, становится видимым для всех соседних контейнеров.
Идентичность страниц page-cache. В контейнере на overlayfs путь /usr/lib/.../libc.so.6 обслуживается ext4-inode нижнего слоя образа. Каждый контейнер, запущенный из того же образа, разделяет этот нижележащий inode, а page-cache ядра индексируется по нижележащему inode — не по overlay и не по пространству имён. Поэтому одна 4-байтовая запись в страницу page-cache видна всем процессам соседних контейнеров, у которых эта страница отображена через mmap.
Уязвимость AF_ALG aead превращает одну такую запись во множество. algif_aead сцепляет пользовательский RX iovec с хвостовыми authsize байтами сцепленного TX SGL, а при ротации ESN в authencesn 4 байта поля seq_high из AAD размещаются в dst[assoclen + cryptlen] — то есть в первом байте этого сцепленного чужого хвоста. Сцепленная страница — это страница page-cache файла, к которому атакующий имеет только доступ на чтение, но шифр всё равно копирует в неё байты, не помечая страницы как dirty. (Подробнее о механике — в crypto/algif_aead.c и crypto/authencesn.c.)
Инициализация вызываемого примитива. Первым делом page_inject разворачивает зону A — закодированную на asm повторную реализацию той же схемы AF_ALG (write_cache.asm), размещённую в кармане секции .text libc. Это превращает 4-байтовую запись в обычный call из любого будущего кода хука — без необходимости настраивать сокет для каждого вызова.
Установка хука. Затем инжектор записывает зону C (zone_c.asm) в карман секции .text libc и патчит первые 7–12 байт read() переходом E9 disp32 на неё. Вытесняемые байты пролога точно эмулируются в быстром пути зоны C (распознаются три разных пролога glibc — см. «Обработка пролога read()» ниже). Теперь хук активен в page-cache libc.
Распространение хука. В каждом соседнем контейнере процессы постоянно вызывают read() (демоны журналирования, healthcheck-скрипты, cat /etc/hostname — что угодно). При первом таком вызове внутри соседнего контейнера перехваченный пролог прыгает в зону C, которая:
stat("/") для корневого inode контейнера (стабильный идентификатор для каждого пространства имён) и использует его как ключ слота контейнера;fork()-ает долгоживущего дочернего процесса командного цикла, который опрашивает область CMD на предмет команд;read()+N, так что вызывающий код ничего не замечает.
Исходный процесс соседнего контейнера продолжает работать. С этого момента у атакующего есть демон внутри этого контейнера.Канал команд. Атакующий использует тот же бинарник page_inject в режиме --shell, чтобы записывать команды в область CMD региона слотов. Дочерний процесс хука каждого зарегистрированного соседнего контейнера опрашивает её, запускает /bin/sh -c <cmd>, захватывает stdout/stderr в область OUTPUT, сигнализирует о завершении и возвращается к опросу. Shell показывает вывод. Поскольку каждая запись в CMD/OUTPUT тоже идёт через примитив уязвимости, никаких особых привилегий не требуется.
Снятие хука. По завершении работы unhook восстанавливает исходные байты пролога read() и обнуляет таблицу слотов; дочерние процессы хуков при следующей итерации видят пустой слот и завершаются сами. Сами изменения page-cache чистые (ядро никогда не помечало изменённые страницы как dirty), поэтому как только все контейнеры с отображённой через mmap libc остановлены, drop_caches полностью восстанавливает кэш — на диске не остаётся никаких следов.
Инжектор собирается вне контейнера-жертвы — как правило, на собственной машине разработки атакующего, — потому что в большинстве продакшен-образов контейнеров нет компилятора. Достаточно стандартного окружения разработки Linux x86_64 с gcc (с поддержкой статической линковки -static) и nasm.
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
Результат сборки — единый статически слинкованный ELF (./page_inject), который запускается на любом современном ядре Linux x86_64.
Получив shell в victim, атакующий загружает бинарник в доступный для записи каталог (обычно /tmp):
# inside the compromised container, attacker session
victim$ ./page_inject
Без аргументов page_inject использует путь по умолчанию /usr/lib/x86_64-linux-gnu/libc.so.6 (расположение Debian/Ubuntu после объединения каталогов). В других дистрибутивах libc лежит по другому пути; можно либо передать его явно, либо использовать --root /, чтобы просканировать встроенную таблицу путей от корня контейнера:
# 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 /
Оба варианта запуска делают одно и то же: разбирают ELF внутриконтейнерной libc, устанавливают хук в её page-cache, ~30 секунд наблюдают за таблицей слотов, пока регистрируются соседние контейнеры, и в качестве проверки работоспособности однократно выполняют id в первом зарегистрировавшемся соседнем контейнере.
После начальной установки можно войти в интерактивную командную оболочку и управлять любым зарегистрированным соседним контейнером:
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 ...