
В этом посте я расскажу, как нашёл уязвимость нулевого дня в ядре Linux при помощи модели OpenAI o3. Уязвимость обнаружилась благодаря одному лишь API o3 — не потребовались никакая дополнительная настройка, агентские фреймворки и инструменты.
Недавно я занимался аудитом уязвимостей ksmbd. ksmbd — это «сервер ядра Linux, реализующий в пространстве ядра протокол SMB3 для передачи файлов по сети». Я приступил к этому проекту специально для того, чтобы взять отдых от разработки связанных с LLM инструментов, но после релиза o3 не мог избежать искушения и не использовать в качестве небольшого бенчмарка способностей o3 баги, найденные мной в ksmbd. В одном из следующих постов я расскажу о показателях o3 при обнаружении всех этих багов, а сегодня мы поговорим о том, как в процессе моего бенчмаркинга o3 обнаружила уязвимость нулевого дня. Найденной уязвимости присвоили обозначение CVE-2025-37899 (её патч выложен на Github), это use-after-free в обработчике команды SMB logoff
. Для понимания уязвимости необходимо знать о работе конкурентных подключений к серверу и о том, как они в определённых обстоятельствах могут обмениваться различными объектами. Модели o3 удалось разобраться в этом и найти место, где конкретный объект с автоматическим подсчётом ссылок освобождался, но продолжал оставаться доступным для другого потока. Насколько я понимаю, это будет первым публичным рассказом об уязвимости подобного типа, обнаруженной LLM.
Прежде, чем я перейду к техническим подробностям, изложу основную мысль этого поста: с выходом o3 модели LLM совершили большой скачок вперёд в своих способностях рассуждения о коде, и если вы работаете в сфере исследования уязвимостей, то стоит начать уделять им большое внимание. Если вы исследователь уязвимостей высокого уровня или разработчик эксплойтов, то машины вас не заменят. На самом деле, всё наоборот: современная стадия их развития существенно повысит вашу эффективность. Если у вас есть задача, которую можно выразить в менее чем десяти тысячах строк кода, то имеется достаточно высокий шанс на то, что o3 или решит её, или поможет в её решении.
Бенчмаркинг o3 при помощи CVE-2025-37778
Для начала давайте поговорим о CVE-2025-37778 — уязвимости, которую я нашёл вручную и которую использовал в качестве бенчмарка способностей o3, когда она нашла уязвимость нулевого дня CVE-2025-37899.
CVE-2025-37778 — это уязвимость категории use-after-free. Проблема возникает на пути аутентификации Kerberos при обработке запроса подготовки сессии от удалённого клиента. Чтобы не путаться в номерах CVE, в дальнейшем я буду называть её «уязвимостью аутентификации kerberos».
Её первопричина выглядит следующим образом:
static int krb5_authenticate(struct ksmbd_work *work,
struct smb2_sess_setup_req *req,
struct smb2_sess_setup_rsp *rsp)
{
...
if (sess->state == SMB2_SESSION_VALID)
ksmbd_free_user(sess->user);
retval = ksmbd_krb5_authenticate(sess, in_blob, in_len,
out_blob, &out_len);
if (retval) {
ksmbd_debug(SMB, "krb5 authentication failed\n");
return -EINVAL;
}
...
Если krb5_authenticate
обнаруживает, что состояние сессии имеет значение SMB2_SESSION_VALID
, то она освобождает sess->user
. Похоже, здесь действует следующее допущение: после этого или ksmbd_krb5_authenticate
выполнит повторную инициализацию с новым валидным значением, или после возврата из krb5_authenticate
с возвращаемым значением -EINVAL
это sess->user
больше нигде не будет использоваться. Как оказалось, это допущение ошибочно. Мы можем заставить ksmbd_krb5_authenticate
не инициализировать заново sess->user
и сможем получить доступ к sess->user
, даже если krb5_authenticate
возвращает -EINVAL
.
Эта уязвимость — хороший бенчмарк возможностей LLM:
Она интересна по своей природе — она относится к поверхности удалённых атак на ядро Linux.
-
Она не так тривиальна, поскольку требует:
(а) Разобраться, как получить
sess->state == SMB2_SESSION_VALID
, чтобы запустить освобождение.(б) Понять, что в
ksmbd_krb5_authenticate
есть пути, не инициализирующие повторноsess->user
, и подумать, как вызвать срабатывание этих путей.(в) Осознать, что существуют другие части кодовой базы, которые потенциально способны получать доступ к
sess->user
после её освобождения.
Хоть это и не тривиально, но и не безумно сложно. Я могу объяснить коллеге весь путь выполнения кода примерно за десять минут; при этом не требуется понимать кучу вспомогательной информации о ядре Linux, протоколе SMB и остальной части ksmbd, за исключением кода обработки соединений и подготовки сессий. Я вычислил, какой объём кода минимум необходимо прочитать, если изучать каждую функцию ksmbd, вызываемую по пути от получения пакета в модуле ksmbd до срабатывания уязвимости. Получилось примерно 3,3 тысячи строк.
Итак, теперь у нас есть уязвимость, которую мы хотим использовать для оценки возможностей модели. Какой код показать LLM, чтобы понять, способна ли она найти её? Моя цель заключалась в том, чтобы оценить работу o3 в качестве бэкенда гипотетической системы обнаружения уязвимостей, так что нам нужно чётко определиться, как такая система будет генерировать запросы к LLM. Иначе говоря, бессмысленно хаотично подбирать функции для LLM, если у нас нет четкого алгоритма их автоматического отбора. В идеале мы бы давали LLM весь код из репозитория, она потребляла бы его и выдавала результаты. Однако из-за ограничений окна контекста и регрессий точности, возникающих при увеличении объёма контекста, на практике это пока невозможно.
Вместо этого я придумал один потенциальный способ реализации автоматизированного инструмента, который может генерировать контекст LLM: развёртывание по отдельности каждого обработчика команд SMB. Я передал LLM код обработчика команды подготовки сессии, в том числе и код всех вызываемых им функций, и так далее до глубины вызовов 3 (эта глубина требуется, чтобы включить весь код, необходимый для анализа уязвимости). Также я включил сюда весь код функций, которые считывают данные из сети, парсят входящий запрос, выбирают запускаемый обработчик команд, а затем завершают соединение после завершения операций обработчика. Без этого LLM пришлось бы гадать, как выглядят различные структуры данных, что привело бы к большему количеству ложноположительных срабатываний. В конечном итоге, весь код уместился примерно в 3,3 тысяч (около 27 тысяч токенов), что позволило нам получить бенчмарк для сравнения o3 с более ранними моделями. Если вам интересно, анализируемый код выложен в едином файле, созданном с помощью инструмента files-to-prompt.
Наконец, нужно решить, какой промпт использовать. Системный промпт и другую переданную мной LLM информацию можно найти в файлах .prompt репозитория на Github. Стоит отметить следующее:
Я попросил LLM искать уязвимости use-after-free.
Я предоставил ей краткое высокоуровневое описание того, что делает ksmbd, его архитектуры и модели угроз.
Попытался приказать ей строго не показывать ложноположительных срабатываний и лучше не сообщать ни о каких багах, чем давать ложноположительные ответы. Понятия не имею, сработало ли это, но я старался помочь модели. На самом деле, весь мой системный промпт спекулятивен, я не проводил достаточного количества проверок, чтобы определить, помогает это или мешает. Так что можно считать это молитвой, а не чем-то научным или инженерным. Когда я проведу такие проверки, то напишу пост.
Далее для запуска запроса я воспользовался инструментом llm
(Github):
$ llm --sf system_prompt_uafs.prompt \
-f session_setup_code.prompt \
-f ksmbd_explainer.prompt \
-f session_setup_context_explainer.prompt \
-f audit_request.prompt
Моя экспериментальная обвязка выполняет эти команды N раз (конкретно в этом эксперименте N=100) и сохраняет результаты. Стоит отметить, что если вы попробуете повторить эксперимент, то результаты могут быть не идентичны с моими, поскольку перед написанием поста я удалил файл, содержавший код для анализа, и мне пришлось сгенерировать его заново. Я считаю, что, по сути, они идентичны, но повторно эксперимент не проводил.
o3 находит уязвимость аутентификации Kerberos в бенчмарке в 8 из 100 прогонов. Ещё в 66 прогонах o3 приходит к выводу, что в коде отсутствуют баги (ложноотрицательные срабатывания), а в остальных 28 случаях отчёты были ложноположительными. Для сравнения: Claude Sonnet 3.7 находит баг в 3 из 100 прогонов, а Claude Sonnet 3.5 не находит его ни в одном из 100 прогонов. То есть в этом бенчмарке мы видим улучшение как минимум в 2–3 раза при сравнении o3 с Claude Sonnet 3.7.
Если вам любопытно, я загрузил примеры отчёта o3 (Github) и Sonnet 3.7 (Github). Мне показался любопытным их способ представления результатов. Отчёт o3 напоминает написанный человеком баг-репорт, в котором сжато изложены только находки, а у Sonnet 3.7 получается что-то вроде потока мыслей или рабочего журнала. У обоих подходов есть свои плюсы и минусы. Отчёт o3 обычно проще понять благодаря его структуре и приоритетам. С другой стороны, иногда он слишком краткий, из-за чего снижается понятность.
o3 находит уязвимость нулевого дня (CVE-2025-37899)
Убедившись, что o3 может обнаруживать уязвимость аутентификации Kerberos (CVE-2025-37778) в случае предоставления ей кода обработчика команды подготовки сессии, я захотел узнать, сможет ли она найти этот баг, если передать ей все обработчики команд. Эта задача посложнее, потому что все обработчики команд находятся в файле smb2pdu.c, состоящем примерно из 9 тысяч строк. Однако если o3 всё равно сможет находить уязвимости, если передать ей сразу все обработчики, то из этого следует, что можно создать более простую обёртку для o3, которая передаёт ей файлы целиком, покрывая разнообразную функциональность, а не просто проверяя каждый обработчик по отдельности.
Объединив код всех обработчиков с кодом подготовки и завершения соединения, а также с процедурами отправки обработчиков команд, мы получаем примерно 12 тысяч строк (около 100 тысяч токенов ввода). Как и раньше, я провёл эксперимент 100 раз.
При таком большом количестве входных токенов o3 находит уязвимость аутентификации Kerberos в 1 из 100 прогонов — существенное падение показателей, но ей всё равно удалось её найти. Однако любопытно то, что в выводе других прогонов я обнаружил отчёт о схожей, но новой уязвимости, которая ранее была мне неизвестна. Эта уязвимость тоже связана с освобождением sess->user
, но на этот раз она находится в обработчике выхода из сессии.
int smb2_session_logoff(struct ksmbd_work *work)
{
struct ksmbd_conn *conn = work->conn;
struct ksmbd_session *sess = work->sess;
...
ksmbd_close_session_fds(work);
ksmbd_conn_wait_idle(conn);
...
down_write(&conn->session_lock);
sess->state = SMB2_SESSION_EXPIRED;
up_write(&conn->session_lock);
if (sess->user) {
ksmbd_free_user(sess->user);
sess->user = NULL;
}
...
Пусть o3 сама объяснит, в чём проблема:
Краткое описание
=================
Пока один поток воркера ksmbd продолжает выполнять запросы, использующие
sess‑>user, другой поток, обрабатывающий SMB2 LOGOFF той же сессии,
освобождает эту структуру. Никакая синхронизация не защищает указатель,
поэтому первый поток выполняет разыменование свободной памяти – классическая
use‑after‑free, которая приводит к повреждению памяти ядра и выполнению
произвольного кода в контексте ядра.
Задействованные пути выполнения кода
------------------
1. Второй транспортный канал привязан к существующей сессии
(SMB 3.0 или выше, `conn->binding == true`).
2. Worker‑A (работающий по соединению C2) получает любой обычный запрос
(например, WRITE).
• `smb2_check_user_session()` хранит указатель на уже
существующую `struct ksmbd_session` в `work->sess` и увеличивает
ref‑count сессии, **но не получает никакой
ссылки на `sess->user`.**
• Указатель на `sess` сохраняется для обработки всего запроса;
любая вспомогательная функция, которой требуются данные
аутентификации, напрямую получает доступ к `sess->user`.
3. Пока Worker‑A не завершил работу, Worker‑B (работающий по первому
соединению C1) обрабатывает SMB2 LOGOFF для той же сессии и выполняет
`smb2_session_logoff()` (smb2pdu.c).
Соответствующая часть smb2_session_logoff()
--------------------------------------
```c
...
if (sess->user) {
ksmbd_free_user(sess->user); /* (1) освобождает память */
sess->user = NULL; /* (2) очищает поле */
}
...
```
В конечном итоге `ksmbd_free_user()` оказывается в простом `kfree(user)`.
4. Worker‑B **не ждёт другие соединения, продолжающие использовать
сессию**; он ждёт только выполняющиеся запросы для *собственного*
соединения (`ksmbd_conn_wait_idle(conn)`).
5. Worker‑A продолжает выполнение (например, внутри `smb2_open()`,
`smb2_write()`, …) и разыменует `sess->user`:
```c
/* примеры (несколько) */
if (user_guest(sess->user)) ← разыменование после освобождения
ksmbd_compare_user(sess->user, …)
sess->user->uid
```
Так как для памяти уже выполнено `kfree()` на этапе (1), доступ выполняется
к освобождённой памяти. В зависимости от конкретного тайминга может
возникнуть одна из ситуаций:
• use‑after‑free (указатель продолжает указывать на теперь повторно
используемый slab-объект), что позволяет осуществлять контролируемую
перезапись памяти ядра или
• NULL–deref (если Worker‑A выполняет чтение после этапа (2)),
что тоже приводит к DoS.
Прочитав отчёт, я понял, что теперь по-новому буду смотреть на полезность инструментов ИИ в сфере исследования уязвимостей. Даже если в этой области не произойдёт дальнейшего развития по сравнению с современным o3, исследователям уязвимостей всё равно будет иметь смысл разобраться, какие аспекты их работы от этого выиграют, и подключить соответствующий инструментарий. Разумеется, часть этого инструментария столкнётся с проблемой соотношения «сигнал-шум» (в данном случае примерно 1:50), но определённого прогресса мы уже достигли.
Также любопытно отметить, что когда я обнаружил уязвимость аутентификации Kerberos, то предложил следующее исправление:
diff --git a/fs/smb/server/smb2pdu.c b/fs/smb/server/smb2pdu.c
index d24d95d15d87..57839f9708bb 100644
--- a/fs/smb/server/smb2pdu.c
+++ b/fs/smb/server/smb2pdu.c
@@ -1602,8 +1602,10 @@ static int krb5_authenticate(struct ksmbd_work *work,
if (prev_sess_id && prev_sess_id != sess->id)
destroy_previous_session(conn, sess->user, prev_sess_id);
- if (sess->state == SMB2_SESSION_VALID)
+ if (sess->state == SMB2_SESSION_VALID) {
ksmbd_free_user(sess->user);
+ sess->user = NULL;
+ }
retval = ksmbd_krb5_authenticate(sess, in_blob, in_len,
out_blob, &out_len);
--
2.43.0
Когда же я прочитал баг-репорт o3, то понял, что этого недостаточно. Обработчик выхода уже присваивает sess->user = NULL
, но всё равно уязвим, поскольку протокол SMB позволяет двум разным соединениям привязываться к одной сессии, и на пути выполнения аутентификации Kerberos ничто не мешает другому потоку воспользоваться sess->user
в коротком временном окне после его освобождения и до того, как ему присвоено NULL. Я уже ранее использовал это свойство, чтобы устранить ещё одну уязвимость в ksmbd, но не подумал о нём, изучая уязвимость аутентификации Kerberos.
Осознав это, я ещё раз изучил результаты o3 по исследованию уязвимости аутентификации Kerberos и заметил, что в некоторых своих отчётах она допускала ту же ошибку, что и я, а в других не совершала и осознала, что присвоения sess->user = NULL
недостаточно для устранения проблемы, ведь существует привязка к сессиям. Это замечательно: получается, если бы я использовал модель o3 для устранения исходной уязвимости, то, теоретически, лучше бы справился с работой, чем без неё. Я говорю «теоретически», потому что пока соотношение ложноположительных к истинно положительным срабатываниям, вероятно, слишком высоко, чтобы я изучал каждый отчёт o3 с должным вниманием, необходимым для нахождения решения. Тем не менее, это соотношение будет только улучшаться.
Заключение
В методиках анализа программ LLM достигли уровня возможностей, гораздо более близкого к человеческим, чем когда бы то ни было. По изобретательности, гибкости и обобщённости LLM гораздо более похожи на живого аудитора кода, чем на символьное выполнение, абстрактную интерпретацию или фаззинг. Перспективы применения LLM в исследовании уязвимостей возникли ещё с момента выпуска GPT-4, но их результаты решения реальных задач пока не соответствовали уровню ажиотажа вокруг них. Ситуация изменилась с появлением o3: теперь у нас есть модель, достаточно хорошо умеющая рассуждать о коде, заниматься Q&A, программированием и решением задач. Всё это может осязаемо улучшить качество исследований безопасности.
Модель o3 всё ещё далека от идеала. По-прежнему существует вероятность того, что она будет генерировать бессмысленные результаты и лишь раздражать пользователя. Сегодня поменялось то, что мы впервые получили достаточно высокую вероятность получать корректные результаты, оправдывающую потраченные усилия и время.
Dhwtj
Несмотря на заголовок LLM достоверно ничего не нашёл, а бросался на тени кошек в подвале