Быстрый security-oriented fuzzing c AFL. Часть 2

В предыдущей части мы начали разговор об American Fuzzy Lop и провели тесты с помощью статических анализаторов, которые не обнаружили проблему. Теперь давайте посмотрим, к какому результату приведёт процесс фаззинга.

В первую очередь качаем и собираем 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, либо использует временный файл. В том случае, если процесс упадёт, American Fuzzy Lop это увидит, поэтому запишет данные в директорию crashes. Важный момент для успешного фаззинга — сборка приложения с помощью Address Sanitizer'а. Из-за этого приложение гарантированно упадёт даже в том случае, если произойдёт перезапись хотя бы одного байта динамической памяти. Про ASAN писать не будем, он описан множество раз, поэтому успешно и давно применяется.

Идём дальше. Для генерации тестов нужен набор тестовых данных, обрабатывающих приложение (т. н. corpus). В случае с curl речь идёт о валидных HTTP-ответах веб-сервера.

В случае с известной уязвимостью у нас существуют 2 пути: 1) фаззить отдельные функции, которые кажутся подозрительными; 2) фаззить всё приложение полностью.

Фаззинг отдельных функций

В первом случае пишем минимальную обёртку к приложению:

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).

Теперь собираем curl и preeny:

$ 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, которое равно одной секунде (меньше ставить мы не можем). Задаём, так как ни AFL, ни curl не закрывают дескриптор; • --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

Итак, мы ознакомились, как используют фаззер American Fuzzy Lop для тестирования приложений. Однако применяя технологию фаззинга, нужно помнить, что любой, пусть даже наиболее эффективный и быстрый фаззер с хорошим покрытием не заменяет анализатор кода, а только дополняет его.