Дисклеймер: Автор не претендует на описание самых эффективных или универсальных методов фаззинга. Автор также не исключает существование других методов решения описанных ниже проблем. Материал носит ознакомительный характер и ориентирован на специалистов, уже имеющих опыт работы с фаззингом, но сталкивающихся с трудностями при тестировании нестандартных функций или нестандартного окружения. Описанные подходы могут потребовать доработки и адаптации под конкретные задачи и не гарантируют полноту покрытия или оптимальные результаты.
Оглавление:
Фаззинг — один из самых эффективных инструментов для поиска ошибок и уязвимостей. Однако при попытке применить готовые движки вроде LibFuzzer или AFL++ к реальным утилитам быстро выясняется, что «из коробки» всё работает далеко не всегда.
Особенно это заметно при использовании persistent mode — режима, при котором тестируемая функция многократно вызывается в одном процессе. Такой подход даёт колоссальный прирост производительности, но он накладывает ограничение: необходимо уметь подавать входные данные туда, где программа их реально ожидает.
В частности, речь идёт о случаях, когда функции принимают данные:
-
через аргументы командной строки
-
из стандартного потока ввода
-
из файловых дескрипторов
Кроме того, остаются нерешёнными сценарии, общие и для LibFuzzer, и для AFL++:
-
ограничение исходящих сетевых сообщений.
Эта статья как раз посвящена тому, как организовать фаззинг в persistent mode для функций с «нестандартным» вводом.
В этой статье я покажу на примере curl и libfuzzer, как решать эти задачи: подменять argv, использовать fmemopen для имитации файлов и перехватывать сетевые вызовы. Всё это позволяет гибко адаптировать фаззер к практическому фаззингу сложных приложений, которые изначально к этому не приспособлены.
Подготовка Curl
Перед созданием обёрток выполним сборку curl с флагами Address Sanitizer’а и libfuzzer’а:
./buildconf; CC=clang CFLAGS="-fsanitize=fuzzer-no-link,address -g" LDFLAGS="-fsanitize=fuzzer-no-link,address -g" ./configure --with-openssl; make;
Фаззинг функций, обрабатывающих аргументы командной строки:
Когда речь идёт о фаззинге функций, которые обрабатывают аргументы командной строки, первое, что нужно понять — как именно приложение работает с массивом argv.
В классическом int main(int argc, char **argv) argv представляет собой набор строк, а argc — количество аргументов.
Структура массива argv представляет собой:
-
argv[0]— имя программы -
Каждая строка (
argv[i] - параметр) должна быть корректно нуль-терминирована
То есть содержимое из фаззера нужно нарезать на строки и в каждую добавить\0. -
argv[argc] == NULL .Не учитывается при подсчетеargc.Для корректного парсинга аргументов командной строки необходимо воссоздать структуру массива argv, с верным указанием количества аргументов argc.
Перед созданием обёртки необходимо выяснить какие инициализирующие функции выполняются в main функции curl — globalconf_init() и globalconf_free.
curl/src/too_main.c:
int main(int argc, char *argv[]) #endif { CURLcode result = CURLE_OK; tool_init_stderr(); ... if(main_checkfds()) { errorf("out of file descriptors"); return CURLE_FAILED_INIT; } ... /* Initialize memory tracking */ memory_tracking_init(); /* Initialize the curl library - do not call any libcurl functions before this point */ result = globalconf_init(); if(!result) { /* Start our curl operation */ result = operate(argc, argv); /* Perform the main cleanup */ globalconf_free(); }
Шаги по созданию фаззинг-обертки:
-
В качестве целевой функции выберем operate. В файле tool_operate.h смотрим необходимые заголовочные файлы.
-
Написание функцию ASCII-фильтрации входных символов (можно пропусть эту часть, запуская libfuzzer с флагом -only-ascii=1).
-
Вызов memset (global) — для избежания накопления ошибок между итерациями фаззера
-
Воссоздание структуры argv и корректное заполнение argc
Итоговый код обертки:
#include <stdint.h> #include <stddef.h> #include <stdio.h> #include <ctype.h> #include "src/tool_setup.h" #include "src/tool_getparam.h" #include "src/tool_operate.h" #include "src/config2setopts.h" #include <curl/curl.h> #include <stdint.h> #include <string.h> #define MAX_ASCII_CHARS 4096 #define MAX_ARGS 32 #define MAX_ARG_LEN 256 // Шаг 2 - Simple ASCII filter static int is_ascii_char(uint8_t c) { return (c >= 0x20 && c <= 0x7E); // printable ASCII } int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { if (size == 0) { return 0; } CURLcode result = CURLE_OK; // Buffer to store only printable ASCII input char ascii_data[MAX_ASCII_CHARS + 1] = {0}; size_t ascii_pos = 0; for (size_t i = 0; i < size && ascii_pos < MAX_ASCII_CHARS - 1; i++) { if (is_ascii_char(data[i])) { ascii_data[ascii_pos++] = (char)data[i]; } } if (ascii_pos == 0) { return 0; } // Шаг 4 - Воссоздание структуры argv, argc char *fuzz_argv[MAX_ARGS + 2] = {0}; int fuzz_argc = 1; fuzz_argv[0] = "argv-fuzz"; char *token = strtok(ascii_data, " "); while (token && fuzz_argc < MAX_ARGS + 1) { fuzz_argv[fuzz_argc++] = token; token = strtok(NULL, " "); } fuzz_argv[fuzz_argc] = NULL; if (fuzz_argc < 4) { return 0; } // Шаг 3 - Избежание накопления ошибок memset(&global, 0, sizeof(global)); tool_init_stderr(); result = globalconf_init(); if (!result) { // Шаг 3 - Избежание накопления ошибок memset(&global->state, 0, sizeof(global->state)); operate(fuzz_argc, fuzz_argv); } globalconf_free(); // Шаг 3 - Избежание накопления ошибок memset(&global, 0, sizeof(global)); return 0; }
Для избежания проблем линковки необходимо выполнить архивацию объектных файлов к каталоге ./src без файлаlibcurltool_la-tool_help.o :ar -rcT static.a $(ls *.o | grep -v libcurltool_la-tool_help.o)
Выполним сборку обёртки:
clang -fsanitize="fuzzer,address" -I ../include/ -I .. -I ../lib arg_fuzzing.c -DSIZEOF_CURL_OFF_T=8 -DHAVE_STRUCT_TIMEVAL -Dsread -Dswrite -o argv_fuzzer ../src/static.a ../lib/.libs/libcurl.a -lssl -lcrypto -lz -lpsl -lzstd
Получаем результаты с низкой скоростью тестирования:
Так как наш исполняемый файл выполняет отправку сетевых сообщений, то необходимо либо запустить сервер и выполнять переадресацию трафика с помощью iptables (так сделали ребята из trailofbits), либо выполнить изоляцию процесса от отправки исходящего трафика.
Изоляция процесса от отправки исходящего трафика
Мы пойдем своим путем и выполним изоляцию при помощи библиотеки механизма seccomp.
seccomp (Secure Computing Mode) — это механизм безопасности в Linux, встроенный в ядро, который позволяет процессу ограничить набор системных вызовов (syscalls), которые он может выполнять.
-
Включается с помощью
prctl()или через seccomp-bpf фильтры. -
Обычно используется в контейнерах (Docker, LXC), в браузерах (Chrome), и в фаззинге (чтобы «глушить» сеть или файл).
Как работает
Программа, включившая seccomp, передаёт в ядро фильтр, написанный на BPF (Berkeley Packet Filter).
Этот фильтр проверяет каждый системный вызов: разрешить, отказать, завершить процесс или вернуть ошибку.
Ограничение сетевых сообщений
Для отправки/приёма сетевых данных приложение вызывает системные вызовы:
-
socket() -
connect() -
send(),sendto(),sendmsg() -
write()(если это сокет) -
recv(),recvfrom(),recvmsg()
На основе информации выше напишем правила блокировки системных вызовов:
#include <seccomp.h> void mute_network() { scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW); if (!ctx) return; // Deny network-related syscalls seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(socket), 0); seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(connect), 0); seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(sendto), 0); seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(sendmsg), 0); seccomp_load(ctx); seccomp_release(ctx); } int LLVMFuzzerInitialize(int *argc, char ***argv) { mute_network(); // your helper to stub network return 0; }
В итоге получаем ускорение фаззинг-тестирование до ~2000 запусков в секунду.
Эмуляция ввода входных данных (stdin)
После использования seccomp при фаззинге появляется проблема ввода пароля прокси-сервера, что приводит к остановке фаззинга.
stdin — это стандартный поток ввода (standard input) в языке C. Это глобальный указатель на структуру FILE, который открывается автоматически при старте программы.
stdin используется в функциях стандартной библиотеки, которые читают данные из стандартного потока ввода:
Построчно:
-
fgets(buf, size, stdin);— читает строку. -
getline(&line, &len, stdin);— читает строку динамически.
Посимвольно:
-
getc(stdin);илиfgetc(stdin); -
ungetc(ch, stdin);
Форматированное чтение:
-
scanf("%d", &x);— читает изstdin. -
fscanf(stdin, "%s", str);
Блоками:
-
fread(buf, 1, size, stdin);— читает блок байтов.
Заменив стандартный поток ввода мы можем продложить фаззинг-тестирование при выполнении функций, перечисленных выше.
Для начала изучим исходный код функции, обрабатывающей пароль прокси-сервера:
char *getpass_r(const char *prompt, /* prompt to display */ char *password, /* buffer to store password in */ size_t buflen) /* size of buffer to store password in */ { ssize_t nread; bool disabled; int fd = open("/dev/tty", O_RDONLY); if(fd == -1) fd = STDIN_FILENO;; /* use stdin if the tty could not be used */ disabled = ttyecho(FALSE, fd); /* disable terminal echo */ fputs(prompt, tool_stderr); nread = read(fd, password, buflen); if(nread > 0) password[--nread] = '\0'; /* null-terminate where enter is stored */ else password[0] = '\0'; /* got nothing */ if(disabled) { /* if echo actually was disabled, add a newline */ fputs("\n", tool_stderr); (void)ttyecho(TRUE, fd); /* enable echo */ } if(STDIN_FILENO != fd) close(fd); return password; /* return pointer to buffer */ }
Как видим на строке 7 для считывания пароля используется tty. Перепишем строку 7 int fd = stdin,удалив строки 8 и 9.
После изменения исходного кода curl необходимо выполнить:
make cd ./src && ar -rcT static.a $(ls *.o | grep -v libcurltool_la-tool_help.o)
Теперь мы можем предоставлять пароль прокси-сервера, создавая файловый дескриптор в оперативной памяти — fmemopen и временно заменяя указатель stdin.
if (!result) { FILE *old_stdin = stdin; FILE *memfile = fmemopen(data, size, "r"); if (!memfile) { perror("fmemopen"); return 1; } // Redirect stdin stdin = memfile; memset(&global->state, 0, sizeof(global->state)); operate(fuzz_argc, fuzz_argv); fclose(memfile); // Restore original stdin stdin = old_stdin; }
Как видим фаззер успешно проходит участок кода с вводом пароля.
По результам фаззинга удалось найти use-afte-free, которая подтвержденна разработчиками:
https://github.com/curl/curl/issues/18352
Итоговый код обёртки:
#include <ctype.h> #include <curl/curl.h> #include <errno.h> #include <seccomp.h> #include <stdarg.h> #include <stddef.h> #include <stdint.h> #include <stdio.h> #include <string.h> #include "src/config2setopts.h" #include "src/tool_getparam.h" #include "src/tool_operate.h" #include "src/tool_setup.h" #define MAX_ASCII_CHARS 4096 #define MAX_ARGS 32 #define MAX_ARG_LEN 256 // Simple ASCII filter static int is_ascii_char(uint8_t c) { return (c >= 0x20 && c <= 0x7E); // printable ASCII } void mute_network() { scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW); if (!ctx) return; // Deny network-related syscalls seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(socket), 0); seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(connect), 0); seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(sendto), 0); seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), SCMP_SYS(sendmsg), 0); seccomp_load(ctx); seccomp_release(ctx); } int LLVMFuzzerInitialize(int *argc, char ***argv) { mute_network(); // your helper to stub network return 0; } int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { if (size == 0) { return 0; } CURLcode result = CURLE_OK; // Buffer to store only printable ASCII input char ascii_data[MAX_ASCII_CHARS + 1] = {0}; size_t ascii_pos = 0; for (size_t i = 0; i < size && ascii_pos < MAX_ASCII_CHARS - 1; i++) { if (is_ascii_char(data[i])) { ascii_data[ascii_pos++] = (char)data[i]; } } if (ascii_pos == 0) { return 0; } char *fuzz_argv[MAX_ARGS + 4] = {0}; int fuzz_argc = 1; fuzz_argv[0] = "argv-fuzz"; // Add --max-time 0.2 to enforce 100 ms timeout fuzz_argv[fuzz_argc++] = "--max-time"; fuzz_argv[fuzz_argc++] = "0.1"; char *token = strtok(ascii_data, " "); while (token && fuzz_argc < MAX_ARGS + 1) { fuzz_argv[fuzz_argc++] = token; token = strtok(NULL, " "); } fuzz_argv[fuzz_argc] = NULL; if (fuzz_argc < 4) { return 0; } memset(&global, 0, sizeof(global)); tool_init_stderr(); result = globalconf_init(); if (!result) { FILE *old_stdin = stdin; FILE *memfile = fmemopen(data, size, "r"); if (!memfile) { perror("fmemopen"); return 1; } // Redirect stdin stdin = memfile; memset(&global->state, 0, sizeof(global->state)); operate(fuzz_argc, fuzz_argv); fclose(memfile); // Restore original stdin stdin = old_stdin; } globalconf_free(); memset(&global, 0, sizeof(global)); return 0
Фаззинг функций с аргументом filename/fd
В качестве целевой функции выберем — parseconfig(filename);
Для эмуляции файлового дескриптора воспользуемся функцией memfd_create:
memfd_create(«fuzz», 0);
-
memfd_create— Linux-системный вызов, который создаёт анонимный файл в памяти (tmpfs), он существует только в рамках текущего процесса (не попадает на диск). -
Возвращает файловый дескриптор
fd. -
Первый аргумент
"fuzz"— это имя, которое будет видно в/proc/<pid>/fdinfo/(для отладки). -
Второй аргумент (
flags = 0) — можно задать флаги (напримерMFD_CLOEXEC).
📌 То есть мы получаем временный «файл», но без создания реального файла на диске.
Если вам необходимо использовать файловый дескриптор в качества аргумента функции, то достаточно выполнить:
int fd = memfd_create("fuzz", 0); write(fd, data, size); lseek(fd, 0, SEEK_SET);
Получив файловый дескриптор, нам также необходимо также подготовить путь к файлу:
-
В Linux для каждого открытого файла есть «ссылка» в каталоге
/proc/self/fd/. -
self= текущий процесс,fd= файловый дескриптор. -
То есть
/proc/self/fd/3указывает на тот файл, который у вас в дескрипторе 3. -
snprintfформирует строку пути к символической ссылке, которая ведёт к нашемуmemfd.
char filename[64]; snprintf(filename, sizeof(filename), "/proc/self/fd/%d", fd);
В результате можно передать
filenameв любую функцию, которая ожидает путь файла . Теперь функция будет читать данные напрямую из созданного в памяти файла.
#include <stdint.h> #include <stddef.h> #include <stdio.h> #include "src/tool_setup.h" #include "src/tool_getparam.h" #include "src/tool_operate.h" #include "src/config2setopts.h" #include <curl/curl.h> #include <string.h> #define MAX_ASCII_CHARS 4096 #define MAX_ARGS 32 #define MAX_ARG_LEN 256 int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { if (size == 0) { return 0; } CURLcode result = CURLE_OK; memset(&global, 0, sizeof(global)); tool_init_stderr(); result = globalconf_init(); int fd = memfd_create("fuzz", 0); write(fd, data, size); lseek(fd, 0, SEEK_SET); char filename[64]; snprintf(filename, sizeof(filename), "/proc/self/fd/%d", fd); if (!result) { parseconfig(filename); } globalconf_free(); memset(&global, 0, sizeof(global)); close(fd); return 0; }
Команда сборки обертки:
clang -fsanitize="fuzzer,address" -I ../include/ -I .. -I ../lib config_fuzzing.c -DSIZEOF_CURL_OFF_T=8 -DHAVE_STRUCT_TIMEVAL -Dsread -Dswrite -o config_fuzzer ../src/static.a ../lib/.libs/libcurl.a -lssl -lcrypto -lz -lpsl -lzstd
За счёт использования memfd_create нам удалось достичь скорость тестирования — ~17000 запусков в секунду
Контакты автора статьи:
Если вам необходима консультация, либо помощь при анализе безопасности ваших/open-source продуктов — вы можете связаться с автором статьи (telegram — @fugaru)
ссылка на оригинал статьи https://habr.com/ru/articles/940946/
Добавить комментарий