[Перевод] Как мы нашли уязвимость в SQLite при помощи LLM
Введение
В нашем предыдущем посте Project Naptime: Evaluating Offensive Security Capabilities of Large Language Models мы рассказали о фреймворке для исследований уязвимостей при помощи языковых моделей и продемонстрировали его потенциал, усовершенствовав показатели современных бенчмарков CyberSecEval2 компании Meta. С тех пор Naptime эволюционировал в Big Sleep — совместный проект Google Project Zero и Google DeepMind.
Сегодня мы с радостью готовы поделиться первой уязвимостью из реального мира. обнаруженной агентом Big Sleep: отрицательным переполнением (underflow) буфера стека с возможностью реализации эксплойтов в SQLite, — широко используемом опенсорсном движке баз данных. Мы обнаружили уязвимость и сообщили о ней разработчикам в начале октября, и они устранили её в тот же день. К счастью, мы обнаружили эту проблему до её появления в официальном релизе, так что она не затронула пользователей SQLite.
Мы считаем, что это первый публичный пример обнаружения ИИ-агентом ранее неизвестной уязвимости безопасности по памяти в широко используемом реальном ПО. В этом же году на мероприятии DARPA AIxCC команда Team Atlanta обнаружила разыменование нулевого указателя в SQLite, что вдохновило нас использовать его в нашем тестировании, чтобы проверить, сможем ли мы найти более серьёзную уязвимость.
Мы считаем, что наша работа обладает огромным защитным потенциалом. Нахождение уязвимостей в ПО ещё до его релиза не позволит нападающим пользоваться ими: уязвимости устраняются ещё до того, как их увидят злоумышленники. Очень сильно помог в поиске уязвимостей фаззинг, но нам нужна методика, позволяющая защищающимся находить баги, которые сложно (или невозможно) обнаруживать фаззингом, и мы надеемся, что ИИ позволит закрыть этот пробел. Мы считаем, что это многообещающий путь к полному изменению ситуации в кибербезопасности и обеспечению асимметричного преимущества для защищающихся.
Сама уязвимость довольно любопытна, к тому же существующая инфраструктура тестирования SQLite (и через OSS-Fuzz, и через собственную инфраструктуру проекта) не обнаружила проблему, так что мы провели дополнительное исследование.
Методология
Основной причиной разработки Naptime и for Big Sleep стало постоянное обнаружение эксплойтов вариантов ранее найденных и пропатченных уязвимостей. Эта тенденция не прекращалась, поэтому было ясно, что фаззинг не позволяет выявлять такие варианты, и что для нападающих ручной анализ вариантов — это очень затратный процесс.
Также мы считали, что задача анализа вариантов лучше подходит современным LLM, чем более обобщённая задача исследования уязвимостей. Предоставив модели исходную точку, например, подробности ранее устранённой уязвимости, мы избавляемся от большой степени неоднозначности в исследовании уязвимостей, и начинаем с конкретной, основанной на фактах теории: «Вот таким был предыдущий баг; вероятно, где-то ещё есть похожий на него».
Наш проект всё ещё находится на исследовательской стадии, и для оценки прогресса мы сейчас используем небольшие программы с известными уязвимостями. Недавно мы решили подвергнуть тестированию наши модели, проведя первый обширный эксперимент с анализом вариантов уязвимостей SQLite. Мы собрали набор недавних коммитов в репозиторий SQLite, вручную удалили тривиальные и относящиеся только к документации изменения. Затем мы настроили промт так, чтобы передавать агенту и сообщение коммита, и diff изменения, и попросили агента просмотреть текущий репозиторий (в HEAD) на предмет связанных проблем, которые могли быть ещё не устранены.
Обнаруженная уязвимость
Эта уязвимость любопытна тем, что специальное контрольное значение -1 используется в поле индексного типа iColumn:
Этот паттерн создаёт потенциальный пограничный случай, который должен обрабатываться всем кодом, использующим это поле, потому что ожидается, что валидный индекс столбца неотрицателен.
Функция seriesBestIndex не обрабатывает этот пограничный случай корректно, что приводит к записи в буфер стека с отрицательным индексом при обработке очереди с ограничением на столбец rowid
. В сборке, переданной нами агенту, были включены отладочные assert
, и это условие проверялось assert
в строке 706:
Однако в релиз-сборке этого assert
нет, и в нашем тестировании (это зависит от компилятора и уровня оптимизаций) последующая операция записи в строке 718 выполнит запись ниже буфера aIdx
, повреждая младшие 32 бита указателя pConstraint
, который будет разыменован в следующей итерации цикла, что приведёт к состоянию, которое можно подвергнуть эксплойту.
Однако, учитывая объяснение уязвимости, живым исследователям (по крайней мере, нам) было сложно понять, как её вызвать — очевидно, хорошей начальной точкой будет ограничение столбца ROWID, но определённо потребуется более тщательное чтение кода. Похоже, агент уже знает о SQLite больше, чем мы, так что может находить короткие пути!
Например, модель сразу же использует для генерации тестовых случаев виртуальную таблицу generate_series
. (Кроме того, мы видели примеры того, как модель сначала исследовала имеющиеся виртуальные таблицы, но она определённо может использовать здесь уже имеющиеся знания).
Основные моменты расследования
В этом успешном прогоне, выполненном на основе Gemini 1.5 Pro, коммитом-источником стал [1976c3f7] — достаточно крупное и неочевидное изменение. Обнаруженный нашим агентом баг очень слабо связан с изменением в самом коммите; такое часто бывает и при ручном анализе вариантов — понимание одного бага в кодовой базе часто приводит исследователя к другим проблемам.
Ниже представлены выдержки с нашими комментариями курсивом; весь текст в блоках ASSISTANT сгенерирован непосредственно агентом.
Агент выполнил разумную оценку изменений в diff, и использовал её для создания начальной точки в исследованиях:
ASSISTANT
CL улучшает обработку общих для ветвей OR выражений WHERE в запросах виртуальных таблиц, добавив функциюtermFromWhereClause
. Эта функция получает конкретное выражение из WhereClause с учётом вложенных операторов. До внесения этого изменения общие выражения, вероятно, обрабатывались некорректно из-за неправильного сопоставления индексов выражений, что потенциально могло привести к неэффективным планам запросов или некорректным результатам. Наиболее вероятная уязвимость может затрагивать некорректную обработку ограничений операторов WHERE в функцияхallocateIndexInfo
иwhereLoopAddVirtualOne
, в которых CL изменил способы использования смещений ограничений в новой вспомогательной функции. Сначала я исследую функциюallocateIndexInfo
.
. . .
После множества этапов (пропущенных здесь) исследования соответствующего кода, агент сформировал более конкретную гипотезу и попытался проверить её, запустив модифицированную версию тестового случая из патча, но это не сработало, потому что включённый в патч тестовый случай использует расширение, недоступное в нашей конфигурации:
TOOL(debugger_run)
Программа выполнила выход без вылета и попаданий в контрольные точки. Код выхода: 1.
Вывод в STDERR был таким:
Runtime error near line 2: no such module: tcl
Parse error near line 3: no such table: x1
ASSISTANT
Программа завершила выполнение из-за недоступности модуля TCL. В тестах используется модуль виртуальных таблиц TCL, который должен быть включен при компиляции SQLite. Передаваемый ввод программы основан на файле bestindexC.test, который использует модуль TCL для тестирования выбора лучших индексов виртуальных таблиц.Чтобы исправить это, необходимо изменить ввод программы так, чтобы в нём использовалась другая виртуальная таблица, не требующая модуля TCL. Однако уязвимость связана с планированием запросов виртуальных таблиц, так что для возникновения проблемы нам всё равно нужна виртуальная таблица.
Я буду использовать инструмент
code_browser_source
для поиска примеров встроенных виртуальных таблиц в SQLite. Затем я адаптирую ввод программы так, чтобы использовать одну из них, и снова установлю контрольную точку вwhereLoopAddVirtualOne
.
. . .
Агент отлично адаптировался к этой неудаче, разобрался с шагами, которые нужно предпринять для исправления тестового случая, а спустя несколько шагов получил всю информацию, необходимую для сборки тестового случая, достигающего функции whereLoopAddVirtualOne:
ASSISTANT
Функцияsqlite3_create_module
регистрирует модули виртуальных таблиц. Файл shell.c содержит множество примеров этого, в том числе модулеgenerate_series
,fsdir
,completion
иexpert
. Любопытной выглядит и таблица json. Я пока сосредоточусь наgenerate_series
в качестве начальной точки. Попробую создать запрос, использующий этот модуль и способный вызвать некорректную обработку ограничений.
. . .
Стоит отметить, что в процессе исходного обнаружения и воспроизведения уязвимости агент довольно расплывчато говорил о «некорректной обработке ограничений», которую он пытался вызвать; и в этом случае полные траектории немного походят на работу живого исследователя — начинаем с одной идеи, продумываем в процессе исследований ещё несколько, и в конечном итоге создаём тестовый случай, воспроизводящий слегка отличающийся баг. В данном случае при вызове whereLoopAddVirtualOne для таблицы generate_series table мы оказываемся в seriesBestIndex, где ограничение и в самом деле обрабатывается некорректно.
Однако особенно интересно отметить, что после вылета программы модель гораздо лучше справилась с объяснением причин: при выполнении анализа первопричины и полного отчёта о вылете агент чётко создаёт тестовый случай под конкретный идентифицированный пограничный случай:
ASSISTANT
Важное нам поле — это iColumn внутри структурыsqlite3_index_constraint
. Это поле представляет номер столбца ограничения. Чтобы вызвать сбой assert, нам нужно наложить ограничение на столбец с индексом больше 3 или меньше 1.Давайте создадим SQL-запрос, накладывающий ограничения на эти столбцы. В таблице
generate_series
есть столбцы с именами value, start, step и stop. Они имеют следующие индексы:— value: 0
— start: 1
— step: 2
— stop: 3
Для вызова бага мы можем добавить ограничение на ROWID. Ограничения на ROWID используют iColumn = -1.
Вот пример запроса:
SELECT * FROM generate_series (1,10,1) WHERE ROWID = 1;
Этот запрос должен вызвать вылет в функции
seriesBestIndex
.
. . .
В конечном итоге мы получили чёткое и точное описание проблемы, которое практически представляет собой готовый отчёт о баге!
TOOL(debugger_run)
Вылет программы: SIGABRT в :0.
Вывод STDERR:
sqlite3: src/shell.c:6816: int seriesBestIndex (sqlite3_vtab *, sqlite3_index_info *): Assertion `iCol>=0 && iCol<=2' failed.
ASSISTANT(report_success)
Программа вылетает с входными данными input SELECT * FROM generate_series (1,10,1) WHERE ROWID = 1; из-за сбоя assert в функции
seriesBestIndex
.assert (iCol>=0 && iCol<=2) вылетает, потому что когда ограничение затрагивает ROWID, поле iColumn в структуре sqlite3_index_info может содержать значения вне допустимого для индексов столбцов диапазона, например, -1.
Это приводит к тому, что результатом вычисления iCol становится -2, вызывая вылет assert.
А что насчёт фаззинга?
Учитывая очевидную простоту способа воспроизведения, возникает вопрос: почему этот баг не был обнаружен ранее традиционным фаззингом? «Простой» ответ таится в конфигурации обвязки фаззинга. Обвязка, используемая OSS-Fuzz, собрана без включенного расширения generate_series, а альтернативная обвязка fuzzingshell.c содержала более старую версию функции seriesBestIndex
, не затронутую багом. Хотя в репозитории SQLite AFL есть конфигурация для фаззинга того же двоичного файла CLI, что и переданная нами агенту Big Sleep, похоже, она используется не так широко.
Чтобы понять, действительно ли баг «лежит на поверхности», мы попытались обнаружить его при помощи фаззинга. Мы выполнили инструкции по фаззингу из документации SQLite и выбрали в качестве целевой платформу CLI. Также перед прогоном AFL мы убедились, что в корпусе фаззинга содержатся ключевые слова generate_series и rowid. Однако даже спустя 150 процессорных часов фаззинга проблему так и не удалось выявить.
Затем мы попытались упростить задачу фаззера, например, добавив необходимые ключевые слова в SQL-словарь AFL. Однако, похоже, что баг можно быстро обнаружить, только если в корпусе содержится пример, очень близкий к вызывающим вылет входящим данным, потому что в случае этой конкретной проблемы покрытие кода не оказывается надёжной основой.
Известно, что AFL — не самый подходящий инструмент для текстовых форматов наподобие SQL, в которых большинство входящих данных оказывается синтаксически недопустимым и будет отклонено парсером. Тем не менее, интересно сравнить этот результат с постом Михала Залевски о фаззинге SQLite, написанным в 2015 году. В то время AFL достаточно эффективно справлялся с обнаружением багов в SQLite; похоже, спустя годы фаззинга инструмент достиг естественной точки насыщения. Хотя наши результаты могут показаться незначительными по сравнению с существенным качественным изменением, произошедшим после выхода AFL, интересно отметить, что он имеет свои сильные стороны и может эффективно обнаруживать уникальное множество уязвимостей.
Заключение
Для нашей команды это стало моментом утверждения в своих силах и успеха: нахождение уязвимости в широко используемом и хорошо покрытом фаззингом опенсорсном проекте — это потрясающий результат! При наличии подходящих инструментов современные LLM способны выполнять исследования уязвимостей.
Однако мы хотим подчеркнуть, что это всё ещё в большой степени экспериментальные результаты. Команда Big Sleep считает, что в настоящее время фаззер под конкретную платформу будет как минимум столь же эффективен (в поиске уязвимостей).
Мы надеемся, что в будущем наши усилия приведут к обеспечению серьёзного преимущества для защищающейся стороны, потенциально позволяя не только находить вызывающие вылеты тестовые случаи, но и проводить высококачественный анализ первопричин; в будущем рассмотрение и устранение проблем могут стать гораздо дешевле и эффективнее. Мы намерены продолжать рассказывать о своих исследованиях в этой сфере, максимально сокращая в ней пробел между публичным и приватным.
Команда Big Sleep продолжит работать в этой области, выполняя миссию Project Zero по повышению сложности уязвимостей 0-day.
Команда Big Sleep
Это уже не только работа одного Project Zero, ниже перечислены все те, кто поучаствовал в данном проекте (имена приведены в алфавитном порядке):
Miltos Allamanis, Martin Arjovsky, Charles Blundell, Lars Buesing, Mark Brand, Sergei Glazunov, Dominik Maier, Petros Maniatis, Guilherme Marinho, Henryk Michalewski, Koushik Sen, Charles Sutton, Vaibhav Tulsyan, Marco Vanotti, Theophane Weber, Dan Zheng