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

9e4c686e0cab288d0d4d457cf7b641a3.png

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

От автора

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

Продолжаем рассматривать afl-fuzz

Время выполнения afl-fuzz основывается на единственном цикле do-while, который выполняет основную логику работы фаззера:

do {
  if (likely(!afl->old_seed_selection)) {
    // Если в очереди появились новые элементы (prev_queued_items < afl->queued_items) или требуется реинициализация таблицы
    // (afl->reinit_table), пересоздается alias-таблица, которая помогает приоритетно обрабатывать элементы очереди.
    if (unlikely(prev_queued_items < afl->queued_items || 
                 afl->reinit_table)) {
      prev_queued_items = afl->queued_items;
      create_alias_table(afl);
    }
    // C помощью функции select_next_queue_entry выбирается следующий элемент из очереди для обработки.
    // Проверяется, что индекс выбранного элемента `current_entry` находится в пределах доступных элементов в очереди.
    // Если нет, продолжается выбор.
    // После выбора элемент записывается в queue_cur.
    do {
      afl->current_entry = select_next_queue_entry(afl);
    } while (unlikely(afl->current_entry >= afl->queued_items));
    afl->queue_cur = afl->queue_buf[afl->current_entry];
  }
  //Основная часть фаззинга происходит в функции fuzz_one
  skipped_fuzz = fuzz_one(afl);

  ...
  // Старая стратегия выбора seed'ов, не будем на ней останавливаться, сейчас испольузется редко
  if (unlikely(!afl->stop_soon && exit_1)) { afl->stop_soon = 2; }
  if (unlikely(afl->old_seed_selection)) {
    while (++afl->current_entry < afl->queued_items &&
           afl->queue_buf[afl->current_entry]->disabled) {};
    if (unlikely(afl->current_entry >= afl->queued_items || 
                 afl->queue_buf[afl->current_entry] == NULL || 
                 afl->queue_buf[afl->current_entry]->disabled)) {
      afl->queue_cur = NULL;
    } else {
      afl->queue_cur = afl->queue_buf[afl->current_entry];
    }
  }
} while (skipped_fuzz && afl->queue_cur && !afl->stop_soon);

В приведенном выше фрагменте кода мы, по сути, настраиваем очередь для фаззинга.

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

Входные данные подаются в функцию с названием common_fuzz_stuff. Вот соответствующий фрагмент кода:

  u8 fault;
  if (unlikely(len = write_to_testcase(afl, (void **)&out_buf, len, 0)) == 0) {
    return 0;
  }
  fault = fuzz_run_target(afl, &afl->fsrv, afl->fsrv.exec_tmout);

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

if (likely(fsrv->use_shmem_fuzz)) {
    // Если используется общая память для фаззинга, ограничиваем длину передаваемых данных.
    if (unlikely(len > MAX_FILE)) 
        len = MAX_FILE;  // Если длина данных больше максимального размера файла, устанавливаем длину на максимально возможное значение.
    // Записываем длину в общую память.
    *fsrv->shmem_fuzz_len = len;
    // Копируем данные из буфера в общую память.
    memcpy(fsrv->shmem_fuzz, buf, len);
    ...
} else {
    // Если не используется общая память, работаем с файловым дескриптором.
    s32 fd = fsrv->out_fd;
    // Если не используется стандартный ввод и задан выходной файл, работаем с файлом.
    if (!fsrv->use_stdin && fsrv->out_file) {   
        // Если не нужно удалять файл, обрабатываем его по обычному сценарию.
        if (unlikely(fsrv->no_unlink)) {
            ...
        } else {
            // Если нужно удалить файл перед записью, вызываем unlink, игнорируя возможные ошибки.
            unlink(fsrv->out_file);  // Игнорируем ошибки удаления файла.
            
            // Создаем новый файл с флагами: запись, создание и исключение, если файл уже существует.
            fd = open(fsrv->out_file, O_WRONLY | O_CREAT | O_EXCL, DEFAULT_PERMISSION);
        }
        
        // Проверка, что файл был успешно открыт, иначе выводим ошибку.
        if (fd < 0) { 
            PFATAL("Unable to create '%s'", fsrv->out_file); 
        }
    } else if (unlikely(fd <= 0)) {
        // Если файловый дескриптор не задан, возникает ошибка.
        FATAL("Nowhere to write output to (neither out_fd nor out_file set (fd is %d))", fd);
    } else {
        // Если дескриптор валиден, устанавливаем его в начало файла.
        lseek(fd, 0, SEEK_SET);
    }
    // Пишем данные в файл или на выходной дескриптор.
    ck_write(fd, buf, len, fsrv->out_file);
    
    // Если используется stdin, обрабатываем файл как стандартный ввод.
    if (fsrv->use_stdin) {
        // Обрезаем файл до нужной длины.
        if (ftruncate(fd, len)) { 
            PFATAL("ftruncate() failed"); 
        }
        
        // Сдвигаем указатель на начало файла.
        lseek(fd, 0, SEEK_SET);
    } else {
        // Если не используется stdin, закрываем файл.
        close(fd);
    }
}

Ссылка на этот кусок кода

Первое условие проверяет, используем ли мы фаззинг через общую память. Если да (то есть, если fsrv->use_shmem_fuzz != 0), то мы просто копируем входные данные в буфер общей памяти с помощью memcpy и записываем длину данных.

Если же используется ввод из файла, то мы попадаем в блок else.

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

Внутри блока if происходит: удаление старого файла (unlink(fsrv->out_file)), создание нового файла с помощью функции open с флагами: запись, создание и исключение, если файл уже существует.

Если используется stdin, то мы просто устанавливаем указатель файла в начало с помощью lseek(fd, 0, SEEK_SET).

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

Если используется stdin, то после записи данных в файл, мы также вызываем ftruncate(fd, len), чтобы обрезать файл до нужной длины. Это предотвращает наличие старых данных в файле после завершения работы программы.
Затем выполняется lseek(fd, 0, SEEK_SET), чтобы указатель на чтение был установлен на начало файла, что гарантирует, что дочерний процесс прочитает данные с самого начала.

На этом этапе мутированные данные записываются. Все, что нам нужно сделать — это перезапустить сервер форков. Это выполняется в функции fuzz_run_target. Эта функция на самом деле просто запускает таймер и вызывает afl_fsrv_run_target. Большая часть этой функции занимается обработкой альтернативных режимов работы AFL++, поэтому я привел ниже самый важный фрагмент кода данной функции.

fsrv_run_result_t __attribute__((hot))
afl_fsrv_run_target(afl_forkserver_t *fsrv, u32 timeout,
                    volatile u8 *stop_soon_p) {
  s32 res;
  u32 exec_ms;
  u32 write_value = fsrv->last_run_timed_out;
...
  /* После этого memset, fsrv->trace_bits[] становятся эффективно volatile, 
     поэтому мы должны предотвратить любые предыдущие операции, которые могут 
     попасть в эту область. */
  memset(fsrv->trace_bits, 0, fsrv->map_size);
  MEM_BARRIER(); //commit all writes

  /* у нас есть работающий сервер форков (или его подделка). 
     Сначала сообщим, если предыдущий запуск завершился с тайм-аутом. */
  if ((res = write(fsrv->fsrv_ctl_fd, &write_value, 4)) != 4) {
    if (*stop_soon_p) { return 0; }
    RPFATAL(res, "Unable to request new process from fork server (OOM?)");
  }
  fsrv->last_run_timed_out = 0;
  if ((res = read(fsrv->fsrv_st_fd, &fsrv->child_pid, 4)) != 4) {
    if (*stop_soon_p) { return 0; }
    RPFATAL(res, "Unable to request new process from fork server (OOM?)");
  }
 
...
  exec_ms = read_s32_timed(fsrv->fsrv_st_fd, &fsrv->child_status, timeout,
                           stop_soon_p);

  if (exec_ms > timeout) {

    /* Если после тайм-аута не было ответа от сервера форков, 
       мы убиваем дочерний процесс. Сервер форков должен сообщить нам об этом позже */
    s32 tmp_pid = fsrv->child_pid;
    if (tmp_pid > 0) {
      kill(tmp_pid, fsrv->child_kill_signal);
      fsrv->child_pid = -1;
    }
    fsrv->last_run_timed_out = 1;
    if (read(fsrv->fsrv_st_fd, &fsrv->child_status, 4) < 4) { exec_ms = 0; }
  }
  if (!exec_ms) {
    if (*stop_soon_p) { return 0; }
    SAYF("\n" cLRD "[-] " cRST
         "Unable to communicate with fork server. Some possible reasons:\n\n"
         "    - You've run out of memory. Use -m to increase the the memory "
         "limit\n"
         "      to something higher than %llu.\n"
         "    - The binary or one of the libraries it uses manages to "
         "create\n"
         "      threads before the forkserver initializes.\n"
         "    - The binary, at least in some circumstances, exits in a way "
         "that\n"
         "      also kills the parent process - raise() could be the "
         "culprit.\n"
         "    - If using persistent mode with QEMU, "
         "AFL_QEMU_PERSISTENT_ADDR "
         "is\n"
         "      probably not valid (hint: add the base address in case of "
         "PIE)"
         "\n\n"
         "If all else fails you can disable the fork server via "
         "AFL_NO_FORKSRV=1.\n",
         fsrv->mem_limit);
    RPFATAL(res, "Unable to communicate with fork server");
  }
  if (!WIFSTOPPED(fsrv->child_status)) { fsrv->child_pid = -1; }
  fsrv->total_execs++;
  /* Любые последующие операции с fsrv->trace_bits не должны быть перемещены 
     компилятором ниже этой точки. После этого момента fsrv->trace_bits[] 
     ведут себя нормально и не должны рассматриваться как volatile. */

  MEM_BARRIER();
  /* Сообщаем результат вызывающему коду. */
  /* Был ли запуск неудачным? (а был ли мальчик...?)*/
  if (unlikely(*(u32 *)fsrv->trace_bits == EXEC_FAIL_SIG)) {
    return FSRV_RUN_ERROR;
}
  ...
  /* успех :) */
  return FSRV_RUN_OK;
}

Давайте пройдемся по этой функции построчно:

Мы начинаем с выполнения memset с нулем для fsrv->trace_bits (или карты покрытия). Это гарантирует, что карта покрытия будет обнулена и сброшена перед следующим запуском.

После этого мы записываем в дочерний процесс через ctl_pipe. Это будит дочерний процесс для следующего запуска.

Затем мы читаем PID дочернего процесса из канала статуса. Напоминаю, что сервер форков записывает PID дочернего процесса в канал статуса после форка.

Предполагая, что это было успешно, мы выполняем чтение с таймаутом для статуса дочернего процесса через read_s32_timed. read_s32_timed — это функция чтение с таймаутом. То есть, после истечения заданного времени таймаута, чтение завершится, независимо от того, было ли что-то прочитано. Это помогает убедиться, что afl-fuzz не ждет бесконечно, если дочерний процесс завис. Значение, считанное с помощью read_s32_timed, — это то же самое значение, которое передается из __afl_start_forkserver здесь.

Как только это возвращено, мы проверяем, истек ли таймаут. Если таймаут истек, выполняется код обработки ошибки. Затем выполняется следующий код:

if (!WIFSTOPPED(fsrv->child_status)) { fsrv->child_pid = -1; } Источник

Этот код проверяет, остановлен ли дочерний процесс. Если дочерний процесс не остановлен (то есть завершился), мы просто устанавливаем fsrv->child_pid = -1, что очищает PID для следующего запуска. Напоминаю, что дочерний процесс может быть остановлен только в режиме постоянного процесса, и если используется этот режим, то дочерний процесс будет перезапущен. Таким образом, если используется режим постоянного процесса, нет смысла устанавливать fsrv->child_pid = -1, поскольку child_pid будет повторно использован.

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

Заключение

Статья сложная, кто прочитал — молодец, кто что-то понял — герой.

На русском языке крайне мало материалов на тему работы фаззера «изнутри», надеюсь, что Вы смогли что-то подчерпнуть для себя.

Что почитать?

Благодарности

@Koltiradw — за рецензии, консультацию и ответы на тупые вопросы в четыре утра.

© Habrahabr.ru