Выпуск 24. Февраль 2015

Реализация удаленного вызова процедур (RPC) в Perl с помощью Thrift | Содержание | Каналы в Perl 6

Fuzzing-тестирование perl-интерпретатора с помощью afl

Закончились новогодние каникулы. Кто-то ездил отдыхать в жаркие страны, кто-то смотрел телевизор и не вылезал из-за(под) стола. Но были и те, кому было интересно провести бесчеловечные эксперименты с Perl. Об одном таком эксперименте и пойдёт речь.

American Fuzzy Lop

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

Не так давно появился очень перспективный фаззер American Fuzzy Lop (afl), который разрабатывает известный эксперт в области безопасности Michal Zalewski. Название фаззера происходит от породы кроликов «Американский пушистый вислоухий». Как поясняет автор, фаззер создавался под впечатлением от утилиты bunny-the-fuzzer (кролик-фаззер) от Tavis Ormandi, что как-то и объясняет такое странное название.

afl внедряет в код специальные ассемблерные инструкции в точки ветвления для отслеживания поведения подопытной программы, позволяя определять, когда происходит переключение на новую ветвь исполнения, и какие входные данные повлияли на это. Для этих целей требуется собрать исходный код программы с помощью компиляторов-обёрток afl-gcc, afl-gcc++ или afl-clang в зависимости от того, какой компилятор используется.

После того как программа собрана, она может быть подвергнута тестированию с помощью фаззера afl-fuzz. Фаззер создаёт форк-сервер, который начинает запускать экземпляр программы и передавать входные данные. Отслеживает инструментальный вывод, который был зашит при компиляция программы, для того чтобы определять выбранную ветвь исполняемого кода. Автоматически вычисляются лимиты для времени выполнения кода, чтобы выявлять зависания. Как только произойдёт крах или зависание, то экземпляр входных данных, который их вызвал, помещается в выделенный каталог для последующего исследования.

Утилите afl-fuzz передаются следующие обязательные параметры:

  • -i — каталог с начальными образцами входных данных, которые будет передаваться тестируемой программе;

  • -o — каталог, где будут сохраняться результаты: образцы ввода, которые приводят к краху/зависанию, а также текущая очередь мутировавших входных данных.

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

$ afl-fuzz -i input -o output testing_program

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

$ afl-fuzz -i input -o output testing_program @@

Иногда программа проверяет расширение файла или имеет проблемы с теми именами, которые придумывает afl-fuzz, для это есть удобная опция -f, которая передаёт одно и тоже имя файла:

$ afl-fuzz -i input -o output -f input_file.ext testing_program @@

После запуска фаззера мы получаем подобную картинку:

afl

afl

Здесь отображаются текущие результаты поиска. Самый важный параметр это uniq crashes, который определяет количество найденных крахов программы.

Тестирование Perl

Вернёмся к новогоднему эксперименту. Некто Brian Carpenter основательно взялся за проверку Perl с помощью фаззера afl. Запустив afl на простом примере CGI-скрипта, он за несколько праздничных дней умудрился собрать богатый урожай багов:

Баг #123539 (исправлен)

Простейший экземпляр кода, приводящий к чтению неинициализированной памяти (... — 80 символов b):

$perl -e '/bbbbbbbbb...bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbAAbbb/il'
panic: reg_node overrun trying to emit 0, f60370>=f60370 at -e line 1.

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

Баги #123551, 123554 (исправлены)

Следующий пример:

$ perl -e '33x~3'

Приводил к панике для perl < 5.20 и ошибке сегментирования в старших версиях. Использование символа ~ для числа повторов приводило к целочисленному переполнению.

Баг #123617

Пример кода, вызывающего ошибку сегментирования:

$ perl -e '"$a{m/""$b
 / m ss
";@c = split /x/'

Проблема пока не исправлена.

Баг #123542 (исправлен)

Пример кода, приводящего к краху:

$ perl -e 's/${<>{})//'

При построении дерева операций происходит разыменование нулевого указателя и последующий крах программы.

Баг #123677

Пример кода, приводящего к краху:

$ perl -e 's)$0{0h());qx(@0);qx(@0);qx(@0)'

Проблема актуальна для версии Perl ≥ 5.21.7.

Как присоединиться к тестированию

Вероятно, у вас появится желание самим заняться тестированием, поэтому можно составить короткую инструкцию о том, как собрать Perl для подобных целей.

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

$ sudo useradd -m fuzzer
$ sudo su - fuzzer

Загрузим и соберём последнюю версию фаззера:

$ wget http://lcamtuf.coredump.cx/afl/releases/afl-latest.tgz
$ tar xf alf-latest.tgz
$ cd afl-1*
$ make PREFIX=$HOME/afl
$ make PREFIX=$HOME/afl install

Чтобы собрать perl с использованием afl, удобнее всего воспользоваться perlbrew. Например, чтобы собрать последний версию Perl из blead, нужно выполнить команду:

$ perlbrew install -n --as perl-blead-afl -Dusedevel \
    -Dcc=$HOME/afl/bin/afl-gcc \
    -DAFL_PATH=$HOME/afl/lib/afl \
    perl-blead

Здесь мы указываем путь к компилятору afl-gcc, которым будет собран perl. Переключимся на новый Perl

$ perlbrew use perl-blead-afl

Создадим произвольный пример кода

$ mkdir perl-input
$ echo '$x = eval { die $! }' > perl-input/test.pl

Запустим фаззер:

$ ~/afl/bin/afl-fuzz -i perl-input -o perl-output perl

Ждем сутки, двое, трое… и возможно фаззер что-нибудь найдёт.

Что ещё удалось найти

Воспользовавшись указанной выше инструкцией, мне удалось найти четыре(!) уникальных примера, которые приводили к краху Perl. Опишу только те, которые успел проанализировать.

Баг #123652

Пример кода:

$ perl -e '$1=eval{a:}'
zsh: segmentation fault perl -e '$1=eval{a:}'

Данная проблема появилась, начиная с Perl 5.13.6. Присвоение переменной только для чтения $1 здесь для отвлечения глаз, а основная проблема в eval, который содержит лишь одну метку. Код оптимизатора содержал ошибку, обращаясь к элементу структуры, которая не была создана. Детальное пояснение сделал Father Chrysostomos, который и исправил этот баг.

Баг #123712

Ещё один простой пример некорректного кода, который вызывает крах парсера:

$ echo -n '/$a[/<<' | perl

В данном примере используется echo -n, чтобы подчеркнуть, что код не содержит переноса строки (при использовании -e это уже не сработает). В конце примера располагается <<, что указывает на начало встроенного документа, но он пустой. Фрагмент кода функции сканирующая встроенный документ S_scan_heredoc:

while (s < bufend - len + 1 &&
    memNE(s,PL_tokenbuf,len) ) {
    if (*s++ == '\n')
        ++shared->herelines;
}

В данном коде указатель на строку s, в которой ищется перенос строки, оказывается равным NULL, и при выполнении функции memNE (memcmp) происходит ошибка сегментирования.

Как получить минимальный пример кода, который приводит к краху?

Предположим, вы получили образец кода, который приводит к краху, но как вычистить мусор, который не влияет на результат и мешает поиску ошибки? Для этих целей фаззер имеет опцию -C, которая позволяет проводить исследование краха. Например:

$ ~/afl/bin/afl-fuzz -C -i dir-with-crash-sample -o other-crash-samples perl

Если в каталоге dir-with-crash-sample есть образец скрипта, который приводит к краху Perl, то в этом режиме фаззер начнёт модифицировать пример и ожидать краха. Если крах не происходит — пример отбрасывается, если происходит, то образец сохраняется. Среди них можно обнаружить наиболее короткие и возможно наиболее чётко выявляющие проблему образцы.

Альтернативный и более быстрый вариант получения минимального образца — это использование утилиты afl-tmin. В этом случае происходит быстрый перебор отсечения блоков различного размера для получения минимального размера файла, который приводит к краху. В этом случае никакой интеллектуальной эвристики не применяется.

$ ~/afl/bin/afl-tmin -i crash_sample.pl -o min_crash_sample.pl perl

Другие варианты исследований

Помимо самого Perl можно производить исследование различных модулей со CPAN и прежде всего XS-модулей. Можно попробовать фаззить сериализаторы Storable, Sereal, JSON::XS и другие. Можно также подвергнуть тестированию шаблонизаторы, например, Text::Xslate. Можно исследовать и модули, работающие с изображениями.

Для примера, возьмём Imager.

$ perlbrew use perl-blead-afl
$ cpanm Imager

В данном случае cpanm автоматически соберёт модуль Imager с использованием компилятора, которым собирался Perl, т.е. afl-gcc.

$ ~/afl/bin/afl-fuzz -i dir-with-image -o imager-output \
    -f image_file \
    perl -MImager -e 'Imager->new(file=>$ARGV[0])' @@

Данная команда будет загружать Imager и подгружать файл изображения, которые генерирует afl-fuzz. Т.о. можно искать ошибки в Imager.

К сожалению, Imager работает достаточно медленно, и пока ничего интересного выявить не удалось.

Заключение

В статье рассмотрен инструмент для фаззинг-тестирования afl, приведены примеры найденных с его помощью ошибок в Perl. Если вас заинтересовал этот инструмент, то вот несколько ссылок на статьи о других найденных ошибках с помощью afl:

Владимир Леттиев


Реализация удаленного вызова процедур (RPC) в Perl с помощью Thrift | Содержание | Каналы в Perl 6
Нас уже 1393. Больше подписчиков — лучше выпуски!

Комментарии к статье