В этой статье мы будем учиться фаззингу и поможет нам в этом программа WinAFL. В сети можно найти документацию по работе с WinAFL, которая мало поможет новичкам. Куда полезнее будет данная статья, в которой покажу, как скачать, установить и использовать WinAFL.
Еще по теме: Отладка программ с помощью WinDbg
Что такое фаззинг
На этот вопрос лучше ответит статья «Что такое Фаззинг».
Фаззинг с помощью WinAFL
Инструмент WinAFL — это форк популярного фаззера AFL, предназначенный для фаззинга программ с закрытым исходным кодом под ОС Windows.
Так же как и AFL, WinAFL собирает информацию о покрытии кода. Делать это он может тремя способами:
- динамическая инструментация с помощью DynamoRIO;
- статическая инструментация с помощью Syzygy;
- трейсинг с помощью IntelPT.
Мы остановимся на классическом первом варианте как самом простом и понятном.
Фаззит WinAFL следующим образом:
- В качестве одного из аргументов вы должны передать смещение так называемой целевой функции внутри бинарника.
- WinAFL инжектится в приложение и ожидает, пока не начнет выполнятся целевая функция.
- WinAFL начинает записывать информацию о покрытии кода.
- При выходе из целевой функции WinAFL приостанавливает работу приложения, подменяет входной файл, перезаписывает RIP/EIP адресом начала функции и продолжает работу.
- Когда число таких итераций достигнет какого-то максимального значения (его вы определяете сами), WinAFL полностью перезапускает приложение.
Данный подход позволяет не тратить лишнего времени на запуск и инициализацию приложения и ускорить фаззинг.
Требования к функции
Из логики работы WinAFL вытекают простые требования к целевой функции для фаззинга. Целевая функция должна:
- Открывать входной файл.
- Парсить файл и завершать свою работу максимально чисто: закрывать файл и все открытые хендлы, не менять глобальные переменные и т.д. В реальности не всегда получается найти идеальную функцию парсинга, но об этом будем говорить позже.
- Выполнение должно доходить до возврата из функции, выбранной для фаззинга.
Компиляция WinAFL
В репозитории WinAFL на GitHub уже лежат скомпилированные бинарники но в моем случае они попросту не работали, поэтому для того, чтобы не пришлось решать проблемы, лучше скомпилировать WinAFL вместе с самой последней версией DynamoRIO.
К счастью, WinAFL компилируются довольно просто на любом компе.
Шаг 1: Скачайте и установите Visual Studio 2019 Community Edition (при установке выберите пункт «Разработка классических приложений на C++».
Шаг 2: Во время установки Visual Studio, скачайте последний релиз DynamoRIO.
Шаг 3: Загрузите исходники WinAFL из репозитория.
Шаг 4: После установки Visual Studio в меню «Пуск» у вас появятся несколько ярлыков для открытия командной строки Visual Studio: x86 Native Tools Command Prompt for VS 2019 и x64 Native Tools Command Prompt for VS 2019. Выбирайте в соответствии с битностью программы, которую вы собираетесь фаззить.
Шаг 5: В командной строке Visual Studio зайдите в папку с исходниками WinAFL.
Для компиляции 32-битной версии выполните эти команды:
1 2 3 4 |
mkdir build32 cd build32 cmake -G"Visual Studio 16 2019" -A Win32 .. -DDynamoRIO_DIR=..\path\to\DynamoRIO\cmake -DINTELPT=0 -DUSE_COLOR=1 cmake --build . --config Release |
Для компиляции 64-битной версии — эти:
1 2 3 4 |
mkdir build64 cd build64 cmake -G"Visual Studio 16 2019" -A x64 .. -DDynamoRIO_DIR=..\path\to\DynamoRIO\cmake -DINTELPT=0 -DUSE_COLOR=1 cmake --build . --config Release |
В моем случае эти команды выглядят следующим образом:
1 2 3 4 5 |
cd C:\winafl_build\winafl-master\ mkdir build32 cd build32 cmake -G"Visual Studio 16 2019" -A Win32 .. -DDynamoRIO_DIR=C:\winafl_build\DynamoRIO-Windows-8.0.18915\cmake -DINTELPT=0 -DUSE_COLOR=1 cmake --build . --config Release |
После компиляции в папке \build<32/64>\bin\Release будут находиться рабочие бинарники WinAFL. Скопируйте их и каталог с DynamoRIO на виртуалку, которую будете использовать для фаззинга.
Поиск подходящей цели для фаззинга
Изначально AFL был создан для фаззинга приложений, которые парсят файлы. Хотя WinAFL можно применять для приложений, которые используют другие способы ввода, путь наименьшего сопротивления — это выбор цели, использующей именно файлы.
Если же вам, как и мне, нравится дополнительный челлендж, вы можете пофаззить сетевые программы. В таком случае вам придется использовать custom_net_fuzzer.dll из состава WinAFL или писать свою собственную обертку.
Но фаззинг сетевых программ выходит за рамки этой статьи.
Таким образом:
- идеальная цель работает с файлами;
- принимает путь к файлу как аргумент командной строки;
- модуль, содержащий функции, который вы хотите пофаззить, должен быть скомпилирован не статически. В противном случае WinAFL будет инструментировать многочисленные библиотечные функции. Это не принесет дополнительного результата, но замедлит фаззинг.
Удивительно, но разработчики не думают о WinAFL, когда пишут свои приложения. Поэтому если ваша цель не соответствует этим критериям, то ее все равно можно при желании адаптировать к WinAFL.
Поиск функции для фаззинга внутри программы
Мы поговорили об идеальной цели, но реальная может быть от идеала далека, поэтому для примера я взял старую софтину, собранную статически, а ее основной исполняемый файл занимает 8 Мб.
У нее хороший функционал, так что, думаю, ее будет интересно пофаззить.
Наша цель принимает на вход файлы, поэтому начнем загрузки бинарника в IDA Pro, — это найдем функцию CreateFileA в импортах и посмотрим перекрестные ссылки на нее.
Мы видим, что она используется в четырех функциях. Вместо того чтобы реверсить их всех в статике, посмотрим в отладчике, какая именно функция вызывается для парсинга файла.
Откроем нашу программа в отладчике (я как правило использую x64dbg) и добавим аргумент к командной строке — тестовый файл. Откуда он взялся? Просто открыл программы, выставил максимальное число опций для документа и сохранил его на диск.
После этого на вкладке Symbols выбираем библиотеку kernelbase.dll и ставим точки останова на экспорты функций CreateFileA и CreateFileW.
Один интересный момент. «Официально» функции CreateFile* предоставляются библиотекой kernel32.dll. Но если хорошенько посмотреть, то это библиотека содержит только jmp на соответствующие функции kernelbase.dll.
Я люблю ставить брейки именно на экспорты в соответствующей библиотеке. Это страховка от случая, когда мы ошиблись и эти функции вызывает не основной исполняемый модуль (.exe), а, например, какие‑то из библиотек наших целей. Также это полезно, если нашему приложению захочется вызвать функцию с помощью GetProcAddress.
После установки брейкпойнтов продолжим выполнение приложения и увидим, как она совершает первый вызов к CreateFileA. Но если обратить внимание на аргументы, то понятно, что наша цель хочет открыть какой‑то из своих служебных файлов, не наш тестовый файл.
Продолжаем выполнение приложения, пока не увидим в списке аргументов путь к нашему тестовому файлу.
Переходим на вкладку Call Stack и видин, что CreateFileA вызывается не из нашей программы, а из функции CFile::Open библиотеки mfc42.
Так как мы только ищем функцию для фаззинга, нам следует помнить, что она должна принимать путь к входному файлу, делать что‑то с файлом и завершать свою работу настолько чисто, насколько это возможно. Поэтому мы будем подниматься по стеку вызовов, пока не найдем нужную функцию.
Скопируем адрес возврата из CFile::Open (125ACBB0), переходим по нему в IDA и посмотрим на функцию. Мы тут же увидим, что данная функция принимает два аргумента, которые потом используются как аргументы к двум вызовам CFile::Open.
Судя по прототипам CFile::Open из документации MSDN, наши переменные a1 и a2 — это пути к файлам. Обратите внимание, что в IDA путь к файлу передается функции CFile::Open в качестве второго аргумента, так как используется thiscall.
1 2 3 4 5 6 7 8 9 |
virtual BOOL Open( LPCTSTR lpszFileName, UINT nOpenFlags, CFileException* pError = NULL); virtual BOOL Open( LPCTSTR lpszFileName, UINT nOpenFlags, CAtlTransactionManager* pTM, CFileException* pError = NULL); |
Данная функция уже выглядит очень интересно, и стоит постараться рассмотреть ее подробнее. Для этого поставим брейки на начало и конец функции, чтобы изучить ее аргументы и понять, что с ними происходит к концу функции.
Сделав это, перезапускаем приложение и видим, что два аргумента — это пути к нашему тестовому файлу и временному файлу.
Настало время посмотреть на содержимое данных файлов. Судя по содержимому нашего тестового файла, он сжат, зашифрован или каким‑то образом закодирован.
Временный же файл просто пуст.
Выполняем функцию до конца и видим, что наш тестовый файл теперь расшифрован. А вот временный файл после выхода из функции по‑прежнему пуст.
Теперь уберем точки останова с этой функции и продолжим отслеживать вызовы CreateFileA. Следующее обращение к CreateFileA дает нам такой стек вызовов.
Функция, которая вызывает CFile::Open, оказывается очень похожей на предыдущую. Таким же образом ставим точки останова в ее начале и конце и смотрим, что произойдет.
Список аргументов этой функции напоминает то, что мы уже видели.
Срабатывает брейк в конце этой функции, и во временном файле мы можем видеть расшифрованное, а скорее даже разархивированное содержимое тестового файла.
Таким образом, данная функция разархивирует файл. Поэкспериментировав с приложением, я выяснил, что оно принимает на вход как сжатые, так и несжатые файлы. Это для нас хорошо — с помощь фаззинга несжатых файлов мы сможем добиться большего покрытия кода и, как следствие, добраться до более интересных фич.
Посмотрим, сможем ли мы найти функцию, которая выполняет какие‑то действия с уже расшифрованным файлом.
Один из подходов к выбору функции для фаззинга — это поиск функции, которая одной из первых начинает взаимодействовать с входным файлом. Двигаясь вверх по стеку вызовов, нахдоим самую первую функцию, которая принимает на вход путь к тестовому файлу.
Функция для фаззинга должна выполняться до конца, соответственно ставим точку останова на конец функции, чтобы быть уверенными, что эти требования выполнятся, и нажимаем F9 в отладчике.
Также следует убедиться, что данная функция после возврата закрывает все открытые файлы. Для этого проверяем список хендлов процесса в Process Explorer — нашего тестового файла там нет.
Видим, что наша функция соответствует требованиям WinAFL. Попробуем начать фаззить!
Аргументы WinAFL
Мои аргументы для WinAFL выглядят примерно так. Давайте разберем по порядку самые важные из них.
1 |
afl-fuzz.exe -i c:\inputs -o c:\winafl_build\out-plain -D C:\winafl_build\DynamoRIO-Windows-8.0.18915\bin32 -t 40000 -x C:\winafl_build\test.dict -f test.test -- -coverage_module target.exe -fuzz_iterations 1000 -target_module target.exe -target_offset 0xA4390 -nargs 3 -call_convention thiscall -- "C:\Program Files (x86)\target.exe" "@@" |
Все аргументы делятся на три группы, которые отделяются друг от друга двумя прочерками.
Первая группа — аргументы WinAFL:
- D — путь к бинарникам DynamoRIO;
- t — максимальный тайм‑аут для одной итерации фаззинга. Если целевая функция не выполнится до конца за это время, WinAFL подсчитает, что программа зависла, и перезапустит ее;
- x — путь к словарю;
- f — с помощью этого параметра можно передать имя и расширение входного файла программы. Полезно, когда программа решает, как будет парсить файл, в зависимости от его расширения.
Вторая группа — аргументы для библиотеки winafl.dll, которая инструментирует целевой процесс:
- coverage_module — модуль для снятия покрытия. Может быть несколько;
- target_module — модуль с функцией для фаззинга. Может быть только один;
- target_offset — виртуальное смещение функции от базового адреса модуля;
- fuzz_iterations — количество итераций фаззинга между перезапусками программы. Чем меньше это значение, тем чаще WinAFL будет перезапускать всю программу целиком, что будет занимать дополнительное время. Однако если долго фаззить программу без перезапуска, могут накопиться нежелательные побочные эффекты;
- call_convention — соглашение о вызове. Поддерживаются sdtcall, cdecl, thiscall;
- nargs — количество аргументов функции. This тоже считается за аргумент.
Третья группа — путь к самой программе. WinAFL изменит @@ на полный путь к входному файлу.
Добавление словаря
Наша цель простая — увеличить количество путей, находимых за секунду. Для этого вы можете распараллелить работу фаззера, поиграть с числом fuzz_iterations или попробовать фаззить умнее. И в этом вам поможет словарь.
WinAFL умеет восстанавливать синтаксис формата данных цели (например, AFL смог самостоятельно создать валидные JPEG-файлы без какой‑либо дополнительной инфы). Обнаруженные синтаксические единицы он использует для генерации новых кейсов для фаззинга.
Это занимает значительное время, и здесь вы можете ему сильно помочь, ведь кто, как не вы, лучше всего знает формат данных вашей программы? Для этого нужно составить словарь в формате <имя переменной>=»значение». Например, вот начало моего словаря:
1 2 3 4 5 6 7 |
x0="ProgVer" x1="WrittenByVersion" x2="FileType" x3="Created" x4="Modified" x5="Name" x6="Core" |
Итак, мы нашли функцию для фаззинга, попутно расшифровав входной файл программы, создали словарь, подобрали аргументы и можем наконец‑то начать фаззить!
И первые же минуты фаззинга приносят первые краши! Но не всегда все происходит так гладко. Ниже я привел несколько особенностей WinAFL, которые могут вам помочь (или помешать) отладить процесс фаззинга.
Особенности WinAFL
А теперь разберем особенности программы.
Побочные эффекты
Вначале я писал, что функция для фаззинга не должна иметь побочных эффектов. Но это в идеале. Часто бывает так, что разработчики забывают добавить в свои программы такие красивые функции, и приходится иметь дело с тем, что есть.
Так как некоторые эффекты накапливаются, возможно, вам удастся успешно пофаззить, уменьшив число fuzz_iterations — с ней WinAFL будет перезапускать вашу программу чаще. Это негативно повлияет на скорость, но зато уменьшит количество побочных эффектов.
Дебаг-режим
Если WinAFL отказывается работать, попробуйте запустить его в дебаг‑режиме. Для этого добавьте параметр -debug к аргументам библиотеки инструментации. После этого в текущем каталоге у вас появится текстовый лог.
При нормальной работе в нем должно быть одинаковое количество строчек In pre_fuzz_handler и In post_fuzz_handler. Также должна присутствовать фраза Everything appears to be running normally.
Не забудьте выключить дебаг‑режим! С ним WinAFL откажется фаззить, даже если все работает, ссылаясь на то, что целевая программа вылетела по тайм‑ауту. Не верьте ему и выключайте отладку.
Эмуляция работы WinAFL
Иногда при фаззинге программу так перемыкает, что она крашится даже на подготовительном этапе работы WinAFL, после чего он разумно отказывается действовать дальше. Чтобы хоть как‑то в этом разобраться, вы можете вручную эмулировать работу фаззера. Для этого ставьте точку останова на начало и конец функции для фаззинга. Когда выполнение достигнет конца функции, правьте аргументы, равняйте стек, меняйте RIP/EIP на начало функции — и так, пока что‑то не сломается.
Стабильность
Stability — очень важный параметр. Он показывает, насколько карта покрытия кода меняется от итерации к итерации. 100% — на каждой итерации программа ведет себя абсолютно одинаково. 0% — каждая итерация полностью отличается от предыдущей. Разумеется, нам нужно значение где‑то посередине.
Автор AFL решил, что ориентироваться надо где‑то на 85%. В нашем примере стабильность держится на уровне 9,5%. Полагаю, это может быть связано в том числе с тем, что программа собрана статически и на стабильность негативно влияют какие‑то из используемых библиотечных функций. Возможно, и мультипоточность тоже повлияла на это.
Набор входных файлов
Чем больше покрытие кода, тем выше шанс найти баг. А максимального покрытия кода можно добиться, создав хороший набор входных файлов. Если вы задались целью пофаззить парсеры файлов каких‑то хорошо известных форматов, то, как говорится, гугл в помощь: некоторым исследователям удавалось собрать внушительный набор файлов именно с помощью парсинга выдачи Google.
Такой набор потом можно минимизировать с помощью скрипта [winafl-cmin.py](http://winafl-cmin.py) из того же репозитория WinAFL. А если вы, как и я, предпочитаете парсилки файлов проприетарных форматов, то поисковик не так часто будет способен помочь. Приходится посидеть и поковыряться в программе, чтобы нагенерировать набор интересных файлов.
Как отучить WinAFL ругаться?
Моя программа довольно многословна и ругалась на неверный формат входного файла, показывая всплывающие сообщения.
Такие проблемы вы легко сможете вылечить, пропатчив используемую программой библиотеку или саму программу.
Заключение
На этом все. Теперь вы сможете сами фаззить программы с помощью WinAFL.
Еще по теме: Удаленная отладка вредоносных программ