
Доказательство концепции эксплуатации для CVE-2026-31431 — локального повышения привилегий в ядре Linux через splice() и специально сформированные параметры сокетов, позволяющего произвольную запись в память ядра для перезаписи setuid-бинарников.
copyFail.py — это proof-of-concept (PoC) эксплойт, демонстрирующий уязвимость локального повышения привилегий в ядре Linux. Злоупотребляя системным вызовом splice() в сочетании со специально сформированными параметрами сокета, эксплойт получает возможность повреждать произвольную память ядра. Он использует эту возможность для перезаписи любого setuid-бинарного файла на диске собственным ELF-исполняемым файлом, после чего запускает изменённый бинарный файл для получения повышенных привилегий.
Тип атаки: Локальное повышение привилегий (LPE) Воздействие: Полный доступ к root-оболочке (при нацеливании на setuid-root бинарные файлы) Сложность: Низкая (один скрипт, без внешних зависимостей)
Использование:
./copyFail_cleaned.py [target_binary] # по умолчанию: /usr/bin/su
В оригинальном PoC был жёстко задан /usr/bin/su, но базовая уязвимость ядра применима к любому файлу — атакующий может перезаписать любой выбранный им setuid-бинарный файл. Очищенная версия принимает необязательный аргумент командной строки для указания цели, делая такое поведение явным.
Ядро Linux предоставляет системный вызов splice(), который копирует данные между двумя файловыми дескрипторами целиком внутри ядра — без предварительного копирования данных в пользовательское пространство. Это называется «zero-copy I/O» и используется для повышения производительности в приложениях с высокой пропускной способностью, таких как веб-серверы и обратные прокси.
Уязвимость возникает из-за того, что ядро не проверяет должным образом внутреннее состояние определённых структур сокетов, когда splice() используется на PACKET-сокетах, настроенных с неподдерживаемыми параметрами сокета на уровне протокола. Аккуратно формируя эти параметры, атакующий может повредить указатели ядра, а затем использовать splice() для перенаправления записи в память ядра в произвольные места.
Простыми словами: у ядра есть сокращённый путь для перемещения данных внутри себя, и этот эксплойт обманывает этот путь, заставляя его записывать данные не туда — а именно, в копию системного бинарного файла в памяти.
Эксплойт следует чёткой последовательности шагов:
Эксплойт создаёт raw PACKET-сокет (AF_PACKET / SOCK_RAW) и привязывает его к намеренно некорректным адресам. Затем он вызывает setsockopt() с SOL_PNIO (уровень протокола 279), что является константой уровня Solaris, которую Linux не распознаёт.
Почему это важно: Когда ядро сталкивается с неподдерживаемым уровнем протокола в setsockopt(), оно переходит к универсальному обработчику, который не проверяет данные должным образом. Это оставляет память ядра в несогласованном, эксплуатируемом состоянии.
После установления соединения через accept(), эксплойт отправляет специально сформированные вспомогательные (control) сообщения с помощью sendmsg(). Эти сообщения имеют намеренно несовпадающие заголовки длины — некоторые заявляют, что они короче, чем есть на самом деле, другие — длиннее.
Почему это важно: Парсер управляющих сообщений ядра использует эти длины для арифметики указателей. Несовпадающие длины заставляют парсер читать или записывать за пределами предполагаемых границ, повреждая соседние структуры ядра — в частности, указатели, которым позже будет следовать splice().
Эксплойт создаёт канал (pipe) и вызывает splice() дважды:
/usr/bin/su в канал.Почему это важно: Вызов splice() следует за повреждёнными указателями ядра, внедрёнными на шаге 2, что заставляет его записывать данные в выбранный атакующим адрес памяти ядра вместо буфера сокета. Это даёт эксплойту примитив произвольной записи в память ядра.
Полезная нагрузка эксплойта — это 160-байтовый ELF-исполняемый файл (см. ниже). Каждый вызов функции эксплойта записывает ровно 4 байта. Таким образом, основной цикл выполняется 40 раз (160 / 4 = 40), каждый раз:
После всех 40 итераций первые 160 байт /usr/bin/su перезаписаны.
Наконец, эксплойт выполняет os.system("su"). Ядро загружает изменённый /usr/bin/su из своего кэша страниц (который теперь содержит шеллкод атакующего вместо настоящего бинарного файла su), и шеллкод выполняется.
Встроенная полезная нагрузка распаковывается в 160-байтовый x86-64 ELF-исполняемый файл, содержащий следующий шеллкод:
; Попытка syscall 105 (execveat) — может быть недоступен на старых ядрах
xor eax, eax
xor edi, edi
mov al, 0x69 ; syscall 105
syscall
; Запасной вариант: syscall 59 (execve) — надёжный путь
lea rdi, [rip+0xf] ; RDI = указатель на "/bin/sh"
xor esi, esi ; RSI = NULL (envp)
push 0x3b ; номер syscall 59
pop eax
cdq ; RDX = NULL (argv)
syscall
; Чистый выход
xor edi, edi
push 0x3c ; syscall 60 (exit)
pop eax
syscall
; Секция данных: "/bin/sh\0\0\0"
Что он делает: Запускает /bin/sh без аргументов, наследуя привилегии процесса, который выполнил su. Если эксплойт запускается от root (или с capabilities, дающими su root-доступ), результирующая оболочка будет root-оболочкой.
Чего он НЕ делает:
Это одноразовое повышение привилегий, соответствующее исследовательскому PoC.
| Константа | Значение | Назначение в эксплойте |
|---|---|---|
AF_PACKET | 17 | Семейство raw packet-сокетов |
SOCK_RAW | 3 | Тип raw-сокета |
SOL_PNIO | 279 | Неподдерживаемый уровень протокола (Solaris) |
MSG_DONTWAIT | 0x400 | Флаг неблокирующего sendmsg |
copyFail.py
├── hex_to_bytes() — декодер hex-строк
├── exploit_splice() — основной эксплойт (настройка сокета + повреждение + splice)
│ ├── Фаза 1: Создание PACKET-сокета, bind, setsockopt (SOL_PNIO)
│ ├── Фаза 2: accept() соединение
│ ├── Фаза 3: sendmsg() со специально сформированными вспомогательными сообщениями
│ ├── Фаза 4: pipe() + splice() для повреждения памяти ядра
│ └── Фаза 5: попытка recv() (закрепляет повреждение)
└── Основной цикл:
├── Открыть /usr/bin/su (только для чтения)
├── Распаковать встроенную полезную нагрузку (zlib → 160-байтовый ELF)
├── Цикл: внедрение 4 байт за итерацию (всего 40 итераций)
└── Запуск изменённого su → root-оболочка
Эксплойт открывает /usr/bin/su с флагом O_RDONLY (только для чтения). Ему не нужен доступ на запись, потому что повреждение происходит через кэш страниц ядра — копию файла в памяти, которую ядро использует перед сбросом изменений на диск. Запись на основе splice() полностью обходит обычные права доступа к файлам, обращаясь напрямую к памяти ядра.
/usr/bin/su (сравните хэш с менеджером пакетов)setsockopt() с неизвестными уровнями протоколаsplice() для нерегулярных файловых дескрипторов