[Перевод] Фаззинг с AFL++. llvm_mode persistent mode

956bace546d8f7053f9e22433e447473.png

Введение

В постоянном режиме AFL++ проверяет цель несколько раз в одном процессе, вместо того, чтобы создавать новый процесс для каждого выполнения фазза. Это самый эффективный способ фаззинга, так как скорость может быть в x10 или x20 раз выше без каких-либо недостатков. Весь профессиональный фаззинг использует этот режим.

Постоянный режим требует, чтобы цель могла быть вызвана в одной или нескольких функциях, и чтобы ее состояние могло быть полностью сброшено, чтобы можно было выполнить несколько вызовов без утечки ресурсов, и чтобы предыдущие запуски не влияли на последующие. Индикатором этого является значение стабильности в пользовательском интерфейсе afl-fuzz. Если это значение уменьшается до меньших значений в постоянном режиме по сравнению с непостоянным режимом, значит, цель фазза сохраняет состояние. Примеры можно найти в utils/persistent_mode.

TL; DR:

Например fuzz_target.c:

#include "what_you_need_for_your_target.h"

__AFL_FUZZ_INIT();

main() {

  // anything else here, e.g. command line arguments, initialization, etc.

#ifdef __AFL_HAVE_MANUAL_CONTROL
  __AFL_INIT();
#endif

  unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;  // must be after __AFL_INIT
                                                 // and before __AFL_LOOP!

  while (__AFL_LOOP(10000)) {
![AFL++ logo](https://raw.githubusercontent.com/AFLplusplus/Website/master/static/aflpp_bg.svg)
    int len = __AFL_FUZZ_TESTCASE_LEN;  // don't use the macro directly in a
                                        // call!

    if (len < 8) continue;  // check for a required/useful minimum input length

    /* Setup function call, e.g. struct target *tmp = libtarget_init() */
    /* Call function to be fuzzed, e.g.: */
    target_function(buf, len);
    /* Reset state. e.g. libtarget_free(tmp) */

  }

  return 0;

}

После компилим:

afl-clang-fast -o fuzz_target fuzz_target.c -lwhat_you_need_for_your_target

И все! Прирост скорости обычно составляет от x10 до x20.

Если вы хотите иметь возможность компилировать цель без afl-clang-fast/lto, то добавьте это сразу после include:

#ifndef __AFL_FUZZ_TESTCASE_LEN
  ssize_t fuzz_len;
  #define __AFL_FUZZ_TESTCASE_LEN fuzz_len
  unsigned char fuzz_buf[1024000];
  #define __AFL_FUZZ_TESTCASE_BUF fuzz_buf
  #define __AFL_FUZZ_INIT() void sync(void);
  #define __AFL_LOOP(x) ((fuzz_len = read(0, fuzz_buf, sizeof(fuzz_buf))) > 0 ? 1 : 0)
  #define __AFL_INIT() sync()
#endif

Отложенная инициализация

AFL++ пытается оптимизировать производительность, выполняя целевой двоичный файл только один раз, останавливая его непосредственно перед main(), а затем клонируя этот «главный» процесс, чтобы получить постоянный запас целей для fuzz.

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

В таких случаях полезно инициализировать forkserver немного позже, когда большая часть работы по инициализации уже выполнена, но до того, как двоичный файл попытается прочитать входные данные fuzzed и разобрать их; в некоторых случаях это может дать прирост производительности более чем в 10 раз. Вы можете реализовать отложенную инициализацию в режиме LLVM довольно простым способом.

Во-первых, найдите подходящее место в коде, где может происходить отложенное клонирование. Это нужно делать с особой осторожностью, чтобы не сломать двоичный код. В частности, программа, вероятно, будет работать неправильно, если вы выберете место после:

  1. Создание любых жизненно важных потоков или дочерних процессов — поскольку forkserver не может легко клонировать их.

  2. Инициализация таймеров с помощью setitimer() или аналогичных вызовов.

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

  4. Любой доступ к входным данным fuzzed, включая чтение метаданных об их размере.

  5. Выбрав местоположение, добавьте этот код в соответствующее место:

#ifdef __AFL_HAVE_MANUAL_CONTROL
  __AFL_INIT();
#endif

Защитные #ifdef не нужны, но их включение гарантирует, что программа будет работать нормально при компиляции с помощью инструмента, отличного от afl-clang-fast/ afl-clang-lto/afl-gcc-fast.

Наконец, перекомпилируйте программу с помощью afl-clang-fast/afl-clang-lto/afl-gcc-fast (afl-gcc или afl-clang не будут генерировать двоичный файл с отложенной инициализацией) — и все готово!

Постоянный режим — Persistance mode

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

Основная структура программы, которая это делает, будет выглядеть следующим образом:

  while (__AFL_LOOP(1000)) {

    /* Read input data. */
    /* Call library code to be fuzzed. */
    /* Reset state. */

  }

  /* Exit normally. */

Числовое значение, указанное в цикле, контролирует максимальное количество итераций, прежде чем AFL++ перезапустит процесс с нуля. Это минимизирует влияние утечек памяти и подобных глюков; 1000 является хорошей отправной точкой, а увеличение этого значения увеличивает вероятность зависаний, не давая реального выигрыша в производительности.

Более подробный шаблон показан в utils/persistent_mode. Как и отложенная инициализация, эта функция работает только в afl-clang-fast; для подавления ее при использовании других компиляторов можно использовать защитные символы #ifdef.

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

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

Фаззинг общей памяти

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

Настроить это очень просто:

После include установите следующий макрос:

__AFL_FUZZ_INIT();

Непосредственно в начале main — или, если вы используете отложенный forkserver с __AFL_INIT(), то после __AFL_INIT():

unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;

Затем в качестве первой строки после цикла __AFL_LOOP while:

int len = __AFL_FUZZ_TESTCASE_LEN;

© Habrahabr.ru