Состояние гонки в AF_ALG ядра Linux позволяет получить root и покинуть контейнер Docker

linux

Уязвимость CVE-2025-39964 в криптографическом интерфейсе AF_ALG ядра Linux даёт непривилегированному пользователю возможность получить права root, а из контейнера Docker - выбраться на хост. Ошибка живёт в коде с 2011 года, и до исправления в 2025 году её никто не замечал. Нашли её исследователь Muhammad Alifa Ramdhan из STAR Labs и его коллега Bing-Jhong Billy Jheng, доведший цепочку эксплуатации до конца. Работа принесла авторам 113 337 долларов по программе Google kernelCTF, где платят за действующий эксплойт против свежего стабильного ядра.

Уязвимость CVE-2025-39964

AF_ALG - это программный интерфейс, через который обычные программы обращаются к криптографии внутри ядра: хешированию, симметричному шифрованию, генерации случайных чисел. Достаточно открыть сокет, выбрать алгоритм и передать данные на обработку. Особых прав для этого не нужно, и потому интерфейс привлекателен для атакующих: любая ошибка в нём открывает дорогу из пользовательского процесса в самое ядро. По словам исследователя, до его работы ни одна заявка в kernelCTF не использовала AF_ALG как точку входа.

Механика уязвимости связана с тем, как ядро принимает данные. Программа отправляет их порциями через сокет, а ядро складывает полученное не в один сплошной буфер, а в список фрагментов, разбросанных по страницам памяти. Каждый фрагмент описывает свою страницу, смещение внутри неё и длину. Список живёт в общем состоянии сокета, и результат одного вызова влияет на обработку следующего. Ядро запоминает, заполнена ли последняя страница полностью или в неё можно дописать ещё немного.

Пока данных мало, всё идёт по плану. Проблема проявляется, когда два потока одной программы вызывают отправку одновременно. Перед изменением состояния ядро захватывает блокировку сокета, и поначалу кажется, что одновременная запись невозможна. Но если буфер отправки заполнен, поток уходит в ожидание, а блокировку на это время отпускает. Иначе данные было бы некому забрать, и место никогда не освободилось бы. Проснувшись, поток возвращается к работе уже с другим состоянием в памяти.

Дальше в дело вступает тонкая расстановка. Первый поток дописывает данные в последнюю страницу, и ядро помечает, что в неё можно дописать ещё. Второй к этому моменту создаёт новый фрагмент списка и получает от программы заведомо неверный адрес в пользовательской памяти. Копирование срывается, ядро освобождает свежую страницу и уходит по пути обработки ошибки. Новый фрагмент при этом остаётся последним в списке, а признак свободного места в странице никуда не девается. Счётчик заполненных элементов у нового фрагмента равен нулю.

На следующем вызове ядро решает, что дописывает данные в последнюю страницу, и вычисляет адрес последнего элемента списка. Из нуля вычитается единица. Ядро обращается не к первому элементу массива, а к памяти прямо перед ним - туда, где лежат служебные поля соседнего объекта в куче. Именно это обращение и лежит в основе уязвимости.

Само по себе чтение чужой памяти не даёт злоумышленнику ничего. Но поле, которое ядро оттуда берёт, используется дальше как указатель на страницу, и от него зависит, куда попадёт копируемые данные. Исследователи заполняли кучу объектами подходящего размера и подставляли в конец соседнего объекта своё значение. Так удалось повлиять на адрес назначения.

Превратить это в произвольную запись помогло поведение механизма копирования из пользовательской памяти. Когда вычисленный адрес указывает на отображённую страницу, копирование проходит, и вызов завершается успешно. Когда адрес не отображён, срабатывает обработчик исключения, и программа получает ошибку, а ядро продолжает работать. Получается удобная проверка: перебирая варианты, можно нащупать нужный адрес, не роняя систему. Ни один промах не требует перезагрузки.

Найденную запись направили на параметр ядра, который задаёт программу для обработки аварийных дампов памяти. Если значение этого параметра начинается с вертикальной черты, ядро запускает указанную программу и передаёт ей дамп. Заменив параметр на путь к своему исполняемому файлу и намеренно уронив дочерний процесс, исследователи добились запуска своего кода с правами root.

Контейнеры здесь ничем не помогают. Они используют ядро хоста, а AF_ALG доступен и внутри контейнера. Поэтому та же цепочка, которая повышает права обычного пользователя, позволяет выйти за пределы изолированной среды и получить root на самой машине. В проверочной среде Docker это подтвердилось на практике.

В 2025 году ошибку закрыли. Разработчики ядра не стали просто добавлять проверку на пустой счётчик - они запретили одновременную запись в один сокет. Пока одна операция отправки не завершится, вторая получает отказ. Так прежнее негласное допущение о единственном писателе превратилось в правило, которое ядро действительно контролирует.

Администраторам стоит установить обновления ядра от своего дистрибутива. Исправления вошли в версии 6.16.9, 6.12.49 и другие поддерживаемые ветки. Проверить, закрыта ли уязвимость, можно по версии ядра и по списку исправлений, который публикует производитель дистрибутива. В облачных и контейнерных средах обновлять нужно ядро хоста, а не только образ контейнера.

История с AF_ALG показывает, насколько долго могут жить ошибки в коде, доступном любому пользователю. Четырнадцать лет никто не проверял, что произойдёт, если два потока начнут писать в один сокет при нехватке памяти. Интерфейсы, открытые для непривилегированных процессов, остаются лакомой целью, и находка такого масштаба приносит исследователям сотни тысяч долларов, а защитникам - очередное напоминание следить за обновлениями ядра.

Ссылки

Комментарии: 0