Ссылки на остальные статьи данного цикла:
От автора
Напоминаю, что статья потенциально может содержать ошибки и неточности, автор открыт к диалогу (и самообразованию), если Вам есть что подсказать, исправить и скорректировать - пишите
Инициализация afl-fuzz
Чтобы понять, как работает AFL++
, полезно взглянуть на исходный код функции afl_fuzz
.
Есть две ключевые структуры, на которые мы будем обращать внимание: afl_state_t и afl_forkserver_t.
afl_state_t
— это структура, которая управляет самим фаззером и множеством его параметров, включая параметры мутатора, оракулов, фидбеков и другие.afl_forkserver_t
— это структура, которая управляет форк-сервером. Форк-сервер — это компонент, который выполняет целевой бинарный файл. Эта структура также содержит другую ключевую информацию, включая входной файл и параметры, связанные с тем, что делает форк-сервер (например, работает ли он в режиме Nyx или QEMU?).afl_forkserver_t
встроен вafl_state_t
.
Для инициализации afl-fuzz
мы начинаем с функции main
в afl-fuzz.c
. Здесь много всего, и большинство из этого связано с обработкой специфичных случаев работы программы, таких как режимы QEMU, Nyx, Frida и другие.
Первое важное действие в функции main
— это получение map_size
. Это размер карты покрытия в общей памяти. Чтобы получить map_size
, вызывается функция get_map_size
, которая выглядит вот так:
/* Reads the map size from ENV */
u32 get_map_size(void) {
uint32_t map_size = DEFAULT_SHMEM_SIZE;
char *ptr;
if ((ptr = getenv("AFL_MAP_SIZE")) || (ptr = getenv("AFL_MAPSIZE"))) {
map_size = atoi(ptr);
if (!map_size || map_size > (1 << 29)) {
FATAL("illegal AFL_MAP_SIZE %u, must be between %u and %u", map_size, 64U,
1U << 29);
}
if (map_size % 64) { map_size = (((map_size >> 6) + 1) << 6); } // если значение не кратно 64, то округляем вверх
} else if (getenv("AFL_SKIP_BIN_CHECK")) {
map_size = MAP_SIZE;
}
return map_size;
}
По умолчанию используется DEFAULT_SHMEM_SIZE
, размер которого равен 8 МБ. В противном случае можно задать размер карты с помощью переменных окружения AFL_MAP_SIZE
или AFL_MAPSIZE
.
Второй важный шаг — инициализация структур afl_state_t
и afl_forkserver_t
:
afl_state_init(afl, map_size);
...
afl_fsrv_init(&afl->fsrv);
```
Эти две функции задают несколько важных переменных.
Функция afl_state_init
устанавливает размер карты покрытия в значение map_size
, как показано ниже:
afl->shm.map_size = map_size ? map_size : MAP_SIZE;
afl_fsrv_init
устанавливает следующие переменные
fsrv->out_fd = -1;
...
fsrv->use_stdin = true; //по умолчанию используем поток ввода
...
fsrv->child_pid = -1;
```
После этого мы устанавливаем afl->shmem_testcase_mode = 1
. Это гарантирует, что мы всегда будем пытаться использовать общую память для хранения тестовых данных.
Далее afl-fuzz
обрабатывает флаги командной строки и сохраняет их при необходимости. Большинство этих аргументов предназначены для управления другими режимами работы AFL++
, такими как QEMU
или Frida
. Однако есть один флаг командной строки, который нас интересует: -o,
представляющий каталог для выходных данных.
case 'o':
if (afl->out_dir) { FATAL("Multiple -o options not supported"); }
afl->out_dir = optarg;
if (!afl->fsrv.out_file) {
u32 j = optind + 1;
while (argv[j]) {
u8 *aa_loc = strstr(argv[j], "@@");
if (aa_loc && !afl->fsrv.out_file) {
afl->fsrv.use_stdin = 0;
default_output = 0;
if (afl->file_extension) {
afl->fsrv.out_file = alloc_printf("%s/.cur_input.%s", afl->tmp_dir,
afl->file_extension);
} else {
afl->fsrv.out_file = alloc_printf("%s/.cur_input", afl->tmp_dir);
}
detect_file_args(argv + optind + 1, afl->fsrv.out_file,
&afl->fsrv.use_stdin);
break;
}
++j;
}
afl->fsrv.out_file — это файл, в который записываются входные данные.
В данном коде происходит следующее:
Сначала программа проходит по аргументам командной строки (argv
) и ищет символы @@
. Напомним, что @@
заменяется на имя входного файла, если входные данные передаются через аргументы командной строки.
Пример запуска фаззера:
afl-fuzz -i in -o out -- ./bin @@
Если @@
найден, с помощью функции alloc_printf
(аналог sprintf
) формируется путь, по которому будут записываться тестовые данные. Кроме того, переменной afl->fsrv.use_stdin
присваивается значение 0
, чтобы указать, что данные поступают через аргументы командной строки, а не через стандартный поток ввода (stdin
).
Путь для записи входных данных выглядит так: [output dir]/.cur_input.
Если нужно использовать другой каталог вместо выходного, это можно сделать через переменную окружения AFL_TMPDIR
. Это особенно полезно, если указать каталог, расположенный на файловой системе, смонтированной в оперативной памяти (например, ramdisk
), что позволяет увеличить скорость фаззера.
Если @@
не найден, вызывается функция setup_stdio_file
, и входные данные передаются через стандартный ввод (stdin
).
Рассмотрим её подробнее:
void setup_stdio_file(afl_state_t *afl) {
if (afl->file_extension) {
afl->fsrv.out_file =
alloc_printf("%s/.cur_input.%s", afl->tmp_dir, afl->file_extension);
} else {
afl->fsrv.out_file = alloc_printf("%s/.cur_input", afl->tmp_dir);
}
unlink(afl->fsrv.out_file); /* Ignore errors */
afl->fsrv.out_fd =
open(afl->fsrv.out_file, O_RDWR | O_CREAT | O_EXCL, DEFAULT_PERMISSION);
if (afl->fsrv.out_fd < 0) {
PFATAL("Unable to create '%s'", afl->fsrv.out_file);
}
}
Мы почти готовы запустить фаззер. Прежде всего, нам нужно настроить дескрипторы общей памяти как для карты покрытия.
Настройка тестового примера с общей памятью:
if (afl->shmem_testcase_mode) { setup_testcase_shmem(afl);
Как мы уже обсуждали, afl->shmem_testcase_mode
всегда равен 1
, поэтому мы всегда запускаем функциюsetup_testcase_shmem
, которая выполняет следующее:
void setup_testcase_shmem(afl_state_t *afl) {
afl->shm_fuzz = ck_alloc(sizeof(sharedmem_t));
u8 *map = afl_shm_init(afl->shm_fuzz, MAX_FILE + sizeof(u32), 1);
afl->shm_fuzz->shmemfuzz_mode = 1;
if (!map) { FATAL("BUG: Zero return from afl_shm_init."); }
#ifdef USEMMAP
setenv(SHM_FUZZ_ENV_VAR, afl->shm_fuzz->g_shm_file_path, 1);
#else
u8 *shm_str = alloc_printf("%d", afl->shm_fuzz->shm_id);
setenv(SHM_FUZZ_ENV_VAR, shm_str, 1);
ck_free(shm_str);
#endif
afl->fsrv.support_shmem_fuzz = 1;
afl->fsrv.shmem_fuzz_len = (u32 *)map;
afl->fsrv.shmem_fuzz = map + sizeof(u32);
}
Этот код выполняет несколько важных действий. Во-первых, он создает саму область общей памяти с помощью afl_shm_init
. Затем устанавливает переменную окружения SHM_FUZZ_ENV_VAR
. Как мы уже видели, эта переменная используется для настройки области памяти.
Кроме того, помимо настройки переменных окружения, также настраиваются переменные fsrv
. Обратите внимание, как afl->fsrv.shmem_fuzz_len = (u32 *)map
;
Это соответствует тому, что мы видели ранее, когда в настройке инструментирования было установлено __afl_fuzz_len = (u32*)map.
Далее настраивается общая карта покрытия:
afl->fsrv.trace_bits =
afl_shm_init(&afl->shm, afl->fsrv.map_size, afl->non_instrumented_mode);
...
if (map_size <= DEFAULT_SHMEM_SIZE) {
afl->fsrv.map_size = DEFAULT_SHMEM_SIZE; // dummy temporary value
char vbuf[16];
snprintf(vbuf, sizeof(vbuf), "%u", DEFAULT_SHMEM_SIZE);
setenv("AFL_MAP_SIZE", vbuf, 1);
}
Это довольно похоже на настройку общей памяти для хранения тестовых данных, так как снова используется afl_shm_init
. Обратите внимание, что размер задается через afl->fsrv.map_size
, который мы настраивали ранее.
Когда все это настроено, мы, наконец, готовы запустить forkserver
. Это происходит через функцию afl_fsrv_get_mapsize
, которая выполняет следующие действия:
u32 afl_fsrv_get_mapsize(afl_forkserver_t *fsrv, char **argv,
volatile u8 *stop_soon_p, u8 debug_child_output) {
afl_fsrv_start(fsrv, argv, stop_soon_p, debug_child_output);
return fsrv->map_size;
Затем мы вызываем функцию afl_fsrv_start
, которая выполняет основную работу. Большая часть начала этой функции занимается настройкой параметров для различных опций и режимов работы. Основная работа начинается, когда мы настраиваем каналы и делаем fork основого процесса:
if (pipe(st_pipe) || pipe(ctl_pipe)) { PFATAL("pipe() failed"); }
...
fsrv->fsrv_pid = fork();
Здесь мы можем сосредоточиться на дочернем и родительском процессе по отдельности. Код для дочернего процесса приведен ниже:
if (!fsrv->use_stdin) {
dup2(fsrv->dev_null_fd, 0);
} else {
dup2(fsrv->out_fd, 0);
close(fsrv->out_fd);
}
if (dup2(ctl_pipe[0], FORKSRV_FD) < 0) { PFATAL("dup2() failed"); }
if (dup2(st_pipe[1], FORKSRV_FD + 1) < 0) { PFATAL("dup2() failed"); }
close(ctl_pipe[0]);
close(ctl_pipe[1]);
close(st_pipe[0]);
close(st_pipe[1]);
close(fsrv->out_dir_fd);
close(fsrv->dev_null_fd);
close(fsrv->dev_urandom_fd);
if (fsrv->plot_file != NULL) {
fclose(fsrv->plot_file);
fsrv->plot_file = NULL;
}
if (!getenv("LD_BIND_LAZY")) { setenv("LD_BIND_NOW", "1", 1); }
set_sanitizer_defaults();
fsrv->init_child_func(fsrv, argv);
...
Дочерний процесс выполняет совсем немного действий.
Сначала, если для ввода используется стандартный поток ввода (stdin
), его заменяют файловым дескриптором, который указывает на out_fd
, настроенный ранее.
Затем с помощью dup2
настраивают поток чтения и поток управления:
if (dup2(ctl_pipe[0], FORKSRV_FD) < 0) { PFATAL("dup2() failed"); }
if (dup2(st_pipe[1], FORKSRV_FD + 1) < 0) { PFATAL("dup2() failed"); }
Этот код перенаправляет потоки ввода и вывода дочернего процесса через каналы. Он настраивает, чтобы дочерний процесс читал из управляющего канала и писал в канал статуса, используя соответствующие файловые дескрипторы. Если процесс перенаправления не удается, программа завершится с ошибкой.
Следующий участок кода, состоящий из функций close
выполняет закрытие файловых дескрипторов. Он освобождает ресурсы, которые были открыты для работы с каналами и другими файлами.
Далее выполняем fsrv->init_child_func(fsrv, argv)
для запуска дочернего процесса.
Давайте вернемся к родительскому процессу. Поскольку его код гораздо сложнее, то начнем его рассматривать сразу после создания fork'a
:
close(ctl_pipe[0]);
close(st_pipe[1]);
fsrv->fsrv_ctl_fd = ctl_pipe[1];
fsrv->fsrv_st_fd = st_pipe[0];
/* Wait for the fork server to come up, but don't wait too long. */
rlen = 0;
if (fsrv->init_tmout) {
...
} else {
rlen = read(fsrv->fsrv_st_fd, &status, 4);
}
Сначала мы закрываем управляющий канал, открытый на чтение (control pipe
) и канал статуса (status pipe
), открытый на запись, так как эти каналы используются исключительно для запуска forkserver
.
После этого мы читаем переменную состояния (status variable
), отправленную forkserver
. Эта переменная состояния содержит информацию о целевом бинарном файле. Как вы могли заметить, эта переменная была отправлена в forkserver
на предыдущем этапе.
С помощью этой переменной состояния forkserver
инициализирует и включает необходимые параметры. Это можно увидеть в двух фрагментах кода ниже:
if ((status & FS_OPT_SHDMEM_FUZZ) == FS_OPT_SHDMEM_FUZZ) {
if (fsrv->support_shmem_fuzz) {
fsrv->use_shmem_fuzz = 1;
if (!be_quiet) { ACTF("Using SHARED MEMORY FUZZING feature."); }
if ((status & FS_OPT_AUTODICT) == 0 || ignore_autodict) {
u32 send_status = (FS_OPT_ENABLED | FS_OPT_SHDMEM_FUZZ);
if (write(fsrv->fsrv_ctl_fd, &send_status, 4) != 4) {
FATAL("Writing to forkserver failed.");
}
}
} else {
FATAL(
"Target requested sharedmem fuzzing, but we failed to enable "
"it.");
}
}
В приведенном фрагменте кода выше мы включаем функцию фаззинга через общую память (shared memory fuzzing
). Если fsrv->support_shmem_fuzz != 1
, то это означает, что поддержка фаззинга через общую память невозможна. В этом случае выводится сообщение об ошибке, и процесс завершается.
Если же поддержка включена, то мы устанавливаем fsrv->use_shmem_fuzz = 1
и сообщаем дочернему процессу, что общая память готова к использованию.
И тут
if ((status & FS_OPT_MAPSIZE) == FS_OPT_MAPSIZE) {
u32 tmp_map_size = FS_OPT_GET_MAPSIZE(status);
if (!fsrv->map_size) { fsrv->map_size = MAP_SIZE; }
fsrv->real_map_size = tmp_map_size;
if (tmp_map_size % 64) {
tmp_map_size = (((tmp_map_size + 63) >> 6) << 6);
}
if (!be_quiet) { ACTF("Target map size: %u", fsrv->real_map_size); }
if (tmp_map_size > fsrv->map_size) {
FATAL(
"Target's coverage map size of %u is larger than the one this "
"afl++ is set with (%u). Either set AFL_MAP_SIZE=%u and restart"
" afl-fuzz, or change MAP_SIZE_POW2 in config.h and recompile "
"afl-fuzz",
tmp_map_size, fsrv->map_size, tmp_map_size);
}
fsrv->map_size = tmp_map_size;
}
В приведенном фрагменте кода мы занимаемся настройкой размера карты покрытия. Сначала мы получаем размер карты из переменной status
и сохраняем его в tmp_map_size
. Затем мы сравниваем этот размер с заранее выделенным размером карты (fsrv->map_size
). Если полученный размер карты больше выделенного размера, программа завершится с ошибкой, так как мы не можем обработать карту такого размера.
После этого размер карты обновляется в fsrv->map_size
, если все условия удовлетворены.
На этом этапе forkserver полностью настроен, и мы готовы перейти к рассмотрению afl-fuzz
.
Конец
В следующей части продолжим рассматривать работуafl-fuzz.