
Фаззинг встраиваемых систем с использованием аппаратных точек останова
Этот репозиторий содержит сопутствующий код для статьи: 'Fuzzing Embedded Systems using Debugger Interfaces'. Препринт статьи можно найти здесь https://publications.cispa.saarland/3950/. Код позволяет пользователям воспроизвести и расширить результаты, представленные в статье. Пожалуйста, цитируйте вышеуказанную статью при сообщении, воспроизведении или расширении результатов.
.
├── benchmark # Скрипты для сборки тестового набора фаззеров Google и запуска экспериментов
├── dependencies # Содержит Makefile для установки зависимостей GDBFuzz
├── evaluation # Необработанные данные экспериментов, представленные в статье
├── example_firmware # Примеры встроенных приложений, используемые для оценки
├── example_programs # Содержит скомпилированную примерную программу и конфиги для тестирования GDBFuzz
├── src # Содержит реализацию GDBFuzz
├── Dockerfile # Для создания Docker-образа со всеми установленными зависимостями GDBFuzz
├── LICENSE # Лицензия
├── Makefile # Makefile для создания docker-образа или локальной установки GDBFuzz
└── README.md # Этот файл README
Идея GDBFuzz заключается в использовании аппаратных точек останова от микроконтроллеров в качестве обратной связи для фаззинга, управляемого покрытием. Для этого GDB используется как универсальный интерфейс для обеспечения широкой применимости. Для бинарного анализа прошивки используется Ghidra. Код содержит настройки эталонного теста для оценки метода. Кроме того, включены примеры файлов прошивок.
GDBFuzz обеспечивает фаззинг, управляемый покрытием, для встраиваемых систем, но – в целях оценки – также может фаззить произвольные пользовательские приложения. Для фаззинга на микроконтроллерах мы рекомендуем локальную установку GDBFuzz, чтобы иметь возможность беспрепятственно отправлять фазз-данные на тестируемое устройство.
GDBFuzz протестирован на Ubuntu 20.04 LTS и Raspberry Pi OS (32-битная). Предварительные требования: java и python3. Сначала создайте новое виртуальное окружение и установите все зависимости.
virtualenv .venv
source .venv/bin/activate
make
chmod a+x ./src/GDBFuzz/main.py
GDBFuzz читает настройки из конфигурационного файла со следующими ключами.
[SUT]
# Путь к бинарному файлу SUT.
# Это может быть, например, файл .elf или .bin.
binary_file_path = <path>
# Адрес корневого узла CFG.
# Точки останова устанавливаются на узлы этого CFG.
# Например, 'LLVMFuzzerTestOneInput' или 'main'
entrypoint = <entrypoint>
# Количество входных данных, которые должны быть выполнены без попадания в точку останова, прежде чем
# точки останова будут повёрнуты.
until_rotate_breakpoints = <number>
# Максимальное количество точек останова, которое может быть установлено в любой момент времени.
max_breakpoints = <number>
# Функции, которые следует игнорировать (чёрный список).
# ignore_functions — это разделённый пробелами список имён функций, например 'malloc free'.
ignore_functions = <space separated list>
# Один из вариантов {Hardware, QEMU, SUTRunsOnHost}
# Hardware: Внешний компонент запускает gdb сервер, и GDBFuzz может подключиться к этому gdb серверу.
# QEMU: GDBFuzz запускает QEMU. QEMU эмулирует binary_file_path и запускает gdbserver.
# SUTRunsOnHost: GDBFuzz запускает целевую программу внутри GDB.
target_mode = <mode>
# Установите это значение в False, если вы хотите запустить ghidra, проанализировать SUT,
# и вручную запустить мост ghidra.
start_ghidra = True
# Разделённый пробелами список адресов, где установлены программные точки останова (для кода
# обработки ошибок). Выполнение по этим адресам считается сбоем.
# Пример: software_breakpoint_addresses = 0x123 0x432
software_breakpoint_addresses =
# Считаются ли все сработавшие программные точки останова сбоями
consider_sw_breakpoint_as_error = False
[SUTConnection]
# Класс 'SUT_connection_class' в файле 'SUT_connection_path' реализует
# способ отправки входных данных в SUT.
# Входные данные могут, например, отправляться по Wi-Fi, через последовательный порт, Bluetooth и т.д.
# Этот класс должен наследоваться от ./connections/SUTConnection.py.
# См. ./connections/SUTConnection.py для получения дополнительной информации.
SUT_connection_file = FIFOConnection.py
[GDB]
path_to_gdb = gdb-multiarch
# Запись в формате адрес:порт
gdb_server_address = localhost:4242
[Fuzzer]
# В байтах
maximum_input_length = 100000
# В секундах
single_run_timeout = 20
# В секундах
total_runtime = 3600
# Необязательно
# Путь к каталогу, где каждый файл содержит одно начальное значение (seed). Если вы не хотите
# использовать seeds, оставьте значение пустым.
seeds_directory =
[BreakpointStrategy]
# Стратегии выбора базовых блоков находятся в
# 'src/GDBFuzz/breakpoint_strategies/'
# Для статьи мы используем следующие стратегии
# 'RandomBasicBlockStrategy.py' - Случайный выбор недостигнутых базовых блоков
# 'RandomBasicBlockNoDomStrategy.py' - Как предыдущая, но не использует отношения доминирования для вывода транзитивно достигнутых узлов.
# 'RandomBasicBlockNoCorpusStrategy.py' - Как первая, но предотвращает рост корпуса входных данных и, следовательно, ведёт себя как фаззинг "чёрного ящика" с измерением покрытия.
# 'BlackboxStrategy.py' - Не устанавливает никаких точек останова
breakpoint_strategy_file = RandomBasicBlockStrategy.py
[Dependencies]
path_to_qemu = dependencies/qemu/build/x86_64-linux-user/qemu-x86_64
path_to_ghidra = dependencies/ghidra
[LogsAndVisualizations]
# Один из вариантов {DEBUG, INFO, WARNING, ERROR, CRITICAL}
loglevel = INFO
# Путь к каталогу, где будут храниться выходные файлы (например, графики, файлы журналов).
output_directory = ./output
# Если установлено в True, клиент MQTT отправляет элементы пользовательского интерфейса (например, графики)
enable_UI = False
Пример конфигурационного файла находится в ./example_programs/ вместе с примером программы, которая была скомпилирована с использованием нашей обвязки для фаззинга в benchmark/benchSUTs/GDBFuzz_wrapper/common/.
Запустите фаззинг на один час следующей командой.
chmod a+x ./example_programs/json-2017-02-12
./src/GDBFuzz/main.py --config ./example_programs/fuzz_json.cfg
Сначала мы видим вывод от Ghidra, анализирующего бинарный исполняемый файл, а затем сообщения о перемещении или срабатывании точек останова.
В зависимости от указанного output_directory в конфигурационном файле, теперь должна появиться папка trial-0 со следующей структурой
.
├── corpus # Папка, содержащая корпус входных данных.
├── crashes # Папка, содержащая сбойные входные данные (если есть).
├── cfg # Граф потока управления в виде списка смежности.
├── fuzzer_stats # Статистика кампании фаззинга.
├── plot_data # Таблица, показывающая, на каком относительном времени в кампании фаззинга был достигнут какой базовый блок.
├── reverse_cfg # Обратный граф потока управления.
Установив start_ghidra = False в конфигурационном файле, GDBFuzz подключается к экземпляру Ghidra, работающему в режиме GUI. Для этого плагин ghidra_bridge необходимо запустить вручную из менеджера скриптов. Во время фаззинга достигнутые программные блоки подсвечиваются зелёным цветом.
Для фаззинга пользовательских приложений Linux GDBFuzz использует стандартную точку входа LLVMFuzzOneInput, которая используется почти всеми фаззерами, такими как AFL, AFL++, libFuzzer и т.д.
В benchmark/benchSUTs/GDBFuzz_wrapper/common находится обёртка, которую можно использовать для компиляции любой совместимой обвязки фаззинга в отдельную программу, которая получает входные данные через именованный канал по адресу /tmp/fromGDBFuzz.
Это позволяет имитировать встроенное устройство, потребляющее данные через чётко определённый интерфейс ввода, и, следовательно, запускать GDBFuzz на любом приложении. Для удобства мы создали скрипт в benchmark/benchSUTs, который компилирует все программы из нашей оценки с нашей обёрткой, как объяснено позже.
ПРИМЕЧАНИЕ: GDBFuzz не предназначен для фаззинга пользовательских приложений Linux. Для этого используйте AFL++ или другие фаззеры. Обёртка существует только для целей оценки, чтобы обеспечить возможность запуска эталонных тестов и сравнений в масштабе!
Общая эффективность нашего подхода показана в крупномасштабном эталонном тесте, развёрнутом в виде Docker-контейнеров.
make dockerimage
Чтобы запустить вышеуказанный эксперимент в Docker-контейнере (на один час, как указано в конфигурационном файле), смонтируйте папки example_programs и output как тома и запустите GDBFuzz следующим образом.
chmod a+x ./example_programs/json-2017-02-12
docker run -it --env CONFIG_FILE=/example_programs/fuzz_json_docker_qemu.cfg -v $(pwd)/example_programs:/example_programs -v $(pwd)/output:/output gdbfuzz:1.0
В текущем рабочем каталоге должна появиться выходная папка со структурой, описанной выше.
Наша оценка разделена на две части.
GDBFuzz может работать с любым GDB-сервером и, следовательно, с большинством отладочных проб для микроконтроллеров.
Относительно RQ1 из статьи, мы выполняем GDBFuzz на различных микроконтроллерах с разными прошивками, расположенными в example_firmware.
Для каждого эксперимента мы запускаем GDBFuzz со стратегией RandomBasicBlock и RandomBasicBlockNoCorpus. Последняя ведёт себя как фаззинг без обратной связи, но мы всё ещё можем измерять достигнутое покрытие.
Для ответа на RQ1 мы сравниваем достигнутое покрытие стратегий RandomBasicBlock и RandomBasicBlockNoCorpus.
Соответствующие конфигурационные файлы находятся в соответствующих подпапках, и теперь мы объясним, как настроить фаззинг на четырёх отладочных платах.
GDBFuzz требует доступа к GDB-серверу. В этом случае используются плата B-L4S5I-IOT01A и её встроенный отладчик. Этот встроенный отладчик настраивает GDB-сервер через программу 'st-util' и обеспечивает доступ к этому GDB-серверу через localhost:4242.
sudo apt-get install stlink-tools gdb-multiarch
Соберите и прошейте прошивку для STM32 B-L4S5I-IOT01A, например, проект arduinojson.
Предварительное условие: Установите platformio (pio)
cd ./example_firmware/stm32_disco_arduinojson/
pio run --target upload
Для вашего сведения: platformio сохранил .elf файл SUT здесь: ./example_firmware/stm32_disco_arduinojson/.pio/build/disco_l4s5i_iot01a/firmware.elf. Этот .elf файл также используется позже в пользовательской конфигурации для Ghidra.
Откройте новый терминал и выполните следующее для запуска GDB-сервера:
st-util
Запустите GDBFuzz с пользовательской конфигурацией для arduinojson. Мы можем отправлять данные через USB-порт на микроконтроллер. Микроконтроллер пересылает эти данные через последовательный порт на SUT. В нашем случае /dev/ttyACM0 — это USB-устройство платы микроконтроллера. Если ваша система назначила другое устройство плате микроконтроллера, измените /dev/ttyACM0 в конфигурационном файле на ваше устройство.
./src/GDBFuzz/main.py --config ./example_firmware/stm32_disco_arduinojson/fuzz_serial_json.cfg
Статистика фаззера и журналы находятся в каталоге ./output/...
Установите pyocd:
pip install --upgrade pip 'mbed-ls>=1.7.1' 'pyocd>=0.16'
Убедитесь, что на устройстве установлен 'KitProg v3', и переведите плату в режим 'Arm DAPLink', нажав соответствующую кнопку. Запустите GDB-сервер:
pyocd gdbserver --persist
Прошейте прошивку и запустите фаззинг, например, с помощью:
gdb-multiarch
target remote :3333
load ./example_firmware/CY8CKIT_json/mtb-example-psoc6-uart-transmit-receive.elf
monitor reset
./src/GDBFuzz/main.py --config ./example_firmware/CY8CKIT_json/fuzz_serial_json.cfg
Соберите и прошейте прошивку для ESP32, например, пример arduinojson с помощью platformio.
cd ./example_firmware/esp32_arduinojson/
pio run --target upload
Добавьте следующую строку в файл конфигурации openocd для отладчика J-Link: jlink.cfg
adapter speed 10000
Откройте новый терминал и выполните следующее для запуска GDB-сервера:
get_idf
openocd -f interface/jlink.cfg -f target/esp32.cfg -c "telnet_port 7777" -c "gdb_port 8888"
Запустите GDBFuzz с пользовательской конфигурацией для arduinojson. Мы можем отправлять данные через USB-порт на микроконтроллер. Микроконтроллер пересылает эти данные через последовательный порт на SUT. В нашем случае /dev/ttyUSB0 — это USB-устройство платы микроконтроллера. Если ваша система назначила другое устройство плате микроконтроллера, измените /dev/ttyUSB0 в конфигурационном файле на ваше устройство.
./src/GDBFuzz/main.py --config ./example_firmware/esp32_arduinojson/fuzz_serial.cfg
Статистика фаззера и журналы находятся в каталоге ./output/...
Установите TI MSP430 GCC с https://www.ti.com/tool/MSP430-GCC-OPENSOURCE
Запустите GDB-сервер
./gdb_agent_console libmsp430.so
или (более стабильно). Соберите mspdebug из https://github.com/dlbeer/mspdebug/ и используйте:
until mspdebug --fet-skip-close --force-reset tilib "opt gdb_loop True" gdb ; do sleep 1 ; done
Ghidra не может анализировать бинарные файлы для контроллера TI MSP430 "из коробки". Чтобы это исправить, импортируйте файл в GUI Ghidra, выберите MSP430X в качестве архитектуры и пропустите автоматический анализ. Затем откройте 'Таблицу символов', отсортируйте их по имени и удалите все символы с именами вида $C$L*. Теперь можно выполнить автоматический анализ. После анализа вручную запустите мост ghidra из GUI Ghidra, а затем запустите GDBFuzz.
./src/GDBFuzz/main.py --config ./example_firmware/msp430_arduinojson/fuzz_serial.cfg
Для доступа к USB-устройствам от имени не-root пользователя с помощью pyusb мы добавляем соответствующие правила в udev. Вставьте следующие строки в /etc/udev/rules.d/50-myusb.rules:
SUBSYSTEM=="usb", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678" GROUP="usbusers", MODE="666"
Перезагрузите udev:
sudo udevadm control --reload
sudo udevadm trigger
В RQ2 из статьи мы сравниваем GDBFuzz с подходом, основанным на эмуляции, Fuzzware. Сначала мы выполняем GDBFuzz и Fuzzware, как описано ранее, на прилагаемых файлах прошивок. Для каждого эксперимента GDBFuzz мы создаём файл с корректными базовыми блоками из файлов графа потока управления следующим образом:
cut -d " " -f1 ./cfg > valid_bbs.txt
Теперь мы можем воспроизвести покрытие по сравнению с результатом fuzzware: fuzzware genstats --valid-bb-file valid_bbs.txt
Когда находятся сбойные или зависающие входные данные, они сохраняются в папке crashes. В ходе оценки мы нашли следующие три ошибки:
GDBFuzz также может работать на хосте Raspberry Pi с небольшими изменениями:
В файле ./dependencies/ghidra/support/launch.sh:125 переменная JAVA_HOME должна быть жёстко задана, например, как JAVA_HOME="/usr/lib/jvm/default-java"
Для фаззинга программного обеспечения на других платах GDBFuzz требует:
src/GDBFuzz/connections), который запускает выполнение кода в точке входа, например, последовательное соединение.Все эти свойства должны быть указаны в конфигурационном файле.
Для RQ 4–8 мы запускаем крупномасштабный эталонный тест.
Сначала соберите Docker-образ, как описано ранее, и скомпилируйте приложения из Fuzzer Test Suite Google с нашей обвязкой для фаззинга в benchmark/benchSUTs/GDBFuzz_wrapper/common.
cd ./benchmark/benchSUTs
chmod a+x setup_benchmark_SUTs.py
make dockerbenchmarkimage
Затем адаптируйте настройки эталонного теста в benchmark/scripts/benchmark.py и benchmark/scripts/benchmark_aflpp.py под свои требования (особенно number_of_cores, trials и seconds_per_trial) и запустите эталонный тест с помощью:
cd ./benchmark/scripts
./benchmark.py $(pwd)/../benchSUTs/SUTs/ SUTs.json
./benchmark_aflpp.py $(pwd)/../benchSUTs/SUTs/ SUTs.json
В ./benchmark/scripts появится папка, содержащая файлы графиков (покрытие во времени), файлы статистики фаззера и файлы графа потока управления для каждого эксперимента, как в evaluation/fuzzer_test_suite_qemu_runs.
GDBFuzz имеет опциональную функцию, которая отображает граф потока управления покрытых узлов. Она отключена по умолчанию. Вы можете включить её, следуя инструкциям этого раздела и установив 'enable_UI' в 'True' в пользовательской конфигурации.
На хосте:
Установите
sudo apt-get install graphviz
Установите последнюю версию node, например, Вариант 2 из здесь. Используйте Вариант 2, а не вариант 1. Это должно установить как node, так и npm. Для справки, наши номера версий (более новые версии тоже должны работать):
➜ node --version
v16.9.1
➜ npm --version
7.21.1
Установите зависимости веб-интерфейса:
cd ./src/webui
npm install
Установите MQTT-брокер mosquitto, например, см. здесь
Обновите конфигурацию брокера mosquitto: Замените файл /etc/mosquitto/conf.d/mosquitto.conf следующим содержимым:
listener 1883
allow_anonymous true
listener 9001
protocol websockets
Перезапустите брокер mosquitto:
sudo service mosquitto restart
Проверьте, работает ли брокер mosquitto:
sudo service mosquitto status
Вывод должен содержать текст 'Active: active (running)'
Запустите веб-интерфейс:
cd ./src/webui
npm start
Ваш веб-браузер должен автоматически открыться по адресу 'http://localhost:3000/'.
Запустите GDBFuzz и используйте пользовательский конфигурационный файл, в котором enable_UI установлено в True. Вы можете использовать Docker-контейнер и SUT arduinojson из примера выше. Но убедитесь, что установили 'enable_UI' в 'True'.
Узлы, покрытые 'синим' цветом, покрыты. Белые узлы не покрыты. Мы показываем только непокрытые узлы, если их родитель покрыт (отрисовка полного графа потока управления занимает слишком много времени, если граф большой).
GDBFuzz распространяется с открытым исходным кодом под лицензией AGPL-3.0. Подробности см. в файле LICENSE.
Список других компонентов с открытым исходным кодом, включённых в GDBFuzz, см. в файле 3rd-party-licenses.txt.