64
Фаззинг — это наш хлеб с маслом. Мы скармливаем программе тонны мусорных данных и ждём, когда она упадёт. Классика, как AFL или libFuzzer, работает, но часто вслепую, как пьяный сапёр на минном поле. Пришло время прокачать этот процесс и добавить ему мозгов с помощью машинного обучения (ML).
Почему классический фаззинг заходит в тупик?
Стандартный фаззер, даже с учётом покрытия кода, — это по большей части генератор случайных чисел. Он бездумно переворачивает биты, меняет байты и склеивает куски данных. Это эффективно для простых форматов, но когда дело доходит до сложной логики, фаззер начинает буксовать.
• Магические числа и чек-суммы: Если программа в начале файла ожидает конкретную сигнатуру (например, PK
для ZIP-архива) или проверяет контрольную сумму, случайные мутации почти никогда не пройдут эту проверку. Фаззер будет топтаться на месте, не в силах проникнуть глубже.
• Сложные форматы: Попробуйте натравить стандартный фаззер на парсер JavaScript или XML. Он сгенерирует синтаксически некорректный бред, который будет отбраковываться на самых ранних стадиях, не доходя до интересного кода.
• Состояние программы: Для некоторых багов нужно выполнить сложную последовательность действий. Случайные мутации редко создают такие цепочки.
ML решает эти проблемы, превращая фаззер из генератора шума в интеллектуального охотника за багами.
Как машинное обучение делает фаззинг умнее
Идея проста: вместо того чтобы мутировать данные вслепую, мы используем ML-модель, которая учится на ходу. Она анализирует, какие изменения в данных приводят к открытию нового кода, и начинает генерировать более “осмысленные” входные данные.
Генерация структурированных данных
Забудьте о случайном перевороте битов. Мы можем обучить нейронную сеть (например, рекуррентную (RNN) или генеративно-состязательную (GAN)) на наборе валидных файлов. Модель изучит их структуру — заголовки, чанки, синтаксис — и начнёт генерировать новые, синтаксически корректные, но неожиданные для программы входные данные.
Представьте, что мы фаззим парсер JSON. Вместо мусора типа {"key":val%&ue}
модель сгенерирует что-то вроде {"key": {}, {}, 1e1000}
— синтаксически верный, но потенциально опасный ввод.
Предсказание “перспективных” мутаций
Это самое мясо. Здесь часто используют обучение с подкреплением (Reinforcement Learning).
1. Агент: Наш фаззер.
2. Среда: Целевая программа.
3. Действие: Применение определённой мутации к входным данным.
4. Награда: Обнаружение нового пути выполнения кода (увеличение покрытия) или, что ещё лучше, — крэш.
Модель учится предсказывать, какие участки входного файла и какие типы мутаций с наибольшей вероятностью принесут “награду”. Она видит, что изменение байтов с 12-го по 16-й в PNG-файле часто открывает новые ветки кода в libpng
, и начинает целенаправленно работать с этой областью. Это больше не случайность, а выверенная стратегия.
Проект NEUZZ — отличный тому пример. Он использует нейросеть для выявления “узких мест” в бинарнике — тех самых проверок, которые сложно пройти. Затем он целенаправленно генерирует данные для преодоления этих барьеров.
Практический пример: интеграция с AFL++
Современные фаззеры, вроде AFL++, уже имеют встроенные механизмы для интеграции кастомных мутаторов, в том числе на основе ML. Вы можете написать свой мутатор на Python с использованием TensorFlow
или PyTorch
и подключить его к AFL++.
Вот упрощённый концептуальный код такого мутатора:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
import afl import random # Представим, что у нас есть обученная модель # from my_ml_model import SmartMutator # model = SmartMutator() # model.load_weights("path/to/weights") def mutate(data, max_size): """ Кастомная функция мутации, вызываемая AFL++. """ # С вероятностью 50% используем умную мутацию if random.random() > 0.5: # Модель анализирует входные данные и предлагает # "умную" мутацию. Она может вернуть полностью новый # сгенерированный инпут или набор байт для замены. # mutated_data = model.predict_mutation(data) # Для примера, просто добавим "магическую" последовательность mutated_data = bytearray(data) magic_sequence = b"\xDE\xAD\xBE\xEF" if len(mutated_data) > len(magic_sequence): pos = random.randint(0, len(mutated_data) - len(magic_sequence)) mutated_data[pos:pos+len(magic_sequence)] = magic_sequence return bytes(mutated_data) # В остальных случаях — возвращаем оригинальные данные, # чтобы AFL++ применил свои стандартные мутации. return data # Основной цикл интеграции с AFL++ afl.init() while afl.loop(): # AFL++ передаёт нам данные через stdin # Мы их читаем, мутируем и записываем обратно # Здесь упрощённая логика для наглядности # В реальности используется afl.fuzz() pass # Запускаем наш фаззер с кастомным мутатором # afl-fuzz -i in -o out -P python_module -- /path/to/target @@ |
Этот код — лишь верхушка айсберга, но он показывает принцип: мы перехватываем данные у фаззера, применяем к ним свою ML-логику и возвращаем результат. AFL++ делает остальную грязную работу.
Что в итоге?
Интеграция ML в фаззинг — это не панацея, но мощный апгрейд.
• Скорость: Вы находите баги в сложных парсерах и протоколах на порядки быстрее.
• Глубина: Фаззер проникает в такие дебри кода, куда случайные мутации не доберутся и за миллион лет.
• Эффективность: Меньше времени тратится на бессмысленные итерации, больше — на исследование перспективных направлений.
Классический фаззинг никуда не денется, но будущее за умными, гибридными системами. Перестаньте бросать спагетти в стену — возьмите в руки скальпель.