Быстрый security-oriented fuzzing c AFL

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

    «Фаззинг – методика тестирования, при которой на вход программы подаются невалидные, непредусмотренные или случайные данные.» © Habrahabr




    В последнее время большую известность за свою эффективность получил фаззер за авторством Michal ZalewskiAmerican Fuzzy Lop.
    Основным его отличием является инструментация кода на этапе компиляции, производительность и ориентированность на практическое применение. AFL не использует SMT solver'ов, а значит должен быть менее требователен к ресурсам, работать быстрее, пусть и не всегда эффективнее.
    Сегодня расскажу как именно можно применять данный инструмент, а заодно проведу небольшой эксперимент, чтобы сравнить результат его работы с результатами нескольких инструментов статического анализа.

    Итак, чтобы начать пользоваться фаззером, нужно понять, работает ли он на практике, а также что именно и как мы будем фазить.
    Для проверки я взял заведомо уязвимую версию популярной библиотеки libcurl — 7.34.0.
    Данная версия содержит уязвимость в функции sanitize_cookie_path() описанную в CVE-2015-3145.

    bleedingeyes

    Функция некорректно обрабатывает входные данные, и передав в нее путь состоящий из двойной кавычки либо нуль-байта, libcurl назначит нуль-байт по отрицательному указателю массива new_path, и испортит память на куче.

    Сначала проверим как на эту уязвимость реагируют статические анализаторы.

    Под рукой были Coverity, PVS Studio и Clang Static Analyzer.

    В clang это можно сделать так:

    $ cd curl
    $ mkdir build-clang
    $ cd build-clang
    $ cmake -DCMAKE_C_COMPILER=/path/to/clang/ccc-analyzer -DCMAKE_CXX_COMPILER=/path/to/clang/ccc-analyzer -DCMAKE_BUILD_TYPE=release ../
    $ scan-build -o html make
    


    после чего в директории html получим результат анализа:



    Coverity перехватывает вызовы компилятора, анализатор запускался со следующими параметрами:

    $ cov-analyze --dir cov --all --security --enable-constraint-fpp --enable-single-virtual --enable-fnptr --enable-callgraph-metrics -j 2 --inherit-taint-from-unions --override-worker-limit
    




    PVS Studio требует Windows, а в trial версии не показывает имена проблемных файлов, однако мы уже знаем строку и тип ошибки, поэтому для бинарной оценки этого будет достаточно. PVS запускался в режиме монитора, а для простоты сборки использовался скрипт build-libcurl-windows.


    (вывод PVS Studio приведен не весь)

    Ни один из статических анализаторов проблему не обнаружил.

    Теперь разберем то, как именно запустить процесс фаззинга.

    Сначала скачаем и соберем AFL:

    $ wget http://lcamtuf.coredump.cx/afl/releases/afl-latest.tgz
    $ tar xvfz afl-latest.tgz
    $ cd afl-1.83b
    $ make
    $ cd llvm_mode
    $ make
    


    Обычно фаззер стартует приложение в новом процессе, затем подает ему на вход тестовые данные в STDIN или используя временный файл, если процесс упадет — AFL это заметит и запишет поданные данные в директорию crashes. Важным моментом для успешного фаззинга будет сборка приложения с использованием Address Sanitizer'а — так приложение гарантированно упадет даже при перезаписи одного байта динамической памяти. Я не буду ничего писать про ASAN, т.к. он описан много раз, давно и успешно применяется.

    Для генерации тестов необходим т.н. corpus — набор тестовых данных, которые обрабатывает приложение, в случае с curl это валидные HTTP ответы веб-сервера.

    В случае с известной уязвимостью у нас есть два пути — фаззить отдельные функции, которые могут показаться подозрительными и фаззить всё приложение целиком.
    В первом случае необходимо написать к приложению минимальную обертку:

    int main(int argc, char **argv)
    {
        unsigned char         buf[2048];
        char *res = NULL;
    
        assert(argc == 2);
    
        FILE *f = fopen(argv[1], "rb");
        assert(f);
    
        size_t len = fread(buf, 1, sizeof(buf), f);
        buf[len] = 0x00;
        if (len == 0 || strlen(buf) == 0) {
            return 0;
        }
    
        printf("read = %zu\n", len);
        printf("in = %s\n", buf);
    
        /* call the code which smell */
        res = sanitize_cookie_path(buf);
    
        if (res) {
            printf("res = %s\n", res);
            free(res);
        }
        return 0;
    }
    


    Далее обертку нужно инструментировать, для чего соберем ее под AFL

    $ afl-clang-fast -g -fsanitize=address path_san.c -o path_san
    


    В директорию inputs достаточно положить один файл с подходящим URI, например "/xxx/".
    И запустим AFL:

    $ AFL_USE_ASAN=1 /path/to/afl/afl-fuzz -m none -i inputs -o out ./path_san @@
    


    параметр -m none отключит лимит памяти, а @@ будет заменяться именем временного файла при фаззинге, если не задать этот параметр — тестовые данные будут подаваться в STDIN. Почти сразу после запуска AFL обнаружит crash и сгенерирует тестовый вход в директории out/crashes.

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

    Как мы знаем — curl взаимодействует с сервером через сокеты, фаззер же этого делать не умеет, а значит нам нужно научиться передавать данные от фаззера в curl.
    Для этого подменим функцию connect так, чтобы вместо создания нового соединения результат connect возвращал дескриптор stdin.
    Сделать это можно через LD_PRELOAD своей динамической библиотеки, которую, к счастью, писать не обязательно — можно воспользоваться готовой (preeny).

    Соберем preeny и curl:

    $ git clone https://github.com/zardus/preeny 
    $ cd preeny && make
    ...
    $ cd curl
    $ mkdir build
    $ export CMAKE_C_FLAGS="-g -fsanitize=address"
    $ cmake -DCMAKE_C_COMPILER=/path/to/afl-clang-fast -DCMAKE_CXX_COMPILER=/path/to/afl-clang-fast -DCMAKE_BUILD_TYPE=release ../
    $ make
    


    Кладем собранные бинари в одну директорию, создаем рядом директорию inputs, а в ней файл с HTTP ответом сервера (для увеличения покрытия лучше создать несколько).

    Например:

    HTTP/1.1 200 OK
    Content-Type: text/html
    Content-Length: 1
    Connection: close
    Set-Cookie: xx=xxx; path=xx; domain=xxx.com; httponly; secure;
    
    1
    


    После этого возвращаемся в директорию с приложением и запускаем AFL:

    $ LD_PRELOAD="/path/to/preeny/x86_64-linux-gnu/desock.so" /path/to/afl/afl-fuzz -m none -i inputs -o out ./curl http://127.0.0.1/ --max-time 1 --cookie-jar /dev/null
    


    LD_PRELOAD здесь задает путь до SO, который подменит функцию connect.

    Параметры curl:
    • 127.0.0.1 — URL соединение к которому мы будем эмулировать, здесь важно указать IP адрес а не домен (помните — мы ведь подменили функции и резолв не пройдет, да и так быстрее)
    • max-time задает максимальное время выполнения curl равное одной секунде (меньше ставить нельзя), задаем т.к. ни curl ни AFL не закрывают дескриптор
    • важно использовать параметр --cookie-jar, т.к. curl вызовет уязвимую функцию только в случае использования cookies




    Через несколько минут AFL найдет первые тестовые данные которые приводят к падению приложения.

    Теперь можно удостовериться, что приложение действительно падает на этих входных данных.

    $ LD_PRELOAD="/path/to/preeny/x86_64-linux-gnu/desock.so" ./curl http://127.0.0.1/ --max-time 1 --cookie-jar /dev/null < out/crashes/id:000010,sig:06,src:000000,op:havoc,rep:2
    




    Так мы ознакомились с тем, как можно использовать AFL для тестирования приложений.
    Применяя фаззинг в процессе тестирования важно понимать, что любой, даже самый быстрый и эффективный фаззер с хорошим покрытием не заменяет анализатор кода, а лишь дополняет его.

    Ссылки по теме и источники:

    • +14
    • 9.9k
    • 7
    Share post

    Similar posts

    Comments 7

      0
      max-time задает максимальное время выполнения curl равное одной секунде (меньше ставить нельзя), задаем т.к. ни curl ни AFL не закрывают дескриптор


      то есть, на каждый вариант входных данных всегда будет тратиться 1 секунда?

      Какой-нибудь много- поточный/процессорный режим есть у AFL?

      А вообще круто, что рассмотренные статические анализаторы не обнаружили баг, в отличие от фаззера :)
        +1
        Да ;) На самом деле можно научить AFL делать close, можно сделать как-нибудь умнее завязавшись на persistent mode AFL, цель этой статьи — howto того как можно попробовать использовать фаззер.

        Многопоточного режима нет, но можно делать распределенный фаззинг + есть persistent режим, когда стартует один процесс, но тогда нужно в обертке самому чистить все используемые переменные для каждого входа.
        0
        Спасибо за статью. По статьям постам в блоге автора инструмента никак не мог понять, что на вход нужно скармливать. Теперь попробую использовать.
          0
          А что у вас с терминалом? На скриншоте окна afl-fuzz не видно тёмно-серых подписей к данным и линий границ полей.
            0
            Это из скрина. Спасибо, как-то не обратил внимание, позже заменю на нормальный.
              0
              ТАК И НЕ ЗАМЕНИЛ!
                0
                НЕ ЗАМЕНИЛ!

            Only users with full accounts can post comments. Log in, please.