• XSS.stack #1 – первый литературный журнал от юзеров форума

Fuzzing WinAFL на практике. Учимся работать фаззером и искать дыры в софте

tabac

CPU register
Пользователь
Регистрация
30.09.2018
Сообщения
1 610
Решения
1
Реакции
3 332
WinAFL — это форк знаменитого фаззера AFL, предназначенный для фаззинга программ с закрытым исходным кодом под Windows. Работа WinAFL описана в документации, но пройти путь от загрузки тулзы до успешного фаззинга и первых крашей не так просто, как может показаться на первый взгляд.

Так же как и AFL, WinAFL собирает информацию о покрытии кода. Делать это он может тремя способами:
  • динамическая инструментация с помощью DynamoRIO;
  • статическая инструментация с помощью Syzygy;
  • трейсинг с помощью IntelPT.
Мы остановимся на классическом первом варианте как самом простом и понятном.

Фаззит WinAFL следующим образом:
  1. В качестве одного из аргументов ты должен передать смещение так называемой целевой функции внутри бинаря.
  2. WinAFL инжектится в программу и ждет, пока не начнет выполнятся целевая функция.
  3. WinAFL начинает записывать информацию о покрытии кода.
  4. Во время выхода из целевой функции WinAFL приостанавливает работу программы, подменяет входной файл, перезаписывает RIP/EIP адресом начала функции и продолжает работу.
  5. Когда число таких итераций достигнет некоторого максимального значения (его ты определяешь сам), WinAFL полностью перезапускает программу.
Такой подход позволяет не тратить лишнее время на запуск и инициализацию программы и значительно увеличить скорость фаззинга.

ТРЕБОВАНИЯ К ФУНКЦИИ​

Из логики работы WinAFL вытекают простые требования к целевой функции для фаззинга. Целевая функция должна:
  1. Открывать входной файл.
  2. Парсить файл и завершать свою работу максимально чисто: закрывать файл и все открытые хендлы, не менять глобальные переменные и так далее. В реальности не всегда получается найти идеальную функцию парсинга, но об этом поговорим позже.
  3. Выполнение должно доходить до возврата из функции, выбранной для фаззинга.

КОМПИЛЯЦИЯ 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-битной версии выполни следующие команды:
    Код:
    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-битной версии — такие:
    Код:
    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
    В моем случае эти команды выглядят так:
    Код:
    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
  6. После компиляции в папке <WinAFL dir>\build<32/64>\bin\Release будут лежать рабочие бинари WinAFL. Скопируй их и папку с DynamoRIO на виртуалку, которую будешь использовать для фаззинга.

ПОИСК ПОДХОДЯЩЕЙ ЦЕЛИ ДЛЯ ФАЗЗИНГА​

AFL создавался для фаззинга программ, которые парсят файлы. Хотя WinAFL можно применять для программ, использующих другие способы ввода, путь наименьшего сопротивления — это выбор цели, использующей именно файлы.

Если же тебе, как и мне, нравится дополнительный челлендж, ты можешь пофаззить сетевые программы. В этом случае тебе придется использовать custom_net_fuzzer.dll из состава WinAFL либо писать свою собственную обертку.

К сожалению, custom_net_fuzzer будет работать не так быстро, потому что он отправляет сетевые запросы своей цели, а на их обработку будет тратиться дополнительное время.
Однако фаззинг сетевых приложений выходит за рамки этой статьи. Оставь комментарий, если хочешь отдельную статью на эту тему.

Таким образом:
  • идеальная цель работает с файлами;
  • принимает путь к файлу как аргумент командной строки;
  • модуль, содержащий функции, который ты хочешь пофаззить, должен быть скомпилирован не статически. В противном случае WinAFL будет инструментировать многочисленные библиотечные функции. Это не принесет дополнительного результата, но сильно замедлит фаззинг.
Удивительно, но большинство разработчиков не думают о WinAFL, когда пишут свои программы. Поэтому если твоя цель не соответствует этим критериям, то ее все равно можно при желании адаптировать к WinAFL.

ПОИСК ФУНКЦИИ ДЛЯ ФАЗЗИНГА ВНУТРИ ПРОГРАММЫ​

Мы поговорили об идеальной цели, но реальная может быть от идеала далека, поэтому для примера я взял программу из старых запасов, которая собрана статически, а ее основной исполняемый файл занимает 8 Мбайт.

У нее много всяких возможностей, так что, думаю, ее будет интересно пофаззить.

Моя цель принимает на вход файлы, поэтому первое, что сделаем после загрузки бинаря в IDA Pro, — это найдем функцию CreateFileA в импортах и посмотрим перекрестные ссылки на нее.

1726812714146.png


Мы сразу же можем увидеть, что она используется в четырех функциях. Вместо того чтобы реверсить каждую из них в статике, посмотрим в отладчике, какая именно функция вызывается для парсинга файла.

Откроем нашу программа в отладчике (я обычно использую x64dbg) и добавим аргумент к командной строке — тестовый файл. Откуда я его взял? Просто открыл программу, выставил максимальное число опций для документа и сохранил его на диск.

1726812749997.png


Дальше на вкладке Symbols выберем библиотеку kernelbase.dll и поставим точки останова на экспорты функций CreateFileA и CreateFileW.

1726812790898.png


Один любопытный момент. «Официально» функции CreateFile* предоставляются библиотекой kernel32.dll. Но если посмотреть внимательнее, то это библиотека содержит только jmp на соответствующие функции kernelbase.dll.

1726812819765.png


Я предпочитаю ставить брейки именно на экспорты в соответствующей библиотеке. Это застрахует нас от случая, когда мы ошиблись и эти функции вызывает не основной исполняемый модуль (.exe), а, например, какие‑то из библиотек нашей целей. Также это полезно, если наша программа захочет вызвать функцию с помощью GetProcAddress.

После установки брейкпойнтов продолжим выполнение программы и увидим, как она совершает первый вызов к CreateFileA. Но если мы обратим внимание на аргументы, то поймем, что наша цель хочет открыть какой‑то из своих служебных файлов, не наш тестовый файл.

1726812856116.png


Продолжим выполнение программы, пока не увидим в списке аргументов путь к нашему тестовому файлу.

1726812891317.png


Перейдем на вкладку Call Stack и увидим, что CreateFileA вызывается не из нашей программы, а из функции CFile::Open библиотеки mfc42.

1726812958151.png


Так как мы только ищем функцию для фаззинга, нам нужно помнить, что она должна принимать путь к входному файлу, делать что‑то с файлом и завершать свою работу настолько чисто, насколько это возможно. Поэтому мы будем подниматься по стеку вызовов, пока не найдем подходящую функцию.

Скопируем адрес возврата из CFile::Open (125ACBB0), перейдем по нему в IDA и посмотрим на функцию. Мы сразу же увидим, что эта функция принимает два аргумента, которые далее используются как аргументы к двум вызовам CFile::Open.

1726812983768.png


Судя по прототипам CFile::Open из документации MSDN, наши переменные a1 и a2 — это пути к файлам. Обрати внимание, что в IDA путь к файлу передается функции CFile::Open в качестве второго аргумента, так как используется thiscall.
Код:
virtual BOOL Open(
    LPCTSTR lpszFileName,
    UINT nOpenFlags,
    CFileException* pError = NULL);
virtual BOOL Open(
    LPCTSTR lpszFileName,
    UINT nOpenFlags,
    CAtlTransactionManager* pTM,
    CFileException* pError = NULL);

Эта функция уже выглядит очень интересно, и стоит постараться рассмотреть ее подробнее. Для этого я поставлю брейки на начало и конец функции, чтобы изучить ее аргументы и понять, что с ними происходит к концу функции.

Сделав это, перезапустим программу и увидим, что два аргумента — это пути к нашему тестовому файлу и временному файлу.

1726813020153.png


Самое время посмотреть на содержимое этих файлов. Судя по содержимому нашего тестового файла, он сжат, зашифрован или каким‑то образом закодирован.

1726813051853.png


Временный же файл просто пуст.

1726813073954.png


Выполним функцию до конца и увидим, что наш тестовый файл все еще зашифрован, а временный файл по‑прежнему пуст. Что ж, убираем точки останова с этой функции и продолжаем отслеживать вызовы CreateFileA. Следующее обращение к CreateFileA дает нам такой стек вызовов.

1726813102588.png


Функция, которая вызывает CFile::Open, оказывается очень похожей на предыдущую. Точно так же поставим точки останова в ее начале и конце и посмотрим, что будет.

1726813136672.png


Список аргументов этой функции напоминает то, что мы уже видели.

1726813168455.png


Срабатывает брейк в конце этой функции, и во временном файле мы видим расшифрованное, а скорее даже разархивированное содержимое тестового файла.

1726813192273.png


Таким образом, эта функция разархивирует файл. Поэкспериментировав с программой, я выяснил, что она принимает на вход как сжатые, так и несжатые файлы. Нам это на руку — с помощь фаззинга несжатых файлов мы сможем добиться гораздо более полного покрытия кода и, как следствие, добраться до более интересных фич.

Посмотрим, сможем ли мы найти функцию, которая выполняет какие‑то действия с уже расшифрованным файлом.

Один из подходов к выбору функции для фаззинга — это поиск функции, которая одной из первых начинает взаимодействовать с входным файлом. Двигаясь вверх по стеку вызовов, найдем самую первую функцию, которая принимает на вход путь к тестовому файлу.

1726813235874.png


Функция для фаззинга должна выполняться до конца, поэтому ставим точку останова на конец функции, чтобы быть уверенными, что это требование выполнится, и жмем F9 в отладчике.

1726813258607.png


Также убедимся, что эта функция после возврата закрывает все открытые файлы. Для этого проверим список хендлов процесса в Process Explorer — нашего тестового файла там нет.

1726813286241.png


Видим, что наша функция соответствует требованиям WinAFL. Попробуем начать фаззить!

АРГУМЕНТЫ WINAFL, ПОДВОДНЫЕ КАМНИ​

Мои аргументы для WinAFL выглядят примерно так. Давай разберем по порядку самые важные из них.
Код:
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 изменит @@ на полный путь к входному файлу.

ПРОКАЧКА WINAFL — ДОБАВЛЯЕМ СЛОВАРЬ​

Наша цель простая — увеличить количество путей, находимых за секунду. Для этого ты можешь распараллелить работу фаззера, поиграть с числом fuzz_iterations или попробовать фаззить умнее. И в этом тебе поможет словарь.

WinAFL умеет восстанавливать синтаксис формата данных цели (например, AFL смог самостоятельно создать валидные JPEG-файлы без какой‑либо дополнительной инфы). Обнаруженные синтаксические единицы он использует для генерации новых кейсов для фаззинга. Это занимает значительное время, и здесь ты можешь ему сильно помочь, ведь кто, как не ты, лучше всего знает формат данных твоей программы? Для этого нужно составить словарь в формате <имя переменной>="значение". Например, вот начало моего словаря:
Код:
x0="ProgVer"
x1="WrittenByVersion"
x2="FileType"
x3="Created"
x4="Modified"
x5="Name"
x6="Core"
Итак, мы нашли функцию для фаззинга, попутно расшифровав входной файл программы, создали словарь, подобрали аргументы и можем наконец‑то начать фаззить!

1726813324825.png


И первые же минуты фаззинга приносят первые краши! Но не всегда все происходит так гладко. Ниже я привел несколько особенностей WinAFL, которые могут тебе помочь (или помешать) отладить процесс фаззинга.

ОСОБЕННОСТИ WINAFL​

Побочные эффекты​

Вначале я писал, что функция для фаззинга не должна иметь побочных эффектов. Но это в идеале. Часто бывает так, что разработчики забывают добавить в свои программы такие красивые функции, и приходится иметь дело с тем, что есть.

Так как некоторые эффекты накапливаются, возможно, тебе удастся успешно пофаззить, уменьшив число fuzz_iterations — с ней WinAFL будет перезапускать твою программу чаще. Это негативно повлияет на скорость, но зато уменьшит количество побочных эффектов.

Дебаг-режим​

Если WinAFL отказывается работать, попробуй запустить его в дебаг‑режиме. Для этого добавь параметр -debug к аргументам библиотеки инструментации. После этого в текущем каталоге у тебя появится текстовый лог. При нормальной работе в нем должно быть одинаковое количество строчек In pre_fuzz_handler и In post_fuzz_handler. Также должна присутствовать фраза Everything appears to be running normally.

1726813361759.png


Не забудь выключить дебаг‑режим! С ним WinAFL откажется фаззить, даже если все работает, ссылаясь на то, что целевая программа вылетела по тайм‑ауту. Не верь ему и выключай отладку.

Эмуляция работы WinAFL​

Иногда при фаззинге программу так перемыкает, что она крашится даже на подготовительном этапе работы WinAFL, после чего он разумно отказывается действовать дальше. Чтобы хоть как‑то в этом разобраться, ты можешь вручную эмулировать работу фаззера. Для этого ставь точку останова на начало и конец функции для фаззинга. Когда выполнение достигнет конца функции, правь аргументы, равняй стек, меняй RIP/EIP на начало функции — и так, пока что‑то не сломается.

Стабильность​

Stability — очень важный параметр. Он показывает, насколько карта покрытия кода меняется от итерации к итерации. 100% — на каждой итерации программа ведет себя абсолютно одинаково. 0% — каждая итерация полностью отличается от предыдущей. Разумеется, нам нужно значение где‑то посередине. Автор AFL решил, что ориентироваться надо где‑то на 85%. В нашем примере стабильность держится на уровне 9,5%. Полагаю, это может быть связано в том числе с тем, что программа собрана статически и на стабильность негативно влияют какие‑то из используемых библиотечных функций. Возможно, и мультипоточность тоже повлияла на это.

Набор входных файлов​

Чем больше покрытие кода, тем выше шанс найти баг. А максимального покрытия кода можно добиться, создав хороший набор входных файлов. Если ты задался целью пофаззить парсеры файлов каких‑то хорошо известных форматов, то, как говорится, гугл в помощь: некоторым исследователям удавалось собрать внушительный набор файлов именно с помощью парсинга выдачи Google. Такой набор потом можно минимизировать с помощью скрипта [winafl-cmin.py](http://winafl-cmin.py) из того же репозитория WinAFL. А если ты, как и я, предпочитаешь парсилки файлов проприетарных форматов, то поисковик не так часто будет способен помочь. Приходится посидеть и поковыряться в программе, чтобы нагенерировать набор интересных файлов.

Отучаем программу ругаться​

Моя программа довольно многословна и ругалась на неверный формат входного файла, показывая всплывающие сообщения.

1726813388977.png


Такие проблемы ты легко сможешь вылечить, пропатчив используемую программой библиотеку или саму программу.

автор moskvin-slava aka Вячеслав Москвин
xakep.ru
 
Последнее редактирование модератором:


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх