Особенности подачи входных данных при фаззинге в режиме Persistent Mode на примере Libfuzzer + CURL

от автора

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

Оглавление:

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

  1. argv[0] — имя программы

  2. Каждая строка (argv[i] - параметр) должна быть корректно нуль-терминирована
    То есть содержимое из фаззера нужно нарезать на строки и в каждую добавить \0.

  3. argv[argc] == NULL . Не учитывается при подсчете argc.

    Для корректного парсинга аргументов командной строки необходимо воссоздать структуру массива argv, с верным указанием количества аргументов argc.

Структура массива argv

Структура массива argv

Перед созданием обёртки необходимо выяснить какие инициализирующие функции выполняются в 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();   }

Шаги по созданию фаззинг-обертки:

  1. В качестве целевой функции выберем operate. В файле tool_operate.h смотрим необходимые заголовочные файлы.

  2. Написание функцию ASCII-фильтрации входных символов (можно пропусть эту часть, запуская libfuzzer с флагом -only-ascii=1).

  3. Вызов memset (global) — для избежания накопления ошибок между итерациями фаззера

  4. Воссоздание структуры 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 запусков в секунду.

Итоги использования seccomp

Итоги использования seccomp

Эмуляция ввода входных данных (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; }

Как видим фаззер успешно проходит участок кода с вводом пароля.

Фаззинг после замены stdin

Фаззинг после замены 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *