Фаззинг, как ты, наверное, знаешь, — это техника, которая позволяет автоматизировать поиск уязвимостей в софте. Бывает фаззинг методом «черного ящика», когда у нас нет исходников программы, а бывает основанный на покрытии — то есть применяемый к исходникам. В этой статье сосредоточимся на втором виде и на примере AFL++ разберем, какую роль здесь играют санитайзеры и как их применять.INFO О фаззинге методом черного ящика читай в статье «WinAFL на практике. Учимся работать фаззером и искать дыры в софте». Суть фаззинга заключается в том, чтобы автоматически подавать программе неправильные или испорченные данные и отлавливать потенциальные уязвимости. На свете существует множество фаззеров: AFL++, libfuzz, WinAFL, CI Fuzz, PeachTech Peach Fuzzer, FuzzDB, go-fuzz и прочие, в том числе самописные. Фаззеры применимы не только для программ, но и для сайтов. Например, wfuzz — это веб‑фаззер, который нужен, чтобы вводить любые данные в любое поле HTTP-запроса. В этой статье идет речь о фаззинге программ. Нашим инструментом будет AFL++ — фаззер, ориентированный на покрытие. Это значит, что он собирает информацию о покрытии для каждого измененного входа, чтобы обнаружить новые пути выполнения и потенциальные ошибки. Если у тебя есть исходник программы, AFL может вставлять вызовы функций в начало каждого базового блока, например в функции и циклы. О том, как при помощи AFL тестировать блекбоксы, написано множество статей, в том числе о том, как его распараллеливать. Но этот фаззер умеет компилировать программы и делать свои вставки в код, что облегчает фаззинг. Такие вставки называются санитайзерами.WARNING Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону. ЧТО ТАКОЕ САНИТАЙЗЕРЫ? Санитайзеры — это утилиты, которые обычно поставляются вместе с компиляторами. Они помогают искать ошибки в коде во время исполнения. Если обобщить, можно выделить три разновидности санитайзеров: address — ищет ошибки работы с памятью; thread — ищет проблемы в многопоточных вычислениях (состояние гонки); undefined — ищет неопределенное поведение в программе. Использование санитайзеров вместе с AFL++ — это хороший тон при фаззинге, потому что с ними фаззер будет лучше знать, какого рода ошибки ему нужно искать. С санитайзерами фаззинг всегда будет проходить успешнее, чем без них. В AFL++ доступны разные санитайзеры, которые помогают обнаруживать ошибки в коде. Вот краткое описание каждого из них. AddressSanitizer (ASAN) предназначен для обнаружения ошибок чтения или записи в память, таких как переполнение буфера или использование освобожденной памяти. Он обеспечивает детальную информацию об ошибках и помогает в их отладке. MemorySanitizer (MSAN) призван обнаружить неинициализированную память. Он помогает выявить ошибки, связанные с использованием неинициализированных данных и способные привести к непредсказуемому поведению программы. ThreadSanitizer (TSAN) предназначен для поиска гонок данных в многопоточных программах. Он помогает выявить ситуации, когда несколько потоков одновременно обращаются к одним и тем же данным, что иногда приводит к непредсказуемым результатам UndefinedBehaviorSanitizer (UBSAN) помогает обнаружить неопределенное поведение в программе, такое как деление на ноль, переполнение целочисленных типов или неправильное использование указателей. Он предупреждает о таких проблемах и помогает предотвратить возможные ошибки. Control Flow Integrity (CFI) обеспечивает защиту от атак, связанных с изменением потока управления программы. Он проверяет целостность вызовов функций и предотвращает нежелательные изменения потока выполнения. Safe-stack предназначен для защиты от переполнения стека. Он обеспечивает дополнительные механизмы безопасности, чтобы предотвратить возможные атаки, связанные с переполнением стека. В разных случаях могут пригодиться разные санитайзеры, в том числе это зависит от особенностей и требований проекта. Например, санитайзер ASAN может выявить следующие проблемы: Use-After-Free — использование чанка кучи после его освобождения; HeapOverflow — переполнение одного чанка и добавление данных в другой чанк; StackOverflow — классическое переполнение буфера. INFO Подробнее о возможностях ASAN читай в документации на GitHub. ТЕНЕВАЯ ПАМЯТЬ Одна из техник, которую используют санитайзеры, — это теневая память. По сути, они поддерживают параллельную структуру данных наряду с фактической памятью программы, при этом каждый байт в теневой памяти соответствует байту в фактической памяти. Эта теневая память используется для хранения метаданных о каждом выделении памяти. Используя теневую память, санитайзеры могут эффективно отслеживать использование памяти и обнаруживать нарушения. Например, в программе определяется какой‑то указатель: *address = ...; // or: ... = *address; Санитайзер меняет его использование на такое: if (IsPoisoned(address)) {ReportError(address, kAccessSize, kIsWrite);}*address = ...; // or: ... = *address; Санитайзер помечает любую высвобождаемую память как «отравленную», а затем следит за доступом к ней. Если произошло переполнение буфера, который выделялся через malloc, то получается, что кто‑то пишет в отравленную область, а в эту область писать нельзя. Так фаззер обнаруживает баг. Подобная же ситуация и с переполнением буфера на стеке. Предположим, у нас есть такая программа: void foo() {char a[8];...return;} Санитайзер ее изменит на вот такую: void foo() {char redzone1[32]; // 32-byte alignedchar a[8]; // 32-byte alignedchar redzone2[24];char redzone3[32]; // 32-byte alignedint *shadow_base = MemToShadow(redzone1);shadow_base[0] = 0xffffffff; // poison redzone1shadow_base[1] = 0xffffff00; // poison redzone2, unpoison 'a'shadow_base[2] = 0xffffffff; // poison redzone3...shadow_base[0] = shadow_base[1] = shadow_base[2] = 0; // unpoison allreturn;} Тут искусственно добавляется отравленная область, и, если произойдет запись в нее, фаззер сработает и сообщит о переполнении. КОМПИЛИРУЕМ ТЕСТОВУЮ ПРОГРАММУ С AFL-CLANG-FAST В качестве подопытного для испытаний используем вот такую незамысловатую программу: #include <stdio.h>int main(){char buf[256] = {0};char *a = 0;unsigned int size = read(0, buf, 256);if(size > 0 && buf[0] == 'H'){ if(size > 1 && buf[1] == 'I'){ if(size > 2 && buf[2] == '!'){ a = 1; } }}if(a == 1){ printf("g00d pwn\n");}return 0;} Для тестирования два раза скомпилируем программу. Первый раз обычным GCC, второй раз компилятором afl-clang-fast. Компиляция при помощи afl-clang-fast выглядит точно так же, как и с GCC: $ afl-clang-fast main.c -o san.elfINFO Если скомпилировать не получается, стоит пробовать варианты afl-clang или afl-gcc. В результате мы получаем инструментированный бинарный файл. При компиляции ты увидишь сообщение о том, что AFL нашел определенное количество места для инструментации и вставил свое логирование в места ветвления. Для проверки работы скомпилированного таким образом бинарного файла можно просто его запустить. Если программа падает, значит, фаззер будет врать: $ ./san.elf HI! g00d pwn Теперь загружу два бинарных файла в IDA Pro, чтобы показать, как это все выглядит внутри. ИЩЕМ ОТЛИЧИЯ БИНАРНЫХ ФАЙЛОВ С ПОМОЩЬЮ IDA PRO Слева загружена оригинальная программа, скомпилированная с помощью GCC. Справа — программа, скомпиленная при помощи afl-clang-fast. Разница очевидна: AFL добавил функцию afl_maybe_log() для логирования сообщения. Эта функция принимает два аргумента: type — тип сообщения. Может принимать значения ERROR, WARNING, INFO или DEBUG; msg — сообщение, которое нужно залогировать. Функция afl_maybe_log() будет выводить сообщение в стандартный вывод только в том случае, если уровень логирования установлен на значение не ниже типа сообщения. Например, если уровень логирования установлен на INFO, то сообщения с типом ERROR и WARNING будут выведены в стандартный вывод, а сообщения с типом INFO и DEBUG — нет. Функция afl_maybe_log() используется для логирования событий, происходящих во время работы AFL++: ошибок, предупреждений, информации о покрытии кода и так далее. Вот пример ее использования: afl_maybe_log(ERROR, "An error occurred!");afl_maybe_log(WARNING, "A warning occurred!");afl_maybe_log(INFO, "Some information");afl_maybe_log(DEBUG, "Some debug information"); Здесь сообщение об ошибке будет выведено в стандартный вывод, WARNING уйдет туда же, а INFO — нет, потому что уровень логирования не установлен на значение, которое не ниже типа этих сообщений. Точно так же можно увидеть те самые семь инструментариев, которые вставил компилятор. ФАЗЗИНГ Как и в прошлый раз, нам нужно создать две директории: /in с тесткейсами и /out — для вывода крашей: mkdir in; mkdir out; cd in; ragg2 -P 10 -r > 1.txt; cd ..INFO Ragg2 — это утилита из набора Radare2, с помощью которой можно генерировать мусорные строки. Например, это удобно, когда нужно вычислять данные для переполнения буфера. Запускаем фаззер: afl-fuzz -m none -i $PWD/in -o $PWD/out -- ./san.elf Давай посмотрим, что делает каждый из параметров: -- — берется содержимое файлов, которые находятся в директории /in; -m none означает, что необходимо убрать ограничение памяти для каждого запускаемого семпла. Это ускоряет фаззинг; -i $PWD/in задает, какие тесткейсы берем для фаззинга; -o $PWD/out задает, куда будут вылетать мутированные данные. После запуска наблюдаем такую картину. Внизу можно увидеть состояние фаззера, в правом верхнем углу — количество циклов и количество крашей. Через пять минут поле state из состояния started перейдет в состояние finished. Значит, фаззинг закончился! Все результаты мутирования входного файла находятся по пути $PWD/out/default/queue. Давай посмотрим последний файл. Можно увидеть разницу между входным файлом и тем, что выдал фаззер. Удобно, что фаззер сам сообщает, когда все закончилось. ВТОРОЙ ПРИМЕР САНИТАЙЗЕРА Допустим, у нас будет вот такая программа: #include <stdio.h>#include <string.h>int main(int argc, const char *argv[]){char *buffer = (char *)malloc(256);char *a = 0;unsigned int size = read(0, buf, 256);if(size > 0 && buf[0] == 'H'){ if(size > 1 && buf[1] == 'I'){ if(size > 2 && buf[2] == '!'){ a = 1; free(buffer); strcpy(buffer, argv[1]); puts(buffer); } }}if(a == 1){ printf("g00d pwn\n");}return 0;} Здесь в явном виде есть уязвимость Use-After-Free, но программа не падает и ошибки никакой не выдает. Наша задача — продемонстрировать работу санитайзера AFL++. Он должен обнаружить эту уязвимость, в то время как при обычном запуске фаззера, без санитайзеров, ничего произойти не должно. Компилировать будем с флагом -fsanitize и в явном виде указываем, какой тип ошибки интересует: afl-clang-fast -fsanitize=address main.c -o binary.elf Далее повторяем те же шаги, что и раньше: mkdir in; mkdir out; cd in; ragg2 -P 10 -r > 1.txt; cd .. И запускаем фаззер: afl-fuzz -i $PWD/in -o $PWD/out ./binary.elf @@ $PWD/in Результат говорит о том, что была обнаружена уязвимость Use-After-Free. Теперь проделаем такой же фокус, только без санитайзеров: gcc main.c -o clear.elf И запустим фаззер: afl-fuzz -Q -D -i $PWD/in -o $PWD/out ./clear.elf @@ $PWD/in Как видно, без санитайзера фаззер работает намного дольше. ВЫВОДЫ Мы убедились, что санитайзеры — это удобная вещь, которую стоит использовать при фаззинге, если есть такая возможность. Можно указать самому фаззеру, на какие дефекты ему стоит обращать внимание. Пример, который мы рассмотрели, показал достоинства санитайзера: фаззер адаптировался под код и заранее узнал, как ему надо двигаться, чтобы дойти до конца программы. Благодаря санитайзерам AFL++ смог грамотно подобрать тесткейс.
Добротно расписан материал, единственное может быть стоило по спойлерам разделить для большего удобства.