Фаззинг — очень популярная методика тестирования программного обеспечения случайными входными данными. В сети огромное количество материалов о том, как находить дефекты ПО с его помощью. При этом в публичном пространстве почти нет статей и выступлений о том, как искать уязвимости с помощью фаззинга. Возможно, исследователи безопасности не хотят делиться своими секретами. Тем интереснее рассмотреть эту тему в данной статье.

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

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

У исследователя безопасности иные цели:

  1. В отличие от разработчика, он интересуется не всеми ошибками в коде, а ищет именно уязвимости. Это ошибки, которые может спровоцировать атакующий, взаимодействующий с поверхностью атаки системы.

  2. Более того, для исследователя безопасности большую ценность представляют ошибки, которые можно сравнительно быстро и надежно спровоцировать в системе.

  3. Наконец, исследователь безопасности стремится найти именно уникальные уязвимости, которые вряд ли найдут его конкуренты. Очень обидно потратить силы и время на анализ сбоя в программе и затем выяснить, что его обнаружил и исправил кто‑то другой.

В данной статье я расскажу, как перечисленные особенности влияют на настройку и использование фаззера.

Для того чтобы статья была конкретной, я рассмотрю свой любимый ядерный фаззер syzkaller. Это известный открытый проект, он используется для динамического анализа ядра во многих операционных системах. Я уже несколько лет использую его для исследования безопасности ядра Linux.

Архитектура фаззера syzkaller

На схеме представлена архитектура syzkaller — см. рис. 1.

 Рисунок 1. Архитектура фаззера syzkaller
Рисунок 1. Архитектура фаззера syzkaller

Главная часть и основная логика фаззера syzkaller находится в компоненте syz-manager. Он отвечает за управление виртуальными машинами в процессе фаззинга. Также syz-manager работает с набором программ для тестирования ядра, который называется корпус. Он добавляет в корпус новые перспективные программы и удаляет бесполезные. Эти программы по сути являются случайными входными данными для фаззинга ядра. Они написаны на специальном языке syzlang, который задает формат и аргументы системных вызовов Linux.

Если в процессе фаззинга ядро уходит в отказ (возникает kernel crash), то фаззер сохраняет это событие в базу и пытается сгенерировать минимальный репродюсер — самую короткую комбинацию системных вызовов, которая способна вызвать эту ошибку в ядре.

Сам процесс фаззинга ядра происходит внутри виртуальной машины. В ее пользовательском пространстве работают части syzkaller, исполняющие системные вызовы и собирающие метрики покрытия кода ядра по итогу тестирования. Эта информация передается в syz-manager, который использует ее при выборе перспективных программ для фаззинг-корпуса. Это очень эффективная технология, которая называется обратной связью по покрытию (coverage guided fuzzing).

Также при фаззинге Linux очень важны детекторы ошибок в ядре и так называемые санитайзеры. Они нужны для того, чтобы увести ядро в отказ при возникновении нештатной ситуации. Без них возникшая ошибка, например использование памяти после освобождения, не будет обнаружена и фаззинг, по сути, будет бесполезен.

Такова базовая архитектура фаззера syzkaller. А теперь рассмотрим, как адаптировать его для поиска уязвимостей в ядре Linux.

Как искать уязвимости в ядре Linux с помощью фаззинга

Уязвимости в ядре Linux можно разделить на два класса:

  1. Уязвимости, позволяющие выполнить локальное повышение привилегий (Local Privilege Escalation, LPE). При эксплуатации такой уязвимости локальный непривилегированный пользователь становится пользователем root или другим пользователем с повышенными привилегиями в системе.

  2. Уязвимости, приводящие к удаленному выполнению кода в ядре (Remote Code Execution, RCE). При эксплуатации такой уязвимости атакующий, взаимодействующий с Linux‑системой по сети, добивается исполнения произвольного кода в пространстве ядра.

Для того чтобы syzkaller находил исключительно ошибки, потенциально приводящие к LPE, нужна единственная модификация — запуск фаззера внутри виртуальной машины без привилегий администратора. В этом случае системные вызовы будут исполняться под учетной записью непривилегированного пользователя и тестироваться будет только поверхность атаки ядра Linux, что отражено на схеме (см. рис. 2).

 Рисунок 2. Запуск фаззера без привилегий администратора
Рисунок 2. Запуск фаззера без привилегий администратора

Для поиска ошибок, потенциально приводящих к RCE, требуется другой подход: нужен фаззинг сетевых интерфейсов ядра Linux. Это детально описано в отличной статье Андрея Коновалова Looking for Remote Code Execution bugs in the Linux kernel. В ней он показал устройство виртуального сетевого интерфейса TUN/TAP и специального вызова syz_emit_ethernet, которые позволяют фаззеру syzkaller взаимодействовать с сетевым стеком ядра Linux.

 Рисунок 3. Взаимодействие фаззера syzkaller с сетевым стеком ядра Linux
Рисунок 3. Взаимодействие фаззера syzkaller с сетевым стеком ядра Linux

Как находить стабильно проявляющиеся уязвимости

Как было сказано выше, для исследователя безопасности большую ценность представляют ошибки, которые можно сравнительно быстро и надежно спровоцировать в системе.

В syz-manager заложена определенная логика, которая срабатывает при обнаружении отказа ядра. Он начинает тестировать весь большой комплект системных вызовов, который спровоцировал ошибку, и методом дихотомии постепенно находит минимальную программу-репродюсер, которая приводит к искомому эффекту. Этот процесс работает нестабильно из-за различных побочных эффектов и состояний гонки в ядре. Поэтому при поиске репродюсера часто бывают ошибки 1-го и 2-го рода.

Чтобы исследователь безопасности не тратил время и силы на разбор нестабильных репродюсеров, ему стоит спроектировать автоматическую систему сортировки результатов фаззинга (отражено на схеме — см. рис. 4). Я тоже разработал такую автоматизацию под свои критерии поиска. Это легко сделать с помощью утилиты syz-repro, которая позволяет несколько раз повторять процесс выявления минимального репродюсера.

 Рисунок 4. Автоматическая сортировка результатов фаззинга
Рисунок 4. Автоматическая сортировка результатов фаззинга

Как находить уникальные уязвимости

Перейдем к наиболее интересной части статьи и рассмотрим, как находить уникальные уязвимости, которые с невысокой вероятностью найдут другие исследователи.

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

На представленной схеме (см. рис. 5) я отметил красным цветом и пронумеровал компоненты фаззера syzkaller и ядра Linux, которые должны быть модифицированы для того, чтобы получать уникальные находки.

 Рисунок 5. Модифицируемые компоненты фаззера syzkaller и ядра Linux
Рисунок 5. Модифицируемые компоненты фаззера syzkaller и ядра Linux
  1. Самая простая идея — ограничить разрешенные системные вызовы Linux, которые исполняет фаззер. Это можно сделать в конфигурации syzkaller. С помощью этого метода можно сузить поверхность атаки, которая подвергается фаззингу. За счет этого syzkaller может продвинуться глубже в коде ядра и получить большее покрытие в исследуемой подсистеме.

  2. Другой действенный способ найти еще не открытые уязвимости — разработать новые описания ядерного API на языке syzlang. Как было сказано выше, syzlang — это специальный язык, описывающий формат и аргументы системных вызовов ядра. Те из них, которые еще не описаны в syzkaller, не подвергаются фаззинг‑тестированию и поэтому представляют собой интересную цель. Множество уязвимостей было найдено исследователями с помощью этого метода.

  3. Существует множество фаззеров для программ в пользовательском пространстве, и они конкурируют между собой за счет совершенствования механизмов мутации фаззинг‑корпуса и применения символьного исполнения. Эта зона роста актуальна и для syzkaller: изменение движка мутаций влияет на то, какие кодовые пути в ядре затрагивает фаззер. Значит, это может помочь найти уникальные уязвимости. Однако такая модификация фаззера требует глубокого погружения в его устройство.

  4. Более простой способ повлиять на процесс фаззинга — старт со специально подготовленным корпусом. Множество исследований говорят о том, что программы в начальном корпусе (также называемые seeds) оказывают существенное влияние на процесс фаззинга.

  5. Переходим к модифицированию компонентов ядра Linux. Наличие исходного кода у исследователя делает возможным замечательный трюк — доработать ядро Linux так, чтобы оно стало более удобным для фаззинг‑тестирования. Именно так я нашел уязвимость CVE-2019–18 683, для которой затем разработал прототип эксплойта, выполнил ответственное разглашение и разработал исправляющий патч. Эта уязвимость ядра Linux была скрыта за предупреждением (kernel warning), и я нашел ее за счет того, что модифицировал ядро, выключив в нем все предупреждения. Изменение фаззинг‑цели может быть очень эффективным для поиска новых ошибок.

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

  7. Еще один необычный способ найти уникальные ошибки в ядре Linux — изменить rootfs. Образ файловой системы виртуальной машины не имеет непосредственного влияния на ядро Linux, которое подвергается фаззингу, но порой изменения в rootfs могут дать неожиданный эффект и включить дополнительные API ядра. Именно так я обнаружил уязвимость CVE-2017–2636, для которой также удалось разработать прототип эксплойта и выполнить ответственное разглашение. В том случае я добавил в образ файловой системы VM скомпилированные модули ядра, и при фаззинге ядро автоматически загрузило модуль n_hdlc, в котором затем была обнаружена ошибка, которую я проанализировал.

  8. Довольно сложный, но очень результативный подход — доработка санитайзеров и других средств обнаружения ошибок в ядре Linux. Некоторые типы ошибок остаются незамеченными при фаззинге из‑за того, что они не отслеживаются детекторами и, следовательно, не приводят к отказу ядра (kernel crash). В качестве примера можно привести выход за границу поля внутри ядерного объекта sk_buff, который является представлением сетевого пакета в памяти ядра Linux. Разработка детектора для такого класса ошибок позволила бы выявлять при фаззинге уязвимости, потенциально приводящие к RCE.

  9. Еще один подход, распространенный в разработке фаззеров для пользовательского пространства, — направленный фаззинг, при котором ограничивается множество тестируемого кода. То же самое можно сделать при фаззинге ядра Linux. Для этого нужно настроить cover_filter в syzkaller или модифицировать ядерную подсистему kcov, чтобы собирать покрытие только для подсистемы Linux, в которой мы ищем уязвимости.

Заключение

Поделившись этими идеями, в заключение расскажу короткую историю.

В 2021 году я нашел в ядре Linux уязвимость CVE-2021-26708. При исследовании методов ее эксплуатации мне нужен был особый heap-spraying-примитив — ядерный объект, размер и содержимое которого может контролировать атакующий из пользовательского пространства. Ни один из публично известных эксплойт-примитивов не подходил. После долгого изнурительного чтения исходного кода ядра я решил делегировать эту задачу компьютеру и использовать фаззинг для поиска нужного объекта. Так я изобрел heap spraying с помощью ядерного объекта msg_msg, который затем стал очень популярным в исследовательском сообществе.

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

Спасибо за внимание!

Комментарии (0)