[Перевод] Фаззинг в стиле 1989 года

habr.png

С наступлением 2019 года хорошо вспомнить прошлое и подумать о будущем. Оглянемся на 30 лет назад и поразмышляем над первыми научными статьями по фаззингу: «Эмпирическое исследование надёжности утилит UNIX» и последующей работой 1995 года «Пересмотр фаззинга» того же автора Бартона Миллера.

В этой статье попытаемся найти баги в современных версиях Ubuntu Linux, используя те же самые инструменты, что и в оригинальных работах по фаззингу. Вы должны прочитать оригинальные документы не только для контекста, но и для понимания. Они оказались весьма пророческими в отношении уязвимостей и эксплоитов на десятилетия вперёд. Внимательные читатели могут заметить дату публикации оригинальной статьи: 1990 год. Ещё более внимательные заметят копирайт в комментариях исходников: 1989.


Для тех, кто не читал документы (хотя это реально следует сделать), в данном разделе краткое резюме и некоторые избранные цитаты.

Программа фаззинга генерирует случайные потоки символов, с возможностью генерировать только печатные или непечатные символы. Она использует некое начальное значение (seed), обеспечивая воспроизводимость результатов, чего современным фаззерам часто не хватает. Набор скриптов запускается на тестируемых программах и проверяет наличие основных дампов. Зависания выявляются вручную. Адаптеры выдают случайные входные данные для интерактивных программ (статья 1990 года), сетевых сервисов (1995) и графических X-приложений (1995).

В статье 1990 года тестируются четыре процессорные архитектуры (i386, CVAX, Sparc, 68020) и пять операционных систем (4.3 BSD, SunOS, AIX, Xenix, Dynix). В статье 1995 года аналогичный выбор платформ. В первой статье удаётся добиться сбоя 25–33% утилит, в зависимости от платформы. В последующей статье эти цифры варьируются от 9% до 33%, причем у GNU (на SunOS) и Linux наименьший процент сбоев.

В статье 1990 года делается вывод, что 1) программисты не проверяют границы массива или коды ошибок, 2) макросы затрудняют чтение и отладку кода и 3) язык C весьма небезопасен. Особого упоминания удостоены экстремально небезопасная функция gets и система типов C. В ходе тестирования авторы нашли уязвимости Format String за годы до их массовой эксплуатации. Статья завершается опросом пользователей о том, как часто они исправляют баги или сообщают о них. Оказалось, что сообщать о багах трудно и не было особого интереса в их исправлении.

В статье 1995 года упоминается ПО с открытым исходным кодом и обсуждается, почему в нём меньше ошибок. Цитата:

Когда мы исследовали причины сбоев, то проявилось тревожное явление: многие из багов (около 40%), о которых сообщалось в 1990 году, по-прежнему присутствуют в своей точной форме в 1995 году. …

Используемые здесь методы просты и в основном автоматизированы. Трудно понять, почему разработчики не используют этот лёгкий и бесплатный источник повышения надёжности.


Только через 15–20 лет техника фаззинга станет стандартной практикой у крупных вендоров.

Ещё мне кажется, что это заявление 1990 году предвидит будущие события:

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


К счастью, спустя 30 лет д-р Бартон по-прежнему предоставляет полный исходный код, скрипты и данные для воспроизведения своих результатов: похвальный пример, которому должны следовать другие исследователи. Скрипты без проблем работают, а в инструмент фаззинга потребовалось внести лишь незначительные изменения для компиляции и запуска.

Для этих тестов мы использовали скрипты и входные данные из репозитория fuzz-1995-basic, потому что там самый свежий список тестируемых приложений. Согласно README, здесь те же самые случайные входные, что и в оригинальном исследовании. Результаты ниже для современного Linux получены точно на том же коде фаззинга и входных данных, что и в оригинальных статьях. Изменился только список утилит для тестирования.


Очевидно, что за последние 30 лет произошли некоторые изменения в программных пакетах Linux, хотя довольно много проверенных утилит по-прежнему ведут свою родословную на протяжении десятилетий. Где возможно, мы взяли современные версии тех же программ из статьи 1995 года. Некоторые программы больше не доступны, их мы заменили. Обоснование всех замен:

  • cfecc1: Эквивалент препроцессору C из статьи 1995 года.
  • dbxgdb: Эквивалент дебаггера 1995 года.
  • ditroffgroff: ditroff больше не доступен.
  • dtblgtbl: Эквивалент GNU Troff старой утилиты dtbl.
  • lispclisp: Стандартная реализация lisp.
  • moreless: Less is more!
  • prologswipl: Есть два варианта prolog: SWI Prolog и GNU Prolog. SWI Prolog предпочтительнее, потому что это более старая и полная реализация.
  • awkgawk: GNU версия awk.
  • ccgcc: Стандартный компилятор C.
  • compressgzip: GZip это идейный наследник старой Unix-утилиты compress.
  • lintsplint: Переписанный lint под лицензией GPL.
  • /bin/mail/usr/bin/mail: Эквивалентная утилита по другому пути.
  • f77fort77: Есть два варианнта компилятора Fortan77: GNU Fortran и Fort77. Первый рекомендуется для Fortran 90, а второй для поддержки Fortran77. Программа f2c активно поддерживается, её список изменений ведётся с 1989 года.


Техника фаззинга 1989 года по-прежнему находит ошибки в 2018 году. Но есть определённый прогресс.

Чтобы измерить прогресс, нужна некая база. К счастью, для утилит Linux такая база существует. Хотя во времена оригинальной статьи 1990 года ОС Linux ещё не существовала, но повторный тест 1995 года запустил тот же код фаззинга на утилитах из дистрибутива Slackware 2.1.0 от 1995 года. Соответствующие результаты приводятся в таблице 3 статьи 1995 года (стр. 7–9). По сравнению с коммерческими конкурентами GNU/Linux выглядит очень хорошо:

Процент сбоев утилит в свободно распространяемой Linux-версии UNIX была второй по величине: 9%.


Итак, сравним утилиты Linux 1995 и 2018 года инструментами для фаззинга 1989 года:

Ubuntu 18.10 (2018) Ubuntu 18.04 (2018) Ubuntu 16.04 (2016) Ubuntu 14.04 (2014) Slackware 2.1.0 (1995)
Сбои 1 (f77) 1 (f77) 2 (f77, ul) 2 (swipl, f77) 4 (ul, flex, indent, gdb)
Зависания 1 (spell) 1 (spell) 1 (spell) 2 (spell, units) 1 (ctags)
Всего протестировано 81 81 81 81 55
Сбои/зависания, % 2% 2% 4% 5% 9%


Удивительно, но количество сбоев и зависаний Linux всё ещё больше нуля, даже на последней версии Ubuntu. Так, f77 вызывает программу f2c с ошибкой сегментации, а программа spell зависает на двух вариантах тестовых входных данных.
Я смог вручную выяснить корневую причину некоторых багов. Одни результаты, такие как ошибка в glibc, стали неожиданными, в то время как другие, такие как sprintf с буфером фиксированного размера, были предсказуемы.

Сбой ul


Ошибка в ul — это на самом деле баг в glibc. В частности, о нём сообщалось здесь и здесь (другой человек нашёл её в ul) в 2016 году. Согласно баг-трекеру, ошибка по-прежнему не исправлена. Поскольку баг невозможно воспроизвести на Ubuntu 18.04 и новее, то он исправлен на уровне дистрибутива. Судя по комментариям к баг-трекеру, основная проблема может быть очень серьёзной.

Сбой f77


Программа f77 идёт в пакете fort77, который сам является скриптом-оболочкой вокруг f2c, транслятора исходного кода с Fortran77 на C. Отладка f2c показывает, что сбой происходит, когда функция errstr печатает слишком длинное сообщение об ошибке. По исходниккам f2c видно, что там используется функция sprintf для записи строки переменной длины в буфер фиксированного размера:

errstr(const char *s, const char *t)
#endif
{
  char buff[100];
  sprintf(buff, s, t);
  err(buff);
}


Похоже, что этот код сохранился с момента создания f2c. Программа ведёт историю изменений, по крайней мере, с 1989 года. В 1995 году при повторном фаззинге компилятор Fortran77 не тестировали, иначе проблему нашли бы раньше.

Зависание spell


Отличный пример классической взаимоблокировки. Программа spell делегирует проверку орфографии программе ispell через канал. spell читает текст строка за строкой и выдаёт блокирующую запись размера строки в ispell. Однако ispell читает максимум BUFSIZ/2 байт за раз (4096 байт в моей системе) и выдаёт блокирующую запись для гарантии, что клиент получил данные о проверке, обработанные до сих пор. Два различных тестовых входных данных заставили spell записать строку более 4096 символов для ispell, что привело к взаимоблокировке: spell ждёт, пока ispell прочитает всю строку, в то время как ispell ждёт от spell подтверждения о прочтении изначальных орфографических правок.

Зависание units


На первый взгляд похоже, что присутствует условие бесконечного цикла. Зависание вроде бы в libreadline, а не в units, хотя более новые версии units не страдают от этой ошибки. Журнал изменений указывает, что была добавлена фильтрация входных данных, которая могла случайно устранить эту проблему. При этом тщательное расследование причин выходит за рамки этого блога. Возможно, способ повесить libreadline ещё остался.

Сбой swipl


Для полноты картины хочу упомянуть сбой swipl, хотя я тщательно его не изучал, так как баг давно исправлен и вроде бы довольно качественно. Сбой на самом деле является утверждением (т. е. тем, что никогда не должно происходить), которое вызывается при преобразовании символов:

[Thread 1] pl-fli.c:2495: codeToAtom: Assertion failed: chrcode >= 0
C-stack trace labeled "crash":
[0] __assert_fail+0x41
[1] PL_put_term+0x18e
[2] PL_unify_text+0x1c4

Аварийное завершение всегда плохо, но здесь хотя бы программа может заявить об ошибке, обваливаясь рано и громко.


В последние 30 лет фаззинг оставался простым и надёжным способом поиска багов. Хотя в этой области идут активные исследования, даже фаззер 30-летней давности успешно находит ошибки в современных утилитах Linux.

Автор оригинальных статей предсказал проблемы безопасности, которые вызовет С в течение ближайших десятилетий. Он убедительно доказывает, что на C слишком легко написать небезопасный код и по возможности его следует избегать. В частности, статьи демонстрируют: баги проявляются даже при самом простом фазинге, и такое тестирование следует включить в стандартную практику разработки программного обеспечения. К сожалению, этому совету не следовали десятилетиями.

Надеюсь, вам понравилась эта 30-летняя ретроспектива. Ждите следующей статьи «Фаззинг в 2000 году», где мы исследуем, насколько устойчивы приложения Windows 10 по сравнению с их эквивалентами Windows NT/2000 при проверке фаззером. Думаю, ответ предсказуем.

© Habrahabr.ru