Быстрый security-oriented fuzzing c AFL
Многие слышали, а некоторые успешно применяют в своих процессах разработки такую вещь, как статический анализ кода — эффективный, относительно быстрый и зачастую удобный способ контроля качества кода.
Для тех, кто уже использует статический анализ кода, на этапе тестирования может быть интересно также попробовать динамический анализ. Об отличиях данных методик написано достаточно, напомню лишь, что статический анализ делается без выполнения кода (например, на этапе компиляции), а динамический соответственно, — в процессе выполнения. При анализе компилируемого кода с точки зрения безопасности, под динамическим анализом часто подразумевают именно фаззинг. Преимуществом фаззинга является практически полное отсутствие ложных срабатываний, что довольно часто встречается при использовании статических анализаторов.
«Фаззинг — методика тестирования, при которой на вход программы подаются невалидные, непредусмотренные или случайные данные.» © Habrahabr
В последнее время большую известность за свою эффективность получил фаззер за авторством Michal Zalewski — American Fuzzy Lop.
Основным его отличием является инструментация кода на этапе компиляции, производительность и ориентированность на практическое применение. AFL не использует SMT solver’ов, а значит должен быть менее требователен к ресурсам, работать быстрее, пусть и не всегда эффективнее.
Сегодня расскажу как именно можно применять данный инструмент, а заодно проведу небольшой эксперимент, чтобы сравнить результат его работы с результатами нескольких инструментов статического анализа.
Итак, чтобы начать пользоваться фаззером, нужно понять, работает ли он на практике, а также что именно и как мы будем фазить.
Для проверки я взял заведомо уязвимую версию популярной библиотеки libcurl — 7.34.0.
Данная версия содержит уязвимость в функции sanitize_cookie_path () описанную в CVE-2015–3145.
Функция некорректно проверяет входные данные, и передав в нее путь состоящий из двойной кавычки либо нуль-байта, 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 ответы веб-сервера.
В случае с известной уязвимостью у нас есть два пути:
1. Фаззить отдельные функции, которые могут показаться подозрительными.
Для этого необходимо написать к ним минимальную обертку:
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:
http://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 для тестирования приложений.
Применяя фаззинг в процессе тестирования важно понимать, что любой, даже самый быстрый и эффективный фаззер с хорошим покрытием не заменяет анализатор кода, а лишь дополняет его.
Ссылки по теме и источники: