Как стать автором
Поиск
Написать публикацию
Обновить

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

Уровень сложностиСредний
Время на прочтение10 мин
Количество просмотров310

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

Оглавление:

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

Теги:
Хабы:
0
Комментарии4

Публикации

Ближайшие события