Dirty Frag: как работает новый Linux LPE

от автора

Универсальный LPE на актуальных ядрах Linux: один бинарь поднимает root на Ubuntu 24.04, RHEL 10.1, AlmaLinux 10, Fedora 44, openSUSE Tumbleweed. CVE пока нет, PoC лежит в открытом репозитории. Разбираем механику, объясняем, почему это не «ещё один Dirty Pipe», и даём чеклист действий для системных администраторов.

Контекст

7 мая 2026 года Hyunwoo Kim (@v4bel) опубликовал полный write-up и рабочий PoC уязвимости, которую он назвал Dirty Frag. Это локальное повышение привилегий (LPE) до root, которое работает на всех мажорных дистрибутивах с актуальными ядрами – от 6.12 до 7.0.

  • 29 апреля – автор отправляет отчёт по RxRPC-варианту в security@kernel.org и патч в netdev.

  • 30 апреля – туда же уходит ESP-вариант. Информация по обеим уязвимостям появляется в публичных рассылках kernel-сообщества.

  • 7 мая – Kim отправляет детали и эксплойт в linux-distros@vs.openwall.org с эмбарго на 5 дней. Условие стандартное: если кто-то третий публикует PoC до окончания эмбарго – автор сразу выкатывает свою версию.

  • 7 мая (через несколько часов) – неизвестная сторона публикует эксплойт ESP-варианта. По обсуждению в LWN и Hacker News видно, что параллельно несколько человек самостоятельно разработали PoC по уже опубликованному в netdev патчу – формально эмбарго не нарушали, но результат тот же.

  • 7 мая (вечером) – по согласованию с мейнтейнерами дистрибутивов автор публикует полный документ с PoC на GitHub.

Что мы имеем на руках:

  • CVE не назначено ни одной из двух уязвимостей цепочки.

  • Патч ESP-варианта уже в netdev (смержен 7 мая), патч RxRPC – пока только в LKML.

  • В дистрибутивах патчи в работе: AlmaLinux уже выкатил тестовые ядра, CloudLinux готовит KernelCare livepatch. Остальные – в режиме «ждём бэкпорта».

  • Жизненный цикл бага: ESP-вариант существует с коммита cac2661c53f3 (январь 2017), RxRPC – с 2dc334f1a63a (июнь 2023). Около 9 лет на ESP-стороне.

Механика уязвимости

Dirty Frag – это не одна, а две независимые уязвимости одного класса, объединённые в общий exploit chain. Класс – тот же, что у Dirty Pipe и Copy Fail: запись в page cache файлов, к которым у атакующего есть только read-доступ. Только Dirty Pipe «грязнил» struct pipe_buffer, а Dirty Frag «грязнит» frag в struct sk_buff.

Общая идея

Атакующий через splice(file -> pipe -> socket) с MSG_SPLICE_PAGES подсаживает в frag отправляющего skb прямую ссылку на страницу page cache, например, страницу /usr/bin/su или /etc/passwd. Этот же skb проходит через loopback и попадает в receiving-путь ядра, где криптокод выполняет in-place crypto прямо поверх этой страницы. AEAD-проверка падает с ошибкой, но 4–8 байт в page cache уже изменены – и сохраняются до перезагрузки или drop_caches.

Вариант 1: xfrm-ESP Page-Cache Write

Точка входа – esp_input() в net/ipv4/esp4.c и net/ipv6/esp6.c. Перед in-place AEAD-расшифровкой нелинейного skb код должен позвать skb_cow_data() и скопировать данные frag в приватный буфер. Но в исходном коде есть ветка, которая пропускает COW:

cif (!skb_cloned(skb)) {    if (!skb_is_nonlinear(skb)) {        nfrags = 1;        goto skip_cow;    } else if (!skb_has_frag_list(skb)) {        nfrags = skb_shinfo(skb)->nr_frags;        nfrags++;        goto skip_cow;          // вот сюда улетает skb с подставленной страницей    }}

if (!skb_cloned(skb)) { if (!skb_is_nonlinear(skb)) { nfrags = 1; goto skip_cow; } else if (!skb_has_frag_list(skb)) { nfrags = skb_shinfo(skb)->nr_frags; nfrags++; goto skip_cow; // вот сюда улетает skb с подставленной страницей } }

Дальше при комбинации ESP + ESN + authencesn(hmac(sha256), cbc(aes)) функция crypto_authenc_esn_decrypt() в рамках перестановки байт seqno делает 4-байтный STORE по адресу assoclen + cryptlen в dst SGL:

cscatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1);

scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1);

Атакующий контролирует и адрес, и значение этих 4 байт:

  • Адрес – через выбор длины payload (точно подгоняется так, чтобы попасть в нужный file offset страницы P).

  • Значение – это seq_hi из XFRMA_REPLAY_ESN_VAL, который пользователь сам выставляет при регистрации SA через netlink.

То есть это arbitrary 4-byte STORE primitive в page cache. AEAD-проверка после STORE возвращает -EBADMSG, но изменение уже сделано.

Цена входа: для регистрации XFRM SA нужен CAP_NET_ADMIN, поэтому эксплойт делает unshare(CLONE_NEWUSER | CLONE_NEWNET). На дистрибутивах, где разрешено создание непривилегированных user namespace, всё работает.

Вариант 2: RxRPC Page-Cache Write

Точка входа – rxkad_verify_packet_1() в net/rxrpc/rxkad.c. На уровне RXRPC_SECURITY_AUTH ядро делает in-place pcbc(fcrypt) decrypt первых 8 байт payload skb:

cret = skb_to_sgvec(skb, sg, sp->offset, 8);memset(&iv, 0, sizeof(iv));skcipher_request_set_crypt(req, sg, sg, 8, iv.x);   // src == dst, in-placeret = crypto_skcipher_decrypt(req);

ret = skb_to_sgvec(skb, sg, sp->offset, 8); memset(&iv, 0, sizeof(iv)); skcipher_request_set_crypt(req, sg, sg, 8, iv.x); // src == dst, in-place ret = crypto_skcipher_decrypt(req);

Проверка перед этим – только skb_cloned(skb), но не skb->data_len, поэтому нелинейный skb с подсаженной страницей в frag проскакивает.

Тут есть нюанс: STORE-значение – это не произвольные байты, а результат fcrypt_decrypt(C, K), где C – уже лежащий по этому смещению ciphertext, а K – сессионный ключ из RxRPC v1 token (его атакующий сам кладёт через add_key("rxrpc", ...), без каких-либо привилегий).

fcrypt – это AFS-cipher с 56-битным ключом и блоком 8 байт. Детерминированная функция, легко портируется в user-space. Эксплойт перебирает K локально (~18 M ключей/сек) до тех пор, пока fcrypt_decrypt(C, K) не выдаст нужный паттерн plaintext. Полные 8 байт перебрать нереально (~2⁵⁶), но если фиксировать только 2–3 ключевых байта – подбор укладывается в миллисекунды.

Цель – не перезаписать всё /etc/passwd, а превратить первую строку в:

textroot::0:0:GGGGGG:/root:/bin/bash

root::0:0:GGGGGG:/root:/bin/bash

Пустое поле пароля + pam_unix.so nullok в PAM common-auth = su - без запроса пароля. Дальше – setresuid(0,0,0) и /bin/bash от root.

Главное: никаких namespace, никакого CAP_*. Только add_key()socket(AF_RXRPC)splice()recvmsg() – всё доступно обычному непривилегированному пользователю.

Почему это не Dirty Pipe-2

Dirty Pipe был race-condition-free, но опирался на специфическое состояние struct pipe_buffer – нужно было «протолкнуть» нужные флаги через splice. Dirty Frag устроен ещё прямолинейнее: in-place crypto по определению пишет в dst, а dst – это страница page cache, на которую у атакующего read-only доступ.

  • Не нужен timing window. Между splice и crypto-STORE нет конкурентного события, которое надо успеть.

  • Не нужен race. Логика детерминированная: подсадили страницу – она прошла до sink – STORE гарантирован.

  • Ядро не паникует при неудаче. AEAD/HMAC возвращает ошибку, skb просто дропается, никаких BUG/oops.

  • Высокий success rate. На тестовых стендах – фактически 100%.

Чем это отличается от Dirty Pipe и Copy Fail

Параметр

Dirty Pipe (CVE-2022-0847)

Copy Fail (CVE-2026-31431)

Dirty Frag (CVE pending)

Объект записи

struct pipe_buffer

TX SGL поверх page cache

frag поверх page cache

Sink

copy_page_to_iter_pipe

scatterwalk_map_and_copy в authencesn

authencesn в esp_input + pcbc(fcrypt) в rxkad_verify_packet_1

Triggering subsystem

pipe + splice

AF_ALG (algif_aead)

xfrm/IPsec ESP + RxRPC

Race condition

Нет

Нет

Нет

Размер примитива

произвольная запись

4 байта

4 байта (ESP) / 8 байт (RxRPC)

Привилегии

непривилегированный user

непривилегированный user (если algif_aead доступен)

CAP_NET_ADMIN через user namespace (ESP) или ничего (RxRPC)

Workaround

патч ядра

blacklist algif_aead

blacklist esp4esp6rxrpc

Mitigation Copy Fail помогает?

Нет, обходится

Affected ядра

5.8+

актуальные

ESP: с 4.10 (2017), RxRPC: с 6.5 (2023)

CVE

назначено

назначено

не назначено

Главный вывод из таблицы – публично известный workaround Copy Fail (algif_aead blacklist) не защищает от Dirty Frag. Это уже отдельный sink, через другой crypto-путь.

Кого это реально касается

К счастью не все среды одинаково уязвимы.

Высокий риск

  • Shared hosting и VPS-провайдеры. Любой клиент с обычной shell-сессией поднимается до root на хост-ноде, если применима cgroup-политика без user namespace lockdown. Для CloudLinux уже отдельный пост с готовым livepatch.

  • Kubernetes worker nodes. Контейнер с hostPID: false и стандартным seccomp всё равно даёт доступ к add_key()socket(AF_RXRPC)splice(). RxRPC-вариант здесь работает «из коробки» на Ubuntu-нодах, где модуль грузится по умолчанию. ESP-вариант – на нодах с kernel.unprivileged_userns_clone=1.

  • CI/CD runners (GitLab Runner, GitHub Actions self-hosted, Jenkins agents). Любой PR от внешнего контрибьютора в публичном репозитории = выполнение произвольного кода в runner-окружении. Если runner общий между проектами – компрометация одного проекта = компрометация всех.

  • Multi-tenant sandbox-окружения: code playgrounds, ML-тренажёры, песочницы для статей и обучения.

Средний риск

  • Серверы с локальными непривилегированными пользователями. Корпоративные jump-hosts, dev-боксы, build-серверы. Если у пользователя есть SSH – у него есть root.

  • Контейнерные среды без user namespace lockdown. Docker без userns-remap, podman в rootfull-режиме на Ubuntu.

Пониженный риск

  • Десктопы с одним пользователем. Без локального доступа постороннего эксплойт не запустить.

  • Single-purpose серверы и сетевые аплайансы. Об этом подробнее ниже.

Наш продукт Ideco NGFW

Уязвим ли Ideco NGFW?

Технически ядро Linux, на котором работает Ideco NGFW, относится к классу затронутых. Но реальный attack surface для Dirty Frag на NGFW принципиально другой:

  • На Ideco NGFW нет shell-сессий обычных пользователей. Управление – через web-консоль и SSH под выделенными аккаунтами администраторов, без splice/add_key-сценариев.

  • Нет CI/CD-нагрузок, нет компиляции произвольного кода, нет multi-tenant изоляции внутри аплайанса.

  • Trigger-вектор Dirty Frag требует локального исполнения произвольного бинаря под непривилегированным пользователем. На правильно сконфигурированном NGFW такой возможности у внешнего атакующего просто нет – нужен сначала RCE из сети, а это уже другая категория угрозы.

То есть для Dirty Frag на NGFW справедлив принцип «не первая дверь, которую можно сломать». Тем не менее, мы готовим обновление с патчем netdev сразу, как только апстрим стабилизируется и пройдёт регрессионное тестирование.

Что делать прямо сейчас

Минимальный чеклист для системного администратора Linux. Порядок шагов важен.

1. Проверить, загружены ли уязвимые модули

lsmod | grep -E '^(esp4|esp6|rxrpc) '

Если ничего не выводится – модули не загружены, риск минимальный (но не нулевой: модули могут подгружаться автоматически по запросу через MODULE_ALIAS_NETPROTO).

2. Оценить, нужны ли модули в проде

  • esp4/esp6 нужны, если есть IPsec (strongSwan, libreswan, racoon, FRR с IPsec-туннелями, любые kernel-mode VPN).

  • rxrpc нужен только при использовании AFS (Andrew File System) – на 99% серверов не используется, особенно на Ubuntu, где он грузится по умолчанию «на всякий случай».

  • WireGuard, OpenVPN, Tailscale, Nebula, GRE – не используют ни esp4/esp6, ни rxrpc. На них не повлияет blacklist модулей esp.

3. Применить workaround там, где это безопасно

Для серверов без IPsec:

bashsudo sh -c "printf 'install esp4 /bin/false\ninstall esp6 /bin/false\ninstall rxrpc /bin/false\n' > /etc/modprobe.d/dirtyfrag.conf; rmmod esp4 esp6 rxrpc 2>/dev/null; true"sudo sync && echo 3 | sudo tee /proc/sys/vm/drop_caches

sudo sh -c "printf 'install esp4 /bin/false\ninstall esp6 /bin/false\ninstall rxrpc /bin/false\n' > /etc/modprobe.d/dirtyfrag.conf; rmmod esp4 esp6 rxrpc 2>/dev/null; true" sudo sync && echo 3 | sudo tee /proc/sys/vm/drop_caches

Для серверов с IPsec – blacklist’ить только rxrpc:

bashsudo sh -c "printf 'install rxrpc /bin/false\n' > /etc/modprobe.d/dirtyfrag-rxrpc.conf; rmmod rxrpc 2>/dev/null; true"

sudo sh -c "printf 'install rxrpc /bin/false\n' > /etc/modprobe.d/dirtyfrag-rxrpc.conf; rmmod rxrpc 2>/dev/null; true"

Это закрывает один из двух векторов цепочки на Ubuntu (где rxrpc загружается по умолчанию даже без AFS). ESP-вариант остаётся открытым до выхода патча от вендора, но требует возможности создания user namespace.

4. Закрыть user namespace там, где они не нужны

На Ubuntu/Debian:

bashsudo sysctl -w kernel.unprivileged_userns_clone=0echo 'kernel.unprivileged_userns_clone = 0' | sudo tee /etc/sysctl.d/99-no-userns.conf

sudo sysctl -w kernel.unprivileged_userns_clone=0 echo 'kernel.unprivileged_userns_clone = 0' | sudo tee /etc/sysctl.d/99-no-userns.conf

На RHEL/Alma/Rocky/Fedora:

bashsudo sysctl -w user.max_user_namespaces=0echo 'user.max_user_namespaces = 0' | sudo tee /etc/sysctl.d/99-no-userns.conf

sudo sysctl -w user.max_user_namespaces=0 echo 'user.max_user_namespaces = 0' | sudo tee /etc/sysctl.d/99-no-userns.conf

Учтите, что это сломает rootless Docker/Podman, Firefox sandbox в некоторых конфигурациях, и часть инструментов вроде bwrap/flatpak. Применять там, где это допустимо.

5. Подписаться на security-уведомления и обновиться, как только выйдет патч

После накатки патча проверить отсутствие SKBFL_SHARED_FRAG-обхода:

bashzgrep -E 'SKBFL_SHARED_FRAG|skb_has_shared_frag' /proc/config.gz 2>/dev/null# или, если /proc/config.gz нет:modinfo esp4 | grep -i version

zgrep -E 'SKBFL_SHARED_FRAG|skb_has_shared_frag' /proc/config.gz 2>/dev/null # или, если /proc/config.gz нет: modinfo esp4 | grep -i version

И отдельно – проверка следов компрометации. Если на сервере уже запускался PoC, в page cache могут оставаться модифицированные копии /usr/bin/su и /etc/passwd. Грубая проверка:

bash# Сравнить хеш файла на диске и через cat (page cache)sha256sum /usr/bin/sucat /usr/bin/su | sha256sumdiff <(getent passwd root) <(grep '^root:' /etc/passwd)

# Сравнить хеш файла на диске и через cat (page cache) sha256sum /usr/bin/su cat /usr/bin/su | sha256sum diff <(getent passwd root) <(grep '^root:' /etc/passwd)

При расхождении – echo 3 > /proc/sys/vm/drop_caches и обязательная перезагрузка с проверкой целостности через rpm -V / dpkg --verify / debsums. Если расхождение остаётся после этого – исходим из факта компрометации хоста.

Итог

Dirty Frag – пример того, что класс «in-place crypto over splice’d page» оказался шире, чем казалось после Copy Fail. Локальное повышение привилегий до root на всех мажорных дистрибутивах через детерминированный баг без race condition, без CVE, с публичным PoC и без готовых патчей в большинстве дистрибутивов – это серьезная проблема безопасности.

Мы в Ideco следим за этой историей с момента появления первой публикации в netdev 30 апреля. Мы уже включили в план ближайших минорных релизов выпуска патчей для поддерживаемых версий – как только апстрим стабилизирует фикс.

Похоже, что ближайшие месяцы принесут ещё несколько уязвимостей этого класса – в других подсистемах, где тоже есть splice → in-place transform → STORE.

ссылка на оригинал статьи https://habr.com/ru/articles/1032842/