Фаззинг cppcheck с помощью IJON+AFL
Во время обучения на совместном курсе ФСТЭК России и ИСП РАН по фаззинг-тестированию одним из заданий было проведение фаззинга open-source программы с помощью связки фаззера AFL и механизма аннотаций IJON. Поскольку найти что-либо по данной теме в интернете было достаточно сложно, я решил разбавить тишину описанием своего решения.
Пост ориентирован на людей, похожих по уровню подготовки на меня сейчас - я понимаю, что такое фаззинг и зачем это нужно, но пока не могу сходу решать задачи по фаззингу чего-либо просто смотря на докерфайл и два скрипта по 40 команд. Мне бы хотелось прочитать более подробно описаный процесс фаззинга какого-нибудь ПО и поэтому я решил написать его сам.
Входные данные:
Open-source программа на C/C++ входящая в top-200 (посмотреть его можно, например, здесь) - cppcheck.
IJON - механизм создания аннотаций, которые помогают направлять фаззер AFL, который включен в репозиторий IJON.
Задача - провести фаззинг-тестирование cppcheck, определив интересующие состояния программы и создав для этих состояний аннотации с помощью IJON.
В результате работы мы:
Сориентируемся, как работает cppcheck
Подготовим набор входных данных с помощью генератора программ на C
Соберем IJON и фаззер
Соберем cppcheck с инструментацией
Запустим фаззинг cppcheck с IJON в 4 потока
Посмотрим на результаты деятельности
Изучаем cppcheck
Для начала постараемся быстро понять, как работает попавшаяся нам программа - cppcheck. Создадим в домашней папке директорию kuzz (kuznetsov + fuzz, очень оригинально, да) и скопируем туда файлы cppcheck
ubuntu@ubuntu:~$ mkdir kuzz
ubuntu@ubuntu:~$ cd kuzz ubuntu@ubuntu:~/kuzz$ git clone https://github.com/danmar/cppcheck.git --single-branch --depth=1 cppcheck_simple
ubuntu@ubuntu:~/kuzz$ cd cppcheck_simple/
Собираем с помощью make.
ubuntu@ubuntu:~/kuzz$ make
Теперь читаем мануал и узнаем, как же работает программа. Узнаем, что на вход она получает файлы с кодом на C++ и потом их анализирует. Значит легитимными входными данными, которые мы будем подавать на вход фаззеру должны быть файлы с кодом на C++. Остается найти какое-то количество таких файлов.
Собираем входные данные
Для подготовки входных данных воспользуемся программой csmith, которая генерирует программы на C. Установим csmith, предварительно установив cmake и m4 согласно документации:
ubuntu@ubuntu:~/kuzz$ sudo apt install cmake m4
ubuntu@ubuntu:~/kuzz$ git clone https://github.com/csmith-project/csmith.git --single-branch --depth=1
ubuntu@ubuntu:~/kuzz$ cd csmith/
ubuntu@ubuntu:~/kuzz/csmith$ cmake .
ubuntu@ubuntu:~/kuzz/csmith$ make
ubuntu@ubuntu:~/kuzz/csmith$ sudo make install
Теперь создадим папку "in" для входных данных, куда положим 10 сгенерированных csmith файлов:
ubuntu@ubuntu:~/kuzz/csmith$ cd ..
ubuntu@ubuntu:~/kuzz$ mkdir in ubuntu@ubuntu:~/kuzz$ cd in
ubuntu@ubuntu:~/kuzz/in$ for i in {1..10}; do csmith > random"${i}".cpp; done

Попробуем подать один из этих файлов на вход нашей cppcheck
ubuntu@ubuntu:~/kuzz$ cd ~/kuzz/cppcheck_simple/
ubuntu@ubuntu:~/kuzz/cppcheck_simple$ ./cppcheck ../in/random1.cpp

Как видим, cppcheck проверил наш файл и не обнаружил ошибок. Теперь нужно определить, какое состояние программы мы будем считать интересным настолько, чтобы написать к нему аннотацию для IJON.
Собираем IJON
IJON не обновлялся достаточно давно, поэтому могут быть различные проблемы, но в общем процесс в соответствии с документацией выглядит так (у меня, например, не было рекомендованного 6-го clang, поэтому пришлось его поставить).
ubuntu@ubuntu:~/kuzz$ git clone https://github.com/RUB-SysSec/ijon ubuntu@ubuntu:~/kuzz$ cd ijon/
ubuntu@ubuntu:~/kuzz/ijon$ make
ubuntu@ubuntu:~/kuzz/ijon$ cd llvm_mode/
ubuntu@ubuntu:~/kuzz/ijon/llvm_mode$ sudo apt install clang-6.0
ubuntu@ubuntu:~/kuzz/ijon/llvm_mode$ LLVM_CONFIG=llvm-config-6.0 CC=clang-6.0 make

Фаззер готов.
Пишем аннотацию под IJON
Для начала сделаем новую копию репозитория, с еще не собранными исходными кодами:
ubuntu@ubuntu:~/kuzz$ git clone https://github.com/danmar/cppcheck.git --single-branch --depth=1
ubuntu@ubuntu:~/kuzz$ cd cppcheck/lib
ubuntu@ubuntu:~/kuzz/cppcheck/lib$ ls

Как видно, cppcheck состоит из достаточно большого количества файлов. Быстро пробежавшись по ним, определяемся с тем, что представляется интересным. Поскольку cppcheck это по сути статический анализатор кода, его предназначение - находить ошибки. Поскольку мы хотим добиться покрытия большего количества кода cppcheck (одна из целей фаззинга изначально), то нам интересно, чтобы ошибки находились как можно чаще. В этом нам может помочь IJON, если мы установим аннотацию, которая будет направлять фаззер в сторону увеличения количества ошибок в генерируемых файлах. Механизм вывода уведомлений о найденных ошибках описан в файле errorlogger.cpp. Откроем его с помощью vim.
ubuntu@ubuntu:~/kuzz/cppcheck/lib$ vim errorlogger.cpp
Разные ошибки вызывают разные функции, но есть то, что их объединяет - вывод уведомления об ошибках setmsg.

Чем чаще вызывается вывод уведомления об ошибках, тем больше (логично) ошибок было найдено, а это как раз то, что нам и нужно. Аннотацию IJON можно было бы "прицепить" к существующей переменной, но поскольку таких нет, то придется сделать свою переменную, которая будет считать количество обращений к этой функции. По рекомендации со stackoverflow добавляем к этой функции статическую переменную, подсчитывающую количество обращений. А потом добавляем аннотацию IJON, которая будет направлять фаззер в сторону увеличения значения этой переменной.
Здесь следует сказать, что мы будем использовать именно максимизирующую аннотацию IJON_MAX(), о других аннотациях можно прочитать в README IJON, но стоит иметь ввиду, что аннотации IJON_ENABLE() и IJON_DISABLE() из коробки не работают (спасибо Дмитрию Пономареву за это замечание).
static unsigned int call_count = 0;
call_count++;
IJON_MAX(call_count);

Собираем проект с инструментацией
Для того, чтобы собрать проект с инструментацией фаззера нужно проделать ту же команду make, только вместо стандартных компиляторов указать компиляторы, находящиеся в папке IJON.
ubuntu@ubuntu:~/kuzz/cppcheck/lib$ cd ..
ubuntu@ubuntu:~/kuzz/cppcheck$ CC=/home/ubuntu/kuzz/ijon/afl-clang-fast CXX=/home/ubuntu/kuzz/ijon/afl-clang-fast++ cmake .
ubuntu@ubuntu:~/kuzz/cppcheck$ CC=/home/ubuntu/kuzz/ijon/afl-clang-fast CXX=/home/ubuntu/kuzz/ijon/afl-clang-fast++ make

Фаззинг
После того как завершена сборка с инструментацией можно наконец запустить фаззинг.
ubuntu@ubuntu:~/kuzz/cppcheck$ cd ../ijon/
ubuntu@ubuntu:~/kuzz/ijon$ ./afl-fuzz -i /home/ubuntu/kuzz/in -o /home/ubuntu/kuzz/out -M fuzzer01 -- /home/ubuntu/kuzz/cppcheck/bin/cppcheck @@
Появляется стандартная ошибка, в тексте которой сразу указано, что нужно исправить:

ubuntu@ubuntu:~/kuzz/ijon$ sudo su
root@ubuntu:/home/ubuntu/kuzz/ijon# echo core >/proc/sys/kernel/core_pattern
root@ubuntu:/home/ubuntu/kuzz/ijon# exit
ubuntu@ubuntu:~/kuzz/ijon$ ./afl-fuzz -i /home/ubuntu/kuzz/in -o /home/ubuntu/kuzz/out -M fuzzer01 -- /home/ubuntu/kuzz/cppcheck/bin/cppcheck @@

Как мы могли заметить и ранее, cppcheck анализирует программы достаточно долго. Это усугубляется тем, что сгенерированные csmith тексты программ достаточно объемные, поэтому за установленную 1 секунду cppcheck может не успевать обработать входной сэмпл. Попробуем увеличить время обработки до 4 секунд и добавить знак "+", чтобы разрешить пропуск входных сэмплов по таймауту.
ubuntu@ubuntu:~/kuzz/ijon$ ./afl-fuzz -i /home/ubuntu/kuzz/in -o /home/ubuntu/kuzz/out -M fuzzer01 -t 4000+ -- /home/ubuntu/kuzz/cppcheck/bin/cppcheck @@

Как видим, часть тестов пропущена, но для нашей задачи это не принципиально. Теперь фаззинг запущен в одном потоке, откроем новые окна терминала и запустим в них параллельные потоки
**открываем новое окно терминала**
ubuntu@ubuntu:~/kuzz/ijon$ ./afl-fuzz -i /home/ubuntu/kuzz/in -o /home/ubuntu/kuzz/out -S fuzzer02 -t 4000+ -- /home/ubuntu/kuzz/cppcheck/bin/cppcheck @@
**открываем новое окно терминала**
ubuntu@ubuntu:~/kuzz/ijon$ ./afl-fuzz -i /home/ubuntu/kuzz/in -o /home/ubuntu/kuzz/out -S fuzzer03 -t 4000+ -- /home/ubuntu/kuzz/cppcheck/bin/cppcheck @@
**открываем новое окно терминала**
ubuntu@ubuntu:~/kuzz/ijon$ ./afl-fuzz -i /home/ubuntu/kuzz/in -o /home/ubuntu/kuzz/out -S fuzzer04 -t 4000+ -- /home/ubuntu/kuzz/cppcheck/bin/cppcheck @@

Поскольку cppcheck достаточно долго обрабатывает файлы, то количество выполнений не выглядит сильно ужасающим, хотя AFL и считает, что раз у нас меньше 100 запусков - мы slow. Посмотрим, получилось ли добиться каких-то результатов именно благодаря использованию IJON.
ubuntu@ubuntu:~/kuzz/ijon$ cd /home/ubuntu/kuzz/out/fuzzer01/ijon_max/ ubuntu@ubuntu:~/kuzz/out/fuzzer01/ijon_max$ ls

Как видим, папка ijon_max не пуста, следовательно есть результаты работы механизма аннотаций
Заключение
Выполнение задания было ограничено по времени, если бы его было больше, то можно было бы:
Собрать покрытие кода (например с помощью llvm-cov), которое скорее всего было бы достаточно низким за то время, которое был запущен фаззер. Поскольку генерировать требуется программы на языке C с достаточно сложным синтаксисом, то для фаззинга нужны гораздо большие мощности и время.
Определить другие состояния программы и установить аннотации IJON и на них тоже.
В целом использование аннотаций IJON кажется очень актуальным для фаззинга в ситуациях, когда нужно максимизировать какую-либо координату (это очень хорошо видно на примере фаззинга игр, но это скорее удобный способ демонстрации, чем реальное применение). Подробнее о возможных способах применения IJON можно прочитать в оригинальной научной публикации разработчика. Чтобы оценить, были ли аннотации полезны в данном задании требуется гораздо больше времени и мощностей для фаззинга.
Буду рад вашим комментариям и замечаниям к тому, что можно и нужно сделать лучше или по-другому. Отдельно хочется сказать большое спасибо коллективу ИСП РАН за проведение данного курса, команде Crusher и отдельно Виталию и Дмитрию за помощь и рекомендации по подготовке этого поста. Всем удачного фаззинга :)