
Эксплойт PoC на Rust для LPE CVE-2026-31431 в ядре Linux (запись в page-cache через AF_ALG), а также защита на этапе выполнения с помощью eBPF, блокирующая создание сокетов AF_ALG через LSM или kprobe без перезагрузки.
CVE-2026-31431 (Copy Fail) — это локальная уязвимость повышения привилегий в модуле algif_aead ядра Linux (подсистема AF_ALG). Логическая ошибка, внесённая в августе 2017 года, позволяет любому непривилегированному локальному пользователю записать 4 контролируемых байта в page cache любого читаемого файла через AF_ALG + splice(), а затем выполнить повреждённый setuid-бинарник для получения root. Эксплойт детерминированный — без гонок, без офсетов ядра, без краха системы. Он затрагивает все основные дистрибутивы Linux с ядрами от 4.14 до 7.0-rc.
CVSS: 7.8 | Исправлено в: ядро 7.0, 6.19.12, 6.18.22 | Исправление в mainline: коммит a664bf3d603d
Источники: copy.fail, The Hacker News, уведомление CloudLinux
crates/
├── exp/ # Эксплойт PoC (реализация на Rust)
├── copy_fail_guard/ # Пользовательский загрузчик eBPF — режим LSM
├── copy_fail_guard-ebpf/ # Программа eBPF LSM (не в workspace)
├── copy_fail_guard_kprobe/ # Пользовательский загрузчик eBPF — режим kprobe
└── copy_fail_guard_kprobe-ebpf/ # Программа eBPF kprobe (не в workspace)
scripts/
└── build_guard.sh # Сборка в один клик → вывод в dist/
Корневая причина — цепочка из трёх независимых функций ядра, небезопасно взаимодействующих друг с другом:
AF_ALG — открывает крипто-API ядра непривилегированному пользовательскому пространствуsplice() — передаёт данные файла с нулевым копированием как ссылки на page cache (не копии) в scatterlist криптоподсистемыauthencesn — использует выходной буфер вызывающего как временное пространство, записывая 4 байта по адресу dst[assoclen + cryptlen]В 2017 году оптимизация на месте в algif_aead.c (72548b093ee3) сделала req->src == req->dst, связав страницы page cache с доступным для записи выходным scatterlist. Когда authencesn записывает свои временные байты, он выходит за пределы выходного буфера в связанные страницы page cache. Атакующий контролирует:
Повреждённая страница никогда не помечается как грязная — файл на диске не затрагивается, но execve() читает из page cache. Повредите setuid-бинарник → root.
Реализация на Rust публичного PoC на Python объёмом 732 байта. Нацелен на /usr/bin/su, вставляет его страницы page cache в AEAD-сокет AF_ALG и перезаписывает их сжатой shell-нагрузкой.
Требуется Rust 1.85+ (edition 2024).
cargo build --release -p copy_fail
Бинарник только для Linux. На других платформах он завершается с ошибкой Unsupported.
Предупреждение: Это эксплуатирует реальную уязвимость ядра. Запускайте только на системах, которыми вы владеете и управляете, в идеале — на одноразовой виртуальной машине.
Загрузите ВМ с уязвимым ядром (любой массовый дистрибутив с ядром < 7.0 / < 6.19.12 / < 6.18.22).
Скопируйте собранный бинарник в ВМ и запустите его от непривилегированного пользователя:
./target/release/copy_fail
Уязвимая система: появляется root-шелл (#). Выполните whoami, чтобы подтвердить root.
Не уязвимая система (исправленное ядро): операция AF_ALG завершается ошибкой, или бинарник su ведёт себя нормально. Вы увидите ошибку или обычный запрос пароля su.
Средство защиты ядра в реальном времени, которое блокирует CVE-2026-31431 без обновления ядра или перезагрузки. Оно использует eBPF для перехвата создания сокетов AF_ALG, отсекая первый шаг эксплойта.
Предоставляются два режима. Загрузчик в один клик автоматически выбирает лучший доступный режим:
| Режим LSM | Режим kprobe | |
|---|---|---|
| Как блокирует | Возвращает -EPERM (создание сокета запрещено) | SIGKILL (процесс убит) |
| Требования к ядру | ≥ 5.7 с параметром загрузки lsm=bpf | ≥ 5.3, без специальных параметров |
| Нужна перезагрузка для включения | Возможно (если lsm=bpf ещё не задан) | Нет |
| Точка перехвата | LSM-хук socket_create | kprobe на __sys_socket |
┌──────────────────────────────────────────────────┐
│ run_guard.sh │
│ • Определяет поддержку BPF LSM │
│ • LSM доступен → copy_fail_guard (EPERM) │
│ • LSM недоступен → copy_fail_guard_kprobe (KILL)│
└──────────────┬───────────────────────────────────┘
│
┌──────────────▼───────────────────────────────────┐
│ Программа eBPF │
│ если семейство сокета == 38 (AF_ALG) │
│ → блокировать (EPERM или SIGKILL) │
│ иначе │
│ → разрешить │
└──────────────────────────────────────────────────┘
Блокировка AF_ALG оказывает почти нулевое влияние на типичные системы:
AF_ALGAF_ALG (например, OpenSSL с включённым движком afalg, некоторые пути аппаратного ускорения криптографии во встраиваемых системах)socket(AF_ALG, ...)Сборка в один клик (рекомендуется):
./scripts/build_guard.sh
Это автоматически устанавливает недостающие тулчейны (nightly, bpf-linker), компилирует все программы eBPF и пользовательские загрузчики и выводит всё в dist/:
dist/
├── copy_fail_guard.bpf.o # Программа eBPF LSM
├── copy_fail_guard_kprobe.bpf.o # Программа eBPF kprobe
├── copy_fail_guard # Пользовательский загрузчик (LSM)
├── copy_fail_guard_kprobe # Пользовательский загрузчик (kprobe)
└── run_guard.sh # Автовыбор: sudo ./run_guard.sh
Скопируйте каталог dist/ на любую целевую машину и выполните sudo ./run_guard.sh для активации защиты.
Ручная сборка (по шагам):
Шаг 1: Скомпилируйте программы eBPF (обязательно на Linux):
# Вариант LSM
cd crates/copy_fail_guard-ebpf
cargo +nightly build --target bpfel-unknown-none -Z build-std=core --release
# Вариант kprobe
cd crates/copy_fail_guard_kprobe-ebpf
cargo +nightly build --target bpfel-unknown-none -Z build-std=core --release
Шаг 2: Соберите пользовательские загрузчики:
cargo build --release -p copy_fail_guard -p copy_fail_guard_kprobe
Рекомендуется — автоматический выбор лучшего режима:
sudo ./dist/run_guard.sh
Вручную — запуск конкретного режима:
# Режим LSM (требуется lsm=bpf)
sudo GUARD_BPF_OBJ=path/to/copy_fail_guard.bpf.o RUST_LOG=info ./copy_fail_guard
# Режим kprobe (работает везде)
sudo GUARD_BPF_OBJ=path/to/copy_fail_guard_kprobe.bpf.o RUST_LOG=info ./copy_fail_guard_kprobe
Нажмите Ctrl-C, чтобы отключить программу eBPF и восстановить нормальное поведение.
С запущенной защитой в одном терминале:
# В другом терминале попробуйте эксплойт:
./target/release/copy_fail
# Режим LSM: "error: Operation not permitted"
# Режим kprobe: "已杀死" / "Killed"
# Или проверьте напрямую через Python:
python3 -c "import socket; socket.socket(38, 5, 0)"
# Режим LSM: PermissionError: [Errno 1] Operation not permitted
# Режим kprobe: Killed
AF_ALG. Нет механизма исключения конкретных процессов по PID, cgroup или имени команды. Это запланировано на будущую версию (через eBPF HashMap). Для подавляющего большинства систем это нормально, поскольку почти ничто не использует AF_ALG.bpf-linker (они нацелены на bpfel-unknown-none и не могут быть обычными членами workspace).Большинство дистрибутивов не включают BPF LSM по умолчанию. Если вы хотите более чистый режим LSM (EPERM вместо SIGKILL):
# Проверьте текущие LSM:
cat /sys/kernel/security/lsm
# Если "bpf" отсутствует, добавьте его:
sudo sed -i 's/^GRUB_CMDLINE_LINUX="\(.*\)"/GRUB_CMDLINE_LINUX="\1 lsm=lockdown,capability,yama,apparmor,bpf"/' /etc/default/grub
sudo update-grub && sudo reboot
Режим kprobe работает без этого шага.
После обновления ядра до ≥ 7.0 / ≥ 6.19.12 / ≥ 6.18.22:
# Подтвердите версию ядра
uname -r
# Повторно запустите эксплойт — он больше не должен давать root-шелл
./target/release/copy_fail
В качестве альтернативы подтвердите, что уязвимый модуль нейтрализован:
modinfo algif_aead | grep filename
# Если встроен в ядро, заблокируйте initcall (требуется перезагрузка)
sudo grubby --update-kernel=ALL --args="initcall_blacklist=algif_aead_init"
sudo reboot