Прим. Wunder Fund: наш СТО Эмиль по совместительству является известным white-hat хакером и специалистом по информационной безопасности, и эту статью он предложил как хорошее знакомство с фаззером afl и вообще с фаззингом как таковым.
Этот материал открывает серию из трёх статей (она продолжает материалы о фаззинге FTP-серверов и FreeRDP), посвящённых фаззинг-тестированию реализации протокола HTTP, представленной в Apache HTTP Server. Это — один из самых популярных веб-серверов и в представлении он не нуждается. Так, Apache HTTP — это один из первых HTTP-серверов, разработка которого началась в 1995 году. По состоянию на январь 2021 года под его управлением работали более чем 300000000 серверов, а значит — он использовался на 26% таких систем и занимал второе место по распространённости, немного уступая лишь Nginx (31%).
В статье я вкратце расскажу о том, как работает Apache, и освещу кое-какие идеи, которые помогут всем желающим лучше понять суть кастомных мутаторов, и то, как можно эффективно их применять для исследования реализаций протокола HTTP.
Кастомные мутаторы
В отличие от фаззинг-тестирования, где применяются совершенно случайные входные данные, в мутационном фаззинге во входные данные вносят небольшие изменения. В результате исследуемая система может продолжать считать эти данные корректными, но при этом она способна, получив их, по-новому на них отреагировать. То, что вносит изменения в данные, называют «мутатором».
В фаззере AFL реализованы базовые мутаторы. Они, например, умеют инвертировать биты, инкрементировать и декрементировать байты, выполнять простые арифметические операции над данными, выполнять сплайсинг блоков. Эти мутаторы, в целом, дают хорошие результаты, особенно при работе с двоичными форматами данных. Но их успехи куда скромнее при их применении к текстовым форматам данных, вроде тех, которые используются в HTTP. Именно поэтому я решил создать несколько дополнительных мутаторов, рассчитанных на конкретную задачу — на фаззинг протокола HTTP. Соответствующий код можно найти здесь.
Среди стратегий мутации, которым я уделил внимание в этом исследовании, отмечу следующие:
Перестановка фрагментов запросов: части двух разных запросов меняют местами.
Строки: строки из двух разных запросов меняют местами.
Слова: слова из двух разных запросов меняют местами.
Брутфорс наборов символов: перебор определённых наборов символов.
1 байт:
0x00 – 0xFF
2 байта:
0x0000 – 0xFFFF
3 буквы:
[a-z]{3}
4 цифры:
[0-9]{4}
3 буквы и цифры:
([a-z][0-9]){3}
Строки, состоящие из 3/4 байтов: брутфорс всех 3/4-байтовых строк во входном файле.
Дополнительные функции, которые понадобятся вам для использования этих кастомных мутаторов, вы можете найти здесь.
Анализ покрытия кода тестовыми данными
Прежде чем устраивать полномасштабное длительное фаззинг-тестирование, нам нужно разобраться в том, эффективно ли кастомные мутаторы решают возлагаемые на них задачи.
Для этого я провёл несколько фаззинг-тестов, используя при этом различные комбинации кастомных мутаторов. Моей целью было нахождение такой комбинации, которая даёт наилучшее покрытие кода тестовыми данными за 24 часа.
Отправной точкой стали следующие результаты, полученные с использованием только исходного набора входных данных:
Строки: 30,5%
Функции: 40,7%
А вот — результаты, полученные для различных комбинаций мутаторов за 24 часа (все тесты были выполнены с использованием параметров AFL_DISABLE_TRIM=1
и -s 123
):
Стратегия мутаций | Покрытие кода данными |
AFL++ по умолчанию | 32,8% |
2-байтовый брутфорс (12 часов) + перестановка строк (6 часов) + AFL HAVOC (6 часов) | 33,0% |
Перестановка строк (8 часов) + 1-байтовый брутфорс (4 часа) + 3-байтовый брутфорс строк (4 часа) + AFL HAVOC (8 часов) | 33,2% |
MOpt -L 0 (8 часов) + 1-байтовый брутфорс (8 часов) + RADAMSA (8 часов) | 33,0% |
RADAMSA | 32,6% |
Мутаторы, которые тут не перечислены, показали более скромные результаты, поэтому их мы даже не рассматриваем. Как видите, выигрышной комбинацией стала та, где применяется перестановка строк и AFL HAVOC.
Вот как выглядит покрытие тестовыми данными строк и функций при использовании комбинации перестановки строк и AFL HAVOC.
Попадания | Общее количество | Покрытие | |
Строки | 16326 | 49128 | 33,2% |
Функции | 1424 | 3365 | 42,3% |
После этого я провёл второй тест, увеличив количество включённых модулей Apache. И тут, снова, лучшей оказалась комбинация «Перестановка строк + AFL HAVOC».
Попадания | Общее количество | Покрытие | |
Строки | 18453 | 53041 | 34,8% |
Функции | 1592 | 3579 | 44,5% |
Хотя я и нашёл наилучшую комбинацию механизмов тестирования, это не значит, что пользовался я только ей. В процессе фаззинг-тестирования Apache HTTP я использовал все имеющиеся в моём распоряжении кастомные мутаторы, так как моей целью было достижение максимально возможного уровня покрытия кода тестовыми данными. При таком сценарии эффективность мутатора оказывается не самым важным показателем.
Кастомная грамматика
Ещё один подход к мутационному фаззинг-тестированию заключается в использовании мутаторов, основанных на грамматических правилах, учитывающих особенности исследуемой системы. В дополнение к применению кастомных мутаторов, я воспользовался и кастомной грамматикой. Сделал я это посредством инструмента, недавно добавленного в состав AFL++. Это — Grammar Mutator.
Пользоваться им довольно просто. Сначала делаем это:
make GRAMMAR_FILE=grammars/http.json
./grammar_generator-http 100 100 ./seeds ./trees
А потом — это:
export AFL_CUSTOM_MUTATOR_LIBRARY=./libgrammarmutator-http.so
export AFL_CUSTOM_MUTATOR_ONLY=1
afl-fuzz …
В моём случае была создана упрощённая спецификация грамматики HTTP.
Я включил в её состав самые распространённые HTTP-операции (GET
, HEAD
, PUT
и так далее). Тут я, кроме того, использовал одиночные 1-байтовые строки, а потом, на более поздних стадиях работы, применил Radamsa для увеличения длины этих строк. Radamsa — это фаззер общего назначения, недавно добавленный в AFL++ в виде библиотеки кастомного мутатора. Кроме того, тут я опустил большую часть дополнительных строк. Их я, вместо этого, решил включить в словари.
Конфигурирование Apache
HTTP-сервер Apache настраивают, редактируя текстовые файлы, находящиеся в папке [install_path]/conf
. Главный конфигурационный файл обычно называется httpd.conf
. Каждая из директив, находящихся в нём, занимает отдельную строку. В дополнение к этому, в систему настройки сервера можно добавить дополнительные файлы. Для этого используется директива Include
и шаблоны, с помощью которых можно описать сразу несколько конфигурационных файлов. Символ обратной косой черты, </code>
может находиться в конце строки, указывая на то, что соответствующая директива продолжается на следующей строке. При этом между данным символом и символом конца строки не должно быть больше никаких символов, включая пробел
Модули, модули, и ещё раз модули
Apache имеет модульную архитектуру. Функционал сервера можно расширять или урезать, включая или отключая модули. Существуют стандартные модули, встроенные в Apache HTTP, но есть и множество модулей, созданных сторонними разработчиками, реализующих расширенный функционал.
Для того чтобы включить в конкретной сборке Apache какой-то определённый модуль, нужно воспользоваться ключом вида --enable-[mod]
на конфигурационном этапе сборки:
./configure --enable-[mod]
Здесь mod
— это имя модуля, который нужно включить в сборку.
Я применил инкрементальный подход: начал с включения небольшого набора модулей (--enable-mods-static=few
), а после вывода фаззинг-тестирования в стабильный режим, включил новый модуль и снова проверил стабильность фаззинга. Я, кроме того, подключил модули статически, воспользовавшись ключами --enable-[mod]=static
и --enable-static-support
. Это привело к значительному повышению скорости фаззинга.
После выполнения сборки мы можем задать то, в каком контексте эти модули должны включаться в работу. Для того чтобы это сделать — я модифицировал файл httpd.conf
и связал каждый модуль с отдельной уникальной сущностью Location
(с директорией или файлом). При таком подходе у нас будут различные пути сервера, связанные с разными модулями Apache.
Для того чтобы облегчить жизнь фаззеру, я сделал так, что имена большинства файлов, находящихся в директории htdocs
, имеют длину в 1-2 символа. Это позволяет AFL++ легче находить корректные URL-запросы.
Например:
GET /a HTTP 1.0
POST /b HTTP 1.1
HEAD /c HTTP 1.1
В процессе фаззинг-тестирования я стремлюсь к тому, чтобы включить как можно больше модулей Apache. Цель этого — обнаружение ошибок, связанных с конкурентной работой модулей.
Увеличение размеров словарей
Когда я попытался фаззить Apache, я столкнулся с одним ограничением. Оно заключается в том, что максимальное количество записей словаря, с которым, используя детерминистический подход, может работать AFL, ограничено 200.
Сложность тут в том, что для каждого из новых модулей и для соответствующих ему сущностей Location
, включаемых в файл httpd.conf
, нужны ещё и записи словаря. Например, если я добавлю папку scripts
в Location
mod_crypto
, мне понадобится добавить в словарь строку scripts
. Более того, некоторые модули (например — webdav
), нуждаются во множестве новых HTTP-операций (PROPFIND
, PROPPATCH
и так далее).
По этой причине, и учитывая то, что поддержка более крупных словарей может пригодиться в других сценариях, я сделал PR в проект AFL++, направленный на добавление в него этой функциональности.
Смысл тут заключается в добавлении новой переменной окружения — AFL_MAX_DET_EXTRAS
. Она позволяет задавать максимальное количество записей словаря, которые будут использоваться с применением детерминистического подхода. Вот — один из использованных мной словарей.
Во второй части этой серии материалов мы посмотрим на более эффективный способ поддержки системных вызовов, связанных с файловой системой, и поговорим о концепции «файловых мониторов».
Изменения кода сервера
MPM-фаззинг
Модульная сущность Apache HTTP Server 2.0. проявляется в самых элементарных функциях веб-сервера. Сервер поставляется с набором модулей многопроцессной обработки (Multi-Processing Module, MPM). Они решают задачи привязки к сетевым портам компьютеров, приёма запросов, создания дочерних процессов для обработки запросов. Подробности об Apache MPM можно найти здесь.
В ОС, основанных на Unix, HTTP-сервер Apache, по умолчанию, использует модуль MPM event
. Выбрать конкретный модуль можно и самостоятельно, через конфигурационную опцию вида --with-mpm=[choice]
. Каждый MPM обладает различными возможностями в плане многопоточной и многопроцессной обработки данных. В результате наш подход к фаззингу будет различаться в зависимости от используемого MPM:
MPM event
(многопоточность и многопроцессность).MPM prefork
(единственный управляющий процесс).
Если говорить об изменениях кода, необходимых для проведения фаззинг-тестирования, то, вместо замены сокетов на дескрипторы локальных файлов, я, для доставки в пункт назначения входных данных фаззинга, применил новый подход. А именно, я создал новое соединение локальной сети и отправлял фаззинг-данные через это соединение (спасибо @n30m1nd за вдохновение!).
Традиционные изменения кода
Для того чтобы ознакомиться с обычными изменениями кода, необходимыми для эффективного фаззинга сетевого сервера, взгляните на этот материал. Ниже приведена сводка по самым важным изменениям. Их, в целом, можно разбить на следующие группы:
Изменения, направленные на уменьшение энтропии:
Замена random и rand на неизменные значения. Вот пример.
Замена вызовов
time()
,localtime()
иgettimeoftheday()
на константы.Замена вызовов
getpid()
на фиксированные значения. Вот пример.
Изменения, направленные на снижение задержек:
Удаление некоторых вызовов
sleep()
иselect()
.
Изменения в криптографической подсистеме.
Отключение вычисления контрольных сумм. Вот пример.
Использование статических значений вместо одноразовых случайных чисел. Вот пример.
Подробно изучить последствия внесения этих изменения в код можно, ознакомившись со следующими патчами:
Патч №1
Патч №2
«Фейковый» баг: ситуация, когда наши инструменты вводят нас в заблуждение
То, что сначала показалось мне простым багом в Apache HTTP, оказалось кое-чем куда более серьёзным. Я подробно расскажу о своём путешествии в кроличью нору гейзенбага из-за того, что оно являет собой хороший пример того, сколь обескураживающим иногда может быть поиск первопричины ошибки. И, кроме того, я полагаю, что этот рассказ может оказаться реально полезным для других исследователей информационной безопасности систем, которые могут оказаться в такой же ситуации, в которой оказался я. А именно, в ситуации, когда точно не знаешь — где произошла ошибка — в исследуемой программе или в собственных инструментах.
Всё началось с того, что я обнаружил баг, который можно было воспроизвести лишь тогда, когда работала программа AFL++. Когда я пытался воспроизвести его непосредственно в бинарном файле httpd
, сервер и не думал «падать». Первое, что тогда пришло мне в голову, заключалось в том, что я имею дело с недетерминированной ошибкой. Другими словами — с ошибкой, которая появляется лишь в одном из N случаев. Поэтому я первым делом создал скрипт, который запускал приложение 10000 раз и перенаправлял в файл то, что оно выводило в stdout
. Но баг себя не проявил. Я увеличил количество запусков программы до 100000, но баг так и не появился.
Любопытным тут было то, что баг стабильно показывал себя, когда я запускал приложение под AFL++. Поэтому я решил копнуть в сторону факторов рабочей среды и ASAN, которые могли быть причиной возникновения ошибки. Но потратив часы на исследование этой гипотезы, я так и не смог найти условия, вызывающие ошибку.
У меня начало появляться подозрение в том, что меня обманывают мои же инструменты. Тогда я и решил детальнее исследовать ошибку, вооружившись GDB.
Выяснилось, что ошибка происходила при вызове функции find
в sanitizer_stackdepotbase.h
. Этот файл является частью библиотеки ASAN, он вызывается каждый раз, когда в стек программы помещают новый элемент. Но по какой-то причине связный список s
был повреждён. В результате, из-за того, что выражение s->link
пыталось разыменовать неправильный адрес памяти, возникала ошибка сегментации.
Может — я столкнулся с новой ошибкой в библиотеке ASAN? Мне это казалось нереальным, но чем дольше я смотрел на этот баг, тем яснее у меня вырисовывалось разумное объяснение происходящего. Во всём этом был, конечно, и позитив, который заключался в том, что у меня появилась возможность многое узнать о внутренних механизмах ASAN.
Но у меня были большие сложности с поиском причины повреждения связного списка. Что виновато? Apache, или AFL++? Тогда я прибегнул к помощи отладчика rr. Это — Linux-инструмент для отладки кода, который позволяет записывать и воспроизводить процесс выполнения программы. Такие инструменты ещё называют «отладчиками с обратным выполнением кода». Они позволяют «вернуться в прошлое» и обнаружить первопричину бага.
После этого я, наконец, смог найти источник таинственной ошибки, связанной с повреждением памяти. AFL++ использует участок общей памяти для хранения сведений о покрытии кода тестовыми данными. Код, внедряемый программой в точки ветвления, в целом, эквивалентен следующему:
cur_location = <COMPILE_TIME_RANDOM>;
shared_mem[cur_location ^ prev_location]++;
prev_location = cur_location >> 1;
Размер этого участка памяти, по умолчанию, составляет 64 Кб. Но, как это видно на рисунке, в переменной guard хранится значение 65576
. Поэтому в данном случае фаззер AFL++ переполняет массив __afl_area_ptr
и перезаписывает память программы. В обычных условиях AFL++ предупредит нас о попытке использования участка памяти, размер которого меньше необходимого. Но в данном конкретном случае система этого не делает. Причина мне неизвестна, и это, как говорится, уже совсем другая история.
Решение этой проблемы оказалось очень простым: достаточно было установить переменную окружения MAP_SIZE
в значение 256000
. Надеюсь, рассказ об этом случае поможет кому-то решить похожую проблему, послужит напоминанием о том, что иногда инструменты, которыми мы пользуемся, могут нас обманывать.
Фаззинг Apache в двух словах
Для тех, кого не интересуют пояснения, кто стремится сразу браться за дело (не скажу, что рекомендую так поступать!), вот — сводка того, что нужно знать для самостоятельного фаззинга Apache:
Примените к исходному коду сервера следующие патчи:
patch -p2 < /Patches/Patch1.patch
patch -p2 < /Patches/Patch2.patch
Настройте и соберите сервер:
CC=afl-clang-fast CXX=afl-clang-fast++ CFLAGS="-g -fsanitize=address,undefined -fno-sanitize-recover=all" CXXFLAGS="-g -fsanitize=address,undefined -fno-sanitize-recover=all" LDFLAGS="-fsanitize=address,undefined -fno-sanitize-recover=all -lm" ./configure --prefix='/home/user/httpd-trunk/install' --with-included-apr --enable-static-support --enable-mods-static=few --disable-pie --enable-debugger-mode --with-mpm=prefork --enable-negotiation=static --enable-auth-form=static --enable-session=static --enable-request=static --enable-rewrite=static --enable-auth_digest=static --enable-deflate=static --enable-brotli=static --enable-crypto=static --with-crypto --with-openssl --enable-proxy_html=static --enable-xml2enc=static --enable-cache=static --enable-cache-disk=static --enable-data=static --enable-substitute=static --enable-ratelimit=static --enable-dav=static
make -j8
make install
Запустите фаззер:
AFL_MAP_SIZE=256000 SHOW_HOOKS=1 ASAN_OPTIONS=detect_leaks=0,abort_on_error=1,symbolize=0,debug=true,check_initialization_order=true,detect_stack_use_after_return=true,strict_string_checks=true,detect_invalid_pointer_pairs=2 AFL_DISABLE_TRIM=1 ./afl-fuzz -t 2000 -m none -i '/home/antonio/Downloads/httpd-trunk/AFL/afl_in/' -o '/home/antonio/Downloads/httpd-trunk/AFL/afl_out_40' -- '/home/antonio/Downloads/httpd-trunk/install/bin/httpd' -X @@
Вот ссылки на материалы, которые вам пригодятся:
Патч №1
Патч №2
Пример конфигурации Apache
Примеры входных данных
Пример словаря
Примеры кастомных мутаторов
Примеры кастомной грамматики
Продолжение следует…
Во второй части этой серии материалов мы углубимся в другие интересные аспекты фаззинга. В частности, там речь пойдёт о кастомных перехватчиках и о файловых мониторах. Ещё я расскажу там, как мне удалось устроить фаззинг-тестирование кое-каких особенных модулей Apache — mod_dav
и mod_cache
.
До встречи!