Разбираемся в устройстве AFL++. Часть 2

Ссылки на остальные статьи данного цикла:

От автора

Напоминаю, что статья потенциально может содержать ошибки и неточности, автор открыт к диалогу (и самообразованию), если Вам есть что подсказать, исправить и скорректировать — пишите!

Инициализация и запуск forkserver

Прежде чем обсуждать процесс инициализации, нам нужно рассмотреть одну из особенностей ELF-файлов, известную как init array.

Когда исполняемый бинарный файл запускается, функция main не вызывается напрямую. Вместо этого выполнение всегда начинается с интерпретатора (обычно это ld.so). Интерпретатор выполняет множество ключевых операций по настройке структуры процессов, таких как динамическая линковка. Однако менее известной особенностью интерпретатора является его способность запускать функции инициализации. Эти функции инициализации находятся в секции, называемой .init_array. Эта секция содержит массив функций, которые выполняются последовательно, прямо перед вызовом main. Это наглядно можно увидеть в коде glibc и в почитать в документации

Функции в секции .init_array зависят от того, был ли бинарный файл скомпилирован с использованием инструментаций PCGUARD или LTO (однако, как мы убедились выше, в большинстве случаев они остаются одинаковыми).

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

Обычные переменные:

  • __afl_area_ptr — Указатель на карту покрытия. По умолчанию это __afl_area_initial. Размер __afl_area_initial равен MAP_INITIAL_SIZE.

  • __afl_final_loc — Последний индекс в __afl_area_ptr, к которому был осуществлен доступ при помощи инструментации. По сути это конец массива карты покрытий.

  • __afl_map_addr — Адрес, по которому карта покрытия будет отображена через mmap. Эта переменная существует только в случае, если AFL_LLVM_MAP_ADDR установлен в режиме LTO. В противном случае её значение равно 0.

  • __afl_map_size — Размер карты покрытия.

  • __afl_area_initial — указатель на массив, который является картой покрытия и используется в общей (разделяемой) памяти между инстансами afl-fuzz.

Переменные окружения:

  • __AFL_SHM_ID (алиас на SHM_ENV_VAR) — Идентификатор общей памяти для карты покрытия.

  • __AFL_SHM_FUZZ_ID (алиас SHM_FUZZ_ENV_VAR) — Идентификатор общей памяти для фаззинга через общую память.

  • AFL_MAP_SIZE — Используется для задания размера буфера общей памяти, выделяемого afl-fuzz.

Макросы:

  • MAP_SIZE — Пользовательское значение, которое afl-fuzz будет использовать для задания размера карты покрытия общей памяти. изначально зависит от системы

  • MAP_INITIAL_SIZE — Размер __afl_area_initial.

Теперь начнем с PCGUARD. Функции инициализации выглядят следующим образом:

init_func_table

init_func_table

Все эти функции (за исключением sancov.module_ctor_trace_pc_guard) можно найти в исходниках

Итак, далее мы рассмотрим функции все функции из скриншота выше.

  1. __afl_auto_first — она по сути ничего не делает, только устанавливает __afl_already_initialized_first = 1, чтобы показать, что был запущен процесс инициализации и проверяет наличие LLVM инструментации.

  2. __afl_auto_second — сравнивает __afl_final_loc и MAP_INITIAL_SIZE. Если первый параметр меньше второго, то выделяет доп.память и обновляет все указатели, связанные с картой покрытия

  3. sancov.module_ctor_trace_pc_guard — просто обернутый вызов функции __sanitizer_cov_trace_pc_guard_init.

    cov_trace_pc_guard

    cov_trace_pc_guard

  4. __afl-auto_eraly — обертка для вызова __afl_map_shm.

  5. __early_forkserver— обертка для вызова функции __afl_auto_init.

  6. __afl_auto_init -

Далее разберем каждую функцию подробнее:

Рассмотрим функцию __sanitizer_cov_trace_pc_guard_init более подробно:

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {

  u32   inst_ratio = 100;
  char *x;

  _is_sancov = 1;

...

  if (start == stop || *start) return;

	...

   if (__afl_final_loc < 4) __afl_final_loc = 4;  // we skip the first 5 entries

  *(start++) = ++__afl_final_loc; //start at 4

  while (start < stop) {

    if (likely(inst_ratio == 100) || R(100) < inst_ratio)
      *start = ++__afl_final_loc;
    else
      *start = 0;  // write to map[0]

    start++;

  }
...

Эта функция принимает два параметра: start и stop, которые обозначают начало и конец массива меток защиты (guards, как показано в приведённом выше примере). Эти метки фактически составляют карту покрытия — массив состояний покрытия ребер кода.

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

Напомню, что метки защиты (guards) привязаны к определённым участкам кода. Эти метки:

  • Хранятся в массиве, который фактически является частью карты покрытия.

  • Выступают в роли уникальных идентификаторов для различных участков кода.

В тестовом примере из начала статьи такими метками выступали dword_91A0 и dword_919C.

Пара слов об этой строке кода:

if (likely(inst_ratio == 100) || R(100) < inst_ratio)

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

likely(inst_ratio == 100)

Указывает, что в большинстве случаев переменная inst_ratio будет равна 100. Это помогает компилятору оптимизировать ветвление для данного условия

R(100) — это макрос или функция, которая возвращает случайное число от 0 до 99 (включительно).

Если inst_ratio == 100, все метки защиты активируются.
Если inst_ratio < 100, метки активируются случайным образом с вероятностью, равной inst_ratio.

Следующая функция — __afl_auto_early. Эта функция вызывает в себе функцию __afl_map_shm.

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

Разделяемая память — это механизм в Linux, который позволяет создавать области памяти, доступные для нескольких процессов. Каждая область разделяемой памяти имеет собственный уникальный идентификатор (ID), который ее идентифицирует. Чтобы два процесса могли получить доступ к одной и той же разделяемой памяти, им нужно только сопоставить память с одинаковым ID. Для более подробной информации о разделяемой памяти рекомендуется обратиться к странице руководства

По сути, afl-fuzz создает экземпляр разделяемой памяти, а целевой процесс сопоставляет дескриптор этой памяти с картой покрытия. В результате afl-fuzz может видеть любые изменения, которые дочерний процесс вносит в карту покрытия.

Функция вышла достаточно объемной, поэтому было решение объяснить её работу через комментарии.
Из функции также вырезаны отладочные сообщения, режим CMPlog и режим частичной инструментации кода, а также обработка некоторых ошибок. Полный код функции доступен тут

static void __afl_map_shm(void) {

  // Если разделяемая память уже инициализирована, выходим
  if (__afl_already_initialized_shm) return;
  __afl_already_initialized_shm = 1;

  // Если указатель карты покрытия не установлен, используем заглушку
  if (!__afl_area_ptr) { __afl_area_ptr = __afl_area_ptr_dummy; }

  // Получаем идентификатор разделяемой памяти из переменной окружения
  char *id_str = getenv(SHM_ENV_VAR);

  // Проверяем финальное местоположение карты покрытия
  if (__afl_final_loc) { // !<----------- максмальный размер массива карты покрытия
    __afl_map_size = ++__afl_final_loc;  // Увеличиваем, так как отсчет начинается с 0

     // Если размер больше допустимого MAP_SIZE, предупреждаем пользователя
   if (__afl_final_loc > MAP_SIZE) {
      char *ptr;
      u32 val = 0;
      if ((ptr = getenv("AFL_MAP_SIZE")) != NULL) { val = atoi(ptr); }
      if (val < __afl_final_loc) {
        if (__afl_final_loc > MAP_INITIAL_SIZE && !getenv("AFL_QUIET")) {
          fprintf(stderr,
                  "Warning: AFL++ tools might need to set AFL_MAP_SIZE to %u "
                  "to be able to run this instrumented program if this "
                  "crashes!\n",
                  __afl_final_loc);
        }
      }
    }
  }

  
  // Проверяем, запущено ли приложение под управлением afl-fuzz
  if (__afl_sharedmem_fuzzing && (!id_str || !getenv(SHM_FUZZ_ENV_VAR) ||
                                  fcntl(FORKSRV_FD, F_GETFD) == -1 ||
                                  fcntl(FORKSRV_FD + 1, F_GETFD) == -1)) {
    __afl_sharedmem_fuzzing = 0;
  }

  // Если ID разделяемой памяти отсутствует, инициализируем значения по умолчанию
  if (!id_str) {
   ...
  }

  // Если приложение запущено под AFL++, привязываем карту покрытия
  if (id_str) {
    if (__afl_area_ptr && __afl_area_ptr != __afl_area_initial &&
        __afl_area_ptr != __afl_area_ptr_dummy) {
      if (__afl_map_addr) {
        munmap((void *)__afl_map_addr, __afl_final_loc);  // Освобождаем предыдущую память
      } else {
        free(__afl_area_ptr);  // Освобождаем старую память
      }
      __afl_area_ptr = __afl_area_ptr_dummy;  // Сбрасываем указатель
    }

    // Преобразуем строковый ID в числовой
    u32 shm_id = atoi(id_str);

    // Проверяем, соответствует ли размер карты
    if (__afl_map_size && __afl_map_size > MAP_SIZE) {
      u8 *map_env = (u8 *)getenv("AFL_MAP_SIZE");
      if (!map_env || atoi((char *)map_env) < MAP_SIZE) {
        fprintf(stderr, "FS_ERROR_MAP_SIZE\n");
        send_forkserver_error(FS_ERROR_MAP_SIZE);
        _exit(1);
      }
    }

//-----------------Здесь происходит вся магия---------------
    // Привязываем разделяемую память
    __afl_area_ptr = (u8 *)shmat(shm_id, (void *)__afl_map_addr, 0);

    // Обрабатываем ошибки привязки
    if (!__afl_area_ptr || __afl_area_ptr == (void *)-1) {
      if (__afl_map_addr)
        send_forkserver_error(FS_ERROR_MAP_ADDR);
      else
        send_forkserver_error(FS_ERROR_SHMAT);

      perror("shmat for map");
      _exit(1);
    }

    // Записываем что-либо в карту, чтобы AFL не прекращал работу
    __afl_area_ptr[0] = 1;

   ...
  }

  // Сохраняем резервный указатель на карту
  __afl_area_ptr_backup = __afl_area_ptr;

...
}
```

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

Выполнения данной функции приводит к тому, что __afl_area_ptr настроен и располагается в разделяемой (общей) памяти и доступен для всех потомков процесса afl-fuzz. Это так же нужно для работы параллельного фаззинга.

Теперь перейдем к функции — __early_forkserver. Эта функция зачастую ничего не делает, поэтому пропустим её.

Последняя функция — __afl_auto_init — эта функция вызывает функцию __afl_manual_init();, а она в свою очередь вызывает __afl_start_forkserver();. Её мы и рассмотрим подробнее:

Итак, функция __afl_start_forkserver();. Исходники можно посмотреть тут, в рамках статьи же рассмотрим эту функцию тезисно, аналогично функции __afl_auto_early:

Не обходится тут и без небольших шуток от разработчиков.

  //Проверяет, используется ли старый режим форксервера (без новых функций). Если __afl_old_forkserver равно 0, выполняется инициализация нового форксервера
  if (!__afl_old_forkserver) {
   
    //Отправляет сообщение родителю через канал. Если отправка не удалась, форксервер завершает выполнение, предполагая, что он не используется.
    if (write(FORKSRV_FD + 1, msg, 4) != 4) { return; }
    //Ожидает ответа от родителя. Если чтение завершилось ошибкой, программа аварийно завершается
    if (read(FORKSRV_FD, reply, 4) != 4) { _exit(1); }
    if (tmp != status2) {

      write_error("wrong forkserver message from AFL++ tool");
      _exit(1);

    }
    ...
    //Устанавливает опции для форксервера
    status = FS_NEW_OPT_MAPSIZE; //Всегда включён, указывает на размер карты покрытия.
    if (__afl_sharedmem_fuzzing) { status |= FS_NEW_OPT_SHDMEM_FUZZ; } //Включается, если используется совместная память для фаззинга.
    if (__afl_dictionary_len && __afl_dictionary) { status |= FS_NEW_OPT_AUTODICT; } //Включается, если доступен автословарь

    //Отправляет опции форксерверу. Завершает выполнение при ошибке записи
    if (write(FORKSRV_FD + 1, msg, 4) != 4) { _exit(1); }
    //Отправляет размер карты покрытия
    status = __afl_map_size;
    if (write(FORKSRV_FD + 1, msg, 4) != 4) { _exit(1); }

    // Далее идет работа с автословарем, опустим
    ...
    //Завершает рукопожатие, отправляя сообщение с версией протокола    
    status = version;
    if (write(FORKSRV_FD + 1, msg, 4) != 4) { _exit(1); }
    //Устанавливает флаг, сигнализирующий, что форксервер подключён к AFL.
    __afl_connected = 1;
    // Если используется совместная память, выполняется её настройка
     if (__afl_sharedmem_fuzzing) { __afl_map_shm_fuzz(); }

     while(1) { ...}
  }

Внутри цикла while(1) происходит основная работа фаззера. Расписывать построчно его действия не имеет смысла, поэтому тезизсно опишу логику работы программы внутри этого цикла:

Как мы помним — AFL++ создает дочерний процесс, через который контролирует запуск уже своих дочерних процессов с помощью функции fork.

(это необходимо для повышения скорости фаззинга — дочерний процесс главного процесса AFL++ создает свой fork непосредственно перед запуском исследуемой программы, а поскольку фаззер постоянно перезапускается, таким образом можно не тратить время на новый запуск и инициализацию процесса — возвращаемся к исходному состоянию программы перед запуском → запускаем → выполняем → завершаем, собираем покрытие → убиваем fork и запускаем новый fork с исходным состоянием)

Непосредственно здесь он получает (через каналы) информацию от родительского процесса с тем, а что именно сейчас необходимо делать — если процесс приостановлен (это корректно для режима persistant mode, о котором ниже), то запускает его. Если процесс завершен (целевая функция успешно отработала), то создаем новый fork дочернего процесса. После этого передаем PID созданного fork’a дочернего процесса в родительский и ожидаем остановки или завершения этого процесса.

Словом, здесь происходит общение между процессами AFL++ с отслеживанием состояния fork’a — процесса, который непосредственно и запускает целевую программу.

Итак, наш фаззер настроен, запущен и нормально работает. Но есть техники, которые позволяют существенно ускорить работу фаззера. Об одной из них ниже.

AFL++ Persistent Mode

Persistent Mode — это особый режим работы фаззера. Когда фаззер работает в обычном режиме, ему нужно каждый раз запускать fork целевой программы, что может занимать много времени, особенно для сложных или длительных процессов. В persistent mode программа запускается один раз, и фаззер использует уже запущенный экземпляр программы для выполнения множества тестов без необходимости перезапуска программы, что значительно ускоряет процесс фаззига. Настоятельно рекомендую почитать документацию на этот механизм.

Рассмотрим следующий фрагмент кода с простейшим примером использования persistant mode:

#include "what_you_need_for_your_target.h"

#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_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)) {

    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++ разворачиваются в достаточно простые команды, которые создают статический массив для входных данных fuzz_buf, считывают в него данные функцией read и записывают длину полученного массива в fuzz_len.
Фактически архитектурно работа фаззера здесь представлена также, как и в обычном режиме фаззера — подаются данные (обычно из потока ввода) на вход программы, программа запускается, выполняется, собирается обратная связь. Здесь же все тоже самое, но данные подаются напрямую на функцию цель, а fork программы не завершается после выполнения программы, а выполняется в цикле __AFL_LOOP.

Запуск фаззера в persistant mode так же влияет на процесс работы forkserver'a:

if (unlikely(waitpid(child_pid, &status, is_persistent ? WUNTRACED : 0) < 0)) {
  write_error("waitpid");
  _exit(1);
}

Если фаззинг запушен в режиме persistant mode, то мы ждем, пока дочерний процесс не остановится (WUNTRACED), а не завершится. Причина этого кроется в макросе __AFL_LOOP, который расширяется в функцию __afl_persistent_loop. Исходник функции

Идея этой функции в том, что существует статическая переменная cycle_cnt, которая отвечает за количество оставшихся циклов. В конце каждого цикла (кроме случая, когда cycle_cnt = 0, в этом случае происходит остановка), посылается сигнал SIGSTOP . Это вызывает выполнение условия waitpid, что позволяет afl-fuzz выполнить свои мутации и обновить карту покрытия. Как только управление возвращается в форксервер, бинарник возобновляется с помощью сигнала SIGCONT — еще раз внимательно смотрим код функции __afl_start_forkserver.

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

__AFL_FUZZ_TESTCASE_LEN — это фактическая длина этого буфера.

Инициализация фаззинга начинается с функции __afl_map_shm_fuzz. Эта функция вызывается из __afl_start_forkserver всякий раз, когда запускается фаззинг с использованием общей (разделяемой) памяти. Для этого используется переменная __afl_sharedmem_fuzzing, которая устанавливается в 1 всякий раз, когда используется общая память. Эта переменная так же поднимается в 1, когда используется макрос __AFL_FUZZ_INIT()

__afl_map_shm_fuzz работает очень похоже на __afl_map_shm, которую мы рассматривали выше. Она получает идентификатор shmem из переменной окружения __AFL_SHM_FUZZ_ID — как я писал выше это всего лишь обертка для SHM_FUZZ_ENV_VAR. Затем она запускает функцию shmat для сопоставления области разделяемой памяти с процессом.

После сопоставления области мы устанавливаем __afl_fuzz_len и __afl_fuzz_ptr (это define для __AFL_FUZZ_TESTCASE_LEN и __AFL_FUZZ_TESTCASE_BUF соответственно) следующим образом:

__afl_fuzz_len = (u32 *)map;
__afl_fuzz_ptr = map + sizeof(u32);

Важно! Обратите внимание, что __afl_fuzz_len на самом деле является типом данных u32*, поскольку первые 4 байта карты памяти на самом деле являются размером данных.

Пара слов про санитайзеры кода

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

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

Особенность работы санитайзеров напрашивается на отдельную статью, благо Google опубликовал отличный вики-ресурс, который описывает каждый санитайзер и его реализацию — искать тут.

Конец

В следующей части подробно поговорим о инициализацииafl-fuzz

© Habrahabr.ru