Быстрый 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 @@
Параметр
Практически сразу после запуска 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, подменяющего функцию
Смотрим параметры curl:
• 127.0.0.1 — URL-соединение. К нему будем эмулировать, поэтому важно указать не домен, а IP-адрес (мы подменили функции, резолв не пройдёт);
• max-time — задаёт наибольшее время выполнения curl, которое равно одной секунде (меньше ставить мы не можем). Задаём, так как ни AFL, ни curl не закрывают дескриптор;
•
В результате через пару минут 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 для тестирования приложений. Однако применяя технологию фаззинга, нужно помнить, что любой, пусть даже наиболее эффективный и быстрый фаззер с хорошим покрытием не заменяет анализатор кода, а только дополняет его.