Дисклеймер: Автор не претендует на описание самых эффективных или универсальных методов фаззинга. Автор также не исключает существование других методов решения описанных ниже проблем. Материал носит ознакомительный характер и ориентирован на специалистов, уже имеющих опыт работы с фаззингом, но сталкивающихся с трудностями при тестировании нестандартных функций или нестандартного окружения. Описанные подходы могут потребовать доработки и адаптации под конкретные задачи и не гарантируют полноту покрытия или оптимальные результаты.
Оглавление:
Фаззинг — один из самых эффективных инструментов для поиска ошибок и уязвимостей. Однако при попытке применить готовые движки вроде 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)