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

Я занимаюсь исследованиями безопасности операционных систем и фаззингом несколько лет. Мне нравится этот инструмент, поскольку он позволяет делегировать компьютеру утомительную задачу написания тестов для ПО. При этом способы использования фаззинга могут сильно различаться в зависимости от целей его применения.
В частности, разработчик использует фаззер для своего кода, чтобы найти все имеющиеся в нем ошибки. Поэтому обычно разработчик включает в своем проекте все доступные детекторы ошибок и разбирает все случаи их срабатывания, которые обнаруживаются при фаззинге.
У исследователя безопасности иные цели:
- В отличие от разработчика, он интересуется не всеми ошибками в коде, а ищет именно уязвимости. Это ошибки, которые может спровоцировать атакующий, взаимодействующий с поверхностью атаки системы.
- Более того, для исследователя безопасности большую ценность представляют ошибки, которые можно сравнительно быстро и надежно спровоцировать в системе.
- Наконец, исследователь безопасности стремится найти именно уникальные уязвимости, которые вряд ли найдут его конкуренты. Очень обидно потратить силы и время на анализ сбоя в программе и затем выяснить, что его обнаружил и исправил кто‑то другой.
В данной статье я расскажу, как перечисленные особенности влияют на настройку и использование фаззера.
Для того чтобы статья была конкретной, я рассмотрю свой любимый ядерный фаззер syzkaller
. Это известный открытый проект, он используется для динамического анализа ядра во многих операционных системах. Я уже несколько лет использую его для исследования безопасности ядра Linux.
Архитектура фаззера syzkaller
На схеме представлена архитектура syzkaller
— см. рис. 1.

Рисунок 1. Архитектура фаззера `syzkaller`
Главная часть и основная логика фаззера syzkaller
находится в компоненте syz-manager
. Он отвечает за управление виртуальными машинами в процессе фаззинга. Также syz-manager
работает с набором программ для тестирования ядра, который называется корпус. Он добавляет в корпус новые перспективные программы и удаляет бесполезные. Эти программы по сути являются случайными входными данными для фаззинга ядра. Они написаны на специальном языке syzlang
, который задает формат и аргументы системных вызовов Linux.
Если в процессе фаззинга ядро уходит в отказ (возникает kernel crash), то фаззер сохраняет это событие в базу и пытается сгенерировать минимальный репродюсер — самую короткую комбинацию системных вызовов, которая способна вызвать эту ошибку в ядре.
Сам процесс фаззинга ядра происходит внутри виртуальной машины. В ее пользовательском пространстве работают части syzkaller
, исполняющие системные вызовы и собирающие метрики покрытия кода ядра по итогу тестирования. Эта информация передается в syz-manager
, который использует ее при выборе перспективных программ для фаззинг-корпуса. Это очень эффективная технология, которая называется обратной связью по покрытию (coverage guided fuzzing).
Также при фаззинге Linux очень важны детекторы ошибок в ядре и так называемые санитайзеры. Они нужны для того, чтобы увести ядро в отказ при возникновении нештатной ситуации. Без них возникшая ошибка, например использование памяти после освобождения, не будет обнаружена и фаззинг, по сути, будет бесполезен.
Такова базовая архитектура фаззера syzkaller
. А теперь рассмотрим, как адаптировать его для поиска уязвимостей в ядре Linux.
Как искать уязвимости в ядре Linux с помощью фаззинга
Уязвимости в ядре Linux можно разделить на два класса:
- Уязвимости, позволяющие выполнить локальное повышение привилегий (Local Privilege Escalation, LPE). При эксплуатации такой уязвимости локальный непривилегированный пользователь становится пользователем
root
или другим пользователем с повышенными привилегиями в системе. - Уязвимости, приводящие к удаленному выполнению кода в ядре (Remote Code Execution, RCE). При эксплуатации такой уязвимости атакующий, взаимодействующий с Linux‑системой по сети, добивается исполнения произвольного кода в пространстве ядра.
Для того чтобы syzkaller
находил исключительно ошибки, потенциально приводящие к LPE, нужна единственная модификация — запуск фаззера внутри виртуальной машины без привилегий администратора. В этом случае системные вызовы будут исполняться под учетной записью непривилегированного пользователя и тестироваться будет только поверхность атаки ядра Linux, что отражено на схеме (см. рис. 2).

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

Рисунок 3. Взаимодействие фаззера `syzkaller` с сетевым стеком ядра Linux
Как находить стабильно проявляющиеся уязвимости
Как было сказано выше, для исследователя безопасности большую ценность представляют ошибки, которые можно сравнительно быстро и надежно спровоцировать в системе.
В syz-manager
заложена определенная логика, которая срабатывает при обнаружении отказа ядра. Он начинает тестировать весь большой комплект системных вызовов, который спровоцировал ошибку, и методом дихотомии постепенно находит минимальную программу-репродюсер, которая приводит к искомому эффекту. Этот процесс работает нестабильно из-за различных побочных эффектов и состояний гонки в ядре. Поэтому при поиске репродюсера часто бывают ошибки 1-го и 2-го рода.
Чтобы исследователь безопасности не тратил время и силы на разбор нестабильных репродюсеров, ему стоит спроектировать автоматическую систему сортировки результатов фаззинга (отражено на схеме — см. рис. 4). Я тоже разработал такую автоматизацию под свои критерии поиска. Это легко сделать с помощью утилиты syz-repro
, которая позволяет несколько раз повторять процесс выявления минимального репродюсера.

Рисунок 4. Автоматическая сортировка результатов фаззинга
Как находить уникальные уязвимости
Перейдем к наиболее интересной части статьи и рассмотрим, как находить уникальные уязвимости, которые с невысокой вероятностью найдут другие исследователи.
Дело в том, что невозможно найти что-то уникальное, пользуясь стандартными инструментами, которые есть у всех. Поэтому вам нужно как-то модифицировать свой процесс фаззинга, чтобы находить уникальные уязвимости.
На представленной схеме (см. рис. 5) я отметил красным цветом и пронумеровал компоненты фаззера syzkaller
и ядра Linux, которые должны быть модифицированы для того, чтобы получать уникальные находки.

Рисунок 5. Модифицируемые компоненты фаззера `syzkaller` и ядра Linux
-
Самая простая идея — ограничить разрешенные системные вызовы Linux, которые исполняет фаззер. Это можно сделать в конфигурации
syzkaller
. С помощью этого метода можно сузить поверхность атаки, которая подвергается фаззингу. За счет этогоsyzkaller
может продвинуться глубже в коде ядра и получить большее покрытие в исследуемой подсистеме. -
Другой действенный способ найти еще не открытые уязвимости — разработать новые описания ядерного API на языке
syzlang
. Как было сказано выше,syzlang
— это специальный язык, описывающий формат и аргументы системных вызовов ядра. Те из них, которые еще не описаны вsyzkaller
, не подвергаются фаззинг‑тестированию и поэтому представляют собой интересную цель. Множество уязвимостей было найдено исследователями с помощью этого метода. -
Существует множество фаззеров для программ в пользовательском пространстве, и они конкурируют между собой за счет совершенствования механизмов мутации фаззинг‑корпуса и применения символьного исполнения. Эта зона роста актуальна и для
syzkaller
: изменение движка мутаций влияет на то, какие кодовые пути в ядре затрагивает фаззер. Значит, это может помочь найти уникальные уязвимости. Однако такая модификация фаззера требует глубокого погружения в его устройство. -
Более простой способ повлиять на процесс фаззинга — старт со специально подготовленным корпусом. Множество исследований говорят о том, что программы в начальном корпусе (также называемые seeds) оказывают существенное влияние на процесс фаззинга.
-
Переходим к модифицированию компонентов ядра Linux. Наличие исходного кода у исследователя делает возможным замечательный трюк — доработать ядро Linux так, чтобы оно стало более удобным для фаззинг‑тестирования. Именно так я нашел уязвимость CVE-2019–18683, для которой затем разработал прототип эксплойта, выполнил ответственное разглашение и разработал исправляющий патч. Эта уязвимость ядра Linux была скрыта за предупреждением (kernel warning), и я нашел ее за счет того, что модифицировал ядро, выключив в нем все предупреждения. Изменение фаззинг‑цели может быть очень эффективным для поиска новых ошибок.
-
Теперь рассмотрим самый очевидный способ отличаться от конкурентов — использовать еще больше вычислительных мощностей. Чем больше серверов выполняют фаззинг, тем больше виртуальных машин на них запущено, тем больше отказов ядра они обнаруживают. При этом важно, чтобы у исследователя хватало сил и времени на их анализ.
-
Еще один необычный способ найти уникальные ошибки в ядре Linux — изменить
rootfs
. Образ файловой системы виртуальной машины не имеет непосредственного влияния на ядро Linux, которое подвергается фаззингу, но порой изменения вrootfs
могут дать неожиданный эффект и включить дополнительные API ядра. Именно так я обнаружил уязвимость CVE-2017–2636, для которой также удалось разработать прототип эксплойта и выполнить ответственное разглашение. В том случае я добавил в образ файловой системы VM скомпилированные модули ядра, и при фаззинге ядро автоматически загрузило модульn_hdlc
, в котором затем была обнаружена ошибка, которую я проанализировал. -
Довольно сложный, но очень результативный подход — доработка санитайзеров и других средств обнаружения ошибок в ядре Linux. Некоторые типы ошибок остаются незамеченными при фаззинге из‑за того, что они не отслеживаются детекторами и, следовательно, не приводят к отказу ядра (kernel crash). В качестве примера можно привести выход за границу поля внутри ядерного объекта
sk_buff
, который является представлением сетевого пакета в памяти ядра Linux. Разработка детектора для такого класса ошибок позволила бы выявлять при фаззинге уязвимости, потенциально приводящие к RCE. -
Еще один подход, распространенный в разработке фаззеров для пользовательского пространства, — направленный фаззинг, при котором ограничивается множество тестируемого кода. То же самое можно сделать при фаззинге ядра Linux. Для этого нужно настроить
cover_filter
вsyzkaller
или модифицировать ядерную подсистемуkcov
, чтобы собирать покрытие только для подсистемы Linux, в которой мы ищем уязвимости.
Заключение
Поделившись этими идеями, в заключение расскажу короткую историю.
В 2021 году я нашел в ядре Linux уязвимость CVE-2021-26708. При исследовании методов ее эксплуатации мне нужен был особый heap-spraying-примитив — ядерный объект, размер и содержимое которого может контролировать атакующий из пользовательского пространства. Ни один из публично известных эксплойт-примитивов не подходил. После долгого изнурительного чтения исходного кода ядра я решил делегировать эту задачу компьютеру и использовать фаззинг для поиска нужного объекта. Так я изобрел heap spraying с помощью ядерного объекта msg_msg
, который затем стал очень популярным в исследовательском сообществе.
Поэтому фаззинг — это замечательный инструмент, который может быть полезен исследователю безопасности не только для поиска уязвимостей. При этом фаззинг требует от исследователя готовности рискнуть своим временем и вычислительными мощностями своих серверов.
Спасибо за внимание!