
Копирование сбоя: 732 байта до root на каждом крупном дистрибутиве Linux.
732 байта. Любой дистрибутив. Root.
Прямолинейная логическая ошибка в криптографическом шаблоне
authencesnядра Linux позволяет непривилегированному локальному пользователю выполнить точную контролируемую запись 4 байт в page cache любого читаемого файла — включая setuid-бинарники. Без гонок. Без повторов. Без перекомпиляции. Root на каждом крупном дистрибутиве Linux, выпущенном с 2017 года.
📄 Технический отчёт · 🔗 Патч ядра · 🛡️ CVSS: Критический
| Дистрибутив | Версия ядра |
|---|---|
| Ubuntu 24.04 LTS | 6.17.0-1007-aws |
| Amazon Linux 2023 | 6.18.8-9.213.amzn2023 |
| RHEL 10.1 | 6.12.0-124.45.1.el10_1 |
| SUSE 16 | 6.12.0-160000.9-default |
Все четыре были взломаны с помощью идентичного Python-скрипта размером 732 байта, без изменений.
AF_ALG предоставляет непривилегированному пользовательскому пространству доступ к криптоподсистеме ядра. splice() передаёт данные файла в pipe по ссылке — передавая страницы page cache напрямую, без копирования. Когда пользователь выполняет splice файла в AEAD-сокет AF_ALG, входной scatterlist сокета содержит живые ссылки на кэшированные страницы этого файла в ядре.
В algif_aead.c оптимизация на месте 2017 года копировала AAD и шифротекст из TX scatterlist в RX-буфер, но связывала страницы тега аутентификации по ссылке с помощью sg_chain(), а затем устанавливала req->src = req->dst:
Входной SGL: [ AAD | CT | Tag ]
^
└─ sg_chain() → всё ещё указывает на страницы page cache
Выходной SGL: [ AAD | CT ] ──→ [ Tag (страницы page cache) ]
(RX-буфер) (связаны из TX SGL)
req->src ──┐
├──→ тот же объединённый scatterlist
req->dst ──┘
Страницы page cache из splice() теперь находились внутри записываемого целевого scatterlist, отделённые от легитимной области записи только границей смещения. Ничто в API не гарантировало, что алгоритмы будут оставаться в пределах границ.
authencesnauthencesn — это AEAD-обёртка, используемая IPsec для поддержки 64-битных расширенных порядковых номеров (ESN). Для перестановки байтов ESN при вычислении HMAC она использует целевой буфер вызывающего как scratch-пространство — включая запись по смещению assoclen + cryptlen, которое находится за границей тега аутентификации:
scatterwalk_map_and_copy(tmp, dst, 0, 8, 0); // чтение AAD[0..7]
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1); // перезапись dst[4..7]
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); // ← запись за пределы тега
Третий вызов записывает 4 байта (seqno_lo) по адресу dst[assoclen + cryptlen]. В пути AF_ALG с операцией на месте scatterwalk пересекает границу из RX-буфера в связанные страницы page cache тега. Ядро отображает страницу page cache через kmap_local_page и записывает напрямую в кэшированную копию целевого файла.
Затем HMAC завершается ошибкой (шифротекст сфабрикован), recvmsg() возвращает ошибку — но запись 4 байт сохраняется навсегда.
Ни одно отдельное изменение не было ошибочным само по себе. Уязвимость находится на пересечении всех трёх.
Целевой файл по умолчанию — /usr/bin/su, setuid-root бинарник, присутствующий во всех протестированных дистрибутивах.
Шаг 1 — Настройка сокета
Открыть AF_ALG сокет, привязать к authencesn(hmac(sha256),cbc(aes))
Установить ключ. Принять request-сокет. (Привилегии не требуются.)
Шаг 2 — Цикл записи (один раз на каждый 4-байтовый фрагмент шеллкода)
sendmsg() → байты AAD [4:8] несут 4 байта для записи (seqno_lo)
splice() → страницы page cache целевого файла в AF_ALG сокет
recv() → запускает decrypt → authencesn записывает seqno_lo в page cache
(recvmsg возвращает ошибку; запись сохраняется)
Шаг 3 — Выполнение
execve("/usr/bin/su")
Ядро загружает бинарник из (теперь повреждённого) page cache
Setuid-root бинарник выполняет внедрённый шеллкод → UID 0
a = socket.socket(38, 5, 0) # AF_ALG, SOCK_SEQPACKET
a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
# ... установить ключ, принять request-сокет u ...
u.sendmsg([b"A"*4 + payload_chunk], [cmsg_headers], MSG_MORE)
os.splice(target_fd, pipe_wr, offset)
os.splice(pipe_rd, alg_fd, offset)
u.recv(...) # запускает запись в page cache
Обновите ядро до версии, содержащей патч a664bf3d603d. Исправление возвращает algif_aead.c к работе вне места: req->src указывает на TX SGL; req->dst указывает на RX-буфер. Страницы page cache из splice() остаются доступными только для чтения. Механизм sg_chain(), связывавший их с записываемым целевым scatterlist, удалён.
// До (уязвимо): src и dst используют один и тот же scatterlist
aead_request_set_crypt(&areq->cra_u.aead_req, rsgl_src, rsgl_src, used, ctx->iv);
// После (исправлено): src — TX SGL, dst — RX-буфер — полностью разделены
aead_request_set_crypt(&areq->cra_u.aead_req, tsgl_src, rsgl_dst, used, ctx->iv);
Отключите модуль ядра algif_aead:
echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif-aead.conf
rmmod algif_aead 2>/dev/null
Или заблокируйте создание сокетов AF_ALG с помощью политики seccomp в профилях ваших рабочих нагрузок.
Примечание для контейнерных сред: Поскольку page cache является общим для хоста, эта уязвимость пересекает границы контейнеров. Применяйте меры смягчения на уровне узла, а не только для отдельных подов. Полные детали побега из Kubernetes см. в Части 2.
Исследователь Theori Taeyang Lee выявил в ходе предыдущей работы над kernelCTF, что AF_ALG + splice() создаёт путь, по которому непривилегированное пользовательское пространство может передавать страницы page cache напрямую в криптоподсистему — и что происхождение страниц в scatterlist является недостаточно изученным классом уязвимостей.
Исследовательская группа использовала Xint Code для масштабирования этого наблюдения на всю подсистему crypto/ со следующим операторским промптом:
"Это подсистема linux crypto/. Изучите все пути кода, достижимые из пользовательских системных вызовов. Обратите внимание на ключевое наблюдение: splice() может доставлять ссылки на page cache файлов, доступных только для чтения (включая setuid-бинарники), в криптографические TX scatterlist."
Примерно через час автоматизированного анализа Copy Fail оказался результатом с наивысшей степенью серьёзности. Дополнительные уязвимости, обнаруженные в ходе того же сканирования, остаются в процессе скоординированного раскрытия.
Часть 2: От пода к хосту — как Copy Fail осуществляет побег из всех крупных облачных платформ Kubernetes. Скоро.
| Свойство | Детали |
|---|
| Детерминированность | Прямолинейная логическая ошибка — без гонок, без временных окон, без повторов |
| Переносимость | Тот же скрипт, те же байты, работает на всех протестированных дистрибутивах и архитектурах |
| Компактность | Python-скрипт на 732 байта, использующий только стандартную библиотеку (os, socket, zlib). Требуется Python 3.10+ для os.splice |
| Скрытность | Повреждённая страница никогда не помечается как грязная. Контрольные суммы на диске не изменяются; модифицируется только page cache в памяти |
| Кросс-контейнерность | Page cache является общим для всей системы и пересекает границы контейнеров — это также примитив для побега из узла Kubernetes (см. Часть 2) |
| Переменная | Контроль через |
|---|
| Целевой файл | Любой файл, читаемый текущим пользователем |
| Смещение записи | assoclen, смещение splice и длина splice |
| Значение записи | Байты 4–7 AAD, переданные в sendmsg() (seqno_lo) |
| Год | Событие |
|---|
| 2011 | authencesn добавлен в ядро (a5079d084f8b) для поддержки ESN в IPsec. Scratch-запись существовала, но была безвредна — её вызывал только внутренний слой xfrm, а AAD находился в отдельном scatterlist. |
| 2015 | AF_ALG получает поддержку AEAD. authencesn переведён на новый интерфейс AEAD (104880a6b470), что вводит смещение записи assoclen + cryptlen. Всё ещё вне места: страницы page cache находились в src (только для чтения). Пока не эксплуатируемо. |
| 2017 | В algif_aead.c добавлена оптимизация на месте (72548b093ee3). req->src = req->dst. Страницы page cache тега связаны в записываемый целевой scatterlist. Уязвимость сформирована. |
| 2026-03-23 | Сообщено команде безопасности ядра Linux. |
| 2026-04-01 | Патч включён в основную ветку. |
| 2026-04-22 | Присвоен CVE-2026-31431. |
| 2026-04-29 | Публичное раскрытие. |
| Дата | Событие |
|---|
| 2026-03-23 | Уязвимость сообщена команде безопасности ядра Linux |
| 2026-03-24 | Получено первоначальное подтверждение |
| 2026-03-25 | Предложены и рассмотрены патчи |
| 2026-04-01 | Патчи включены в основную ветку ядра |
| 2026-04-22 | Присвоен CVE-2026-31431 |
| 2026-04-29 | Публичное раскрытие |