A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Cancel Create
QA_bible / vidy-metody-urovni-testirovaniya / fazzing-testirovanie-fuzz-testing.md
- Go to file T
- Go to line L
- Copy path
- Copy permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Cannot retrieve contributors at this time
24 lines (15 sloc) 5.9 KB
- Open with Desktop
- View raw
- Copy raw contents Copy raw contents Copy raw contents
Copy raw contents
Фаззинг-тестирование (Fuzz testing)
FUZZ testing (fuzzing) — это тип тестирования безопасности, который обнаруживает ошибки кодирования и лазейки в программном обеспечении, операционных системах или сетях. Фаззинг включает в себя ввод огромного количества случайных данных, называемых fuzz, в тестируемое программное обеспечение, чтобы заставить его дать сбой или прорвать его защиту. Фаззинг часто выявляет уязвимости, которые могут быть использованы с помощью SQL-инъекции, переполнения буфера, отказа в обслуживании (DOS) и XSS. Fuzz-тестирование выполняется с помощью фаззера — программы, которая автоматически вводит полуслучайные данные в программу и обнаруживает ошибки. Fuzz-тестирование обычно выполняется автоматически.
Фаззинг или тестирование мусорными данными / Максим Бакиров (2ГИС)
Обычно fuzzing обнаруживает наиболее серьезные ошибки или дефекты безопасности. Это очень экономически эффективный метод тестирования. Fuzzing — один из самых распространенных методов хакеров, используемых для обнаружения уязвимости системы (сюда относятся популярные SQL- или скриптовые инъекции). Примеры фаззеров:
- Mutation-Based Fuzzers: Этот тип фаззера проще всего создать, поскольку он изменяет существующие образцы данных для создания новых тестовых данных. Это относится к тупому фаззеру, но его можно использовать с более интеллектуальными фаззерами. Мы можем сделать это, выполнив некоторый уровень анализа образцов, чтобы гарантировать, что он изменяет только определенные части или не нарушает общую структуру ввода;
- Generation-Based Fuzzers: Этот тип фаззера требует большего интеллекта для создания тестовых данных с нуля, т.е. новые тестовые данные создаются на основе входной модели. Обычно он разбивает протокол или формат файла на фрагменты, которые затем выстраиваются в допустимом порядке, и эти фрагменты случайным образом распределяются независимо друг от друга;
- PROTOCOL-BASED-fuzzer: самый успешный фаззер — это детальное знание тестируемого формата протокола. Понимание зависит от спецификации. Это включает в себя запись массива спецификации в инструмент, а затем с помощью метода генерации тестов на основе модели проходится спецификация и добавляется неравномерность в содержимое данных, последовательность и т. д. Это также известно как синтаксическое тестирование, грамматическое тестирование, тестирование надежности, и т. д. Fuzzer может генерировать Test case из существующего или использовать допустимые или недействительные входные данные;
Типы ошибок, обнаруживаемых Fuzz testing:
Николай Шаплов — Как работает fuzzing-тестирование. Рассказ простым языком
- Сбои ассертов и утечки памяти (Assertion failures and memory leaks). Эта методология широко используется для больших приложений, где ошибки влияют на безопасность памяти, что является серьезной уязвимостью;
- Некорректный ввод (Invalid input). Фаззеры используются для генерирования неверного ввода, который используется для тестирования процедур обработки ошибок, и это важно для программного обеспечения, которое не контролирует его ввод. Простой фаззинг может быть способом автоматизации отрицательного тестирования;
- Исправление ошибок (Correctness bugs). Fuzzing также может использоваться для обнаружения некоторых типов ошибок «правильности». Например, поврежденная база данных, плохие результаты поиска и т. д.;
- Fuzz Testing Guide
- Fuzz Testing(Fuzzing) Tutorial: What is, Types, Tools https://github.com/VladislavEremeev/QA_bible/blob/master/vidy-metody-urovni-testirovaniya/fazzing-testirovanie-fuzz-testing.md» target=»_blank»]github.com[/mask_link]
Фаззинг-тестирование для повышения безопасности ПО
В одной из предыдущих статей мы говорили о нормативных требованиях к обеспечению безопасности на протяжении всего жизненного цикла ПО. Важная процедура контроля безопасности — фаззинг-тестирование. В этой статье мы разберем, что такое фаззинг, где он применяется и как повысить его эффективность.
- Что такое фаззинг-тестирование
- Когда следует применять фаззинг
- Рекомендации по проведению фаззинга
- Как повысить эффективность фаззинга
- Плюсы, минусы и подводные камни фаззинга
- Заключение
Что такое фаззинг-тестирование
Фаззинг — это процесс автоматического тестирования программного кода, который заключается в генерации разнообразных входных данных с целью поиска ошибок в коде. В рамках тестирования на вход передается большое количество образцов случайных данных, и каждый образец, число которых может доходить до миллиарда, запускается для проверки кода отдельно.
У процедуры фаззинг-тестирования две главных цели:
- Формирование набора образцов входных данных, которые позволяют выполнить наибольшее количество фрагментов кода, повышая тестовое покрытие.
- Мутирование входных данных так, чтобы они не соответствовали ограничениям и гипотетически могли вызывать недекларированное поведение. Недекларированное поведение возникает, когда из-за ошибки результат вычислений получается совсем не таким, какого ожидал разработчик.
Для фаззинга подойдут любые «экстремальные» случаи, которые, с одной стороны, укладываются в ограничения, а с другой не предусмотрены разработчиком специально. Именно обработка таких входных данных интересна для верификации надлежащей работы программы.
В настоящее время фаззинг — один из самых эффективных методов динамического тестирования, которому посвящено множество научных исследований и конференций. В современной практике тестирования ПО фаззинг является ключевой технологией автоматической валидации сложных программных продуктов.
Когда следует применять фаззинг
Фаззинг особенно актуален, когда для программного продукта разработан интерфейс, принимающий на вход пользовательские данные (например, форму с полями для ввода текста или возможностью загрузки файла).
Обычно фаззинг используется для проверки корректности работы парсера входных данных. Парсеры могут иметь внутри сложную логику и множество ветвлений, поэтому их важно хорошо протестировать. Однако с помощью фаззинга можно проверять корректность работы почти любого кода, обрабатывающего входные данные, даже если они поступают не от внешнего интерфейса.
Есть и кейсы, когда применение фаззинга не обосновано. Такой метод тестирования обычно не актуален для простых функций, например, с единственным аргументом типа bool. Но в более сложных случаях его так или иначе можно применять.
Рекомендации по проведению фаззинга
Ниже приведены несколько простых советов для успешного выполнения фаззинг-тестирования:
- Используйте мутационный фаззинг, учитывающий покрытие при обработке каждого образца. Каждый из образцов мутируется отдельно. Это можно сделать с помощью инструментирования на этапе компиляции целевого кода или эмулятора.
- Определите функции в коде, использующиеся для обработки входных данных, чтобы учитывать покрытие и применять детекторы ошибок только в этих функциях.
- Зафиксируйте формат входных данных для фаззинга.
- Создайте обертку для передачи данных от фаззера в парсер. Оптимизируйте ее так, чтобы она работала максимально быстро.
- Создайте набор входных данных, удовлетворяющих ограничениям и формату, чтобы откалибровать фаззер и создавать новые образцы.
- Используйте словари для генерации новых входных данных. Словари позволяют задать «контекст грамматики» для генератора входных данных. Например, они могут содержать образцы значений определенных полей в сложной структуре данных.
- Выполняйте фаззинг параллельно на всех ядрах процессора с использованием кластера фаззеров.
Как повысить эффективность фаззинга
Несколько факторов делают фаззинг более эффективным:
- Использование отклика от тестируемой программы, благодаря чему мутирование образцов входных данных будет выполняться не «вслепую», а с учетом результатов обработки предыдущих образцов. Это позволяет достигнуть большего покрытия по строкам кода.
- Использование санитайзеров ошибок для обнаружения опасных мест, которые не приводят к аварийному завершению работы программы, но могут быть источником серьезной проблемы, вплоть до уязвимостей безопасности.
- Быстрое выполнение фаззинга, что повышает шансы обнаружить что-то интересное.
- Генерация входных данных с учетом формата входных данных, что сокращает количество «холостых» итераций фаззинга, когда входные данные не проходят первый этап проверки.
- Определение информации о покрытии только интересующего нас кода.
- Хорошая стабильность: при сборе покрытия от каждой итерации следует по возможности не учитывать все источники энтропии. Например, можно не учитывать генераторы случайных чисел. Это важно, ведь при низкой стабильности теряется эффективность во время работы по отклику о покрытии.
Плюсы, минусы и подводные камни фаззинга
Одно из основных преимуществ фаззинга — возможность автоматически находить ошибки в сложном коде с большим количеством ветвлений. Фаззинг может обнаружить ошибки, которые почти невозможно обнаружить при ручном и статическом анализе. Второе преимущество заключается в том, что после этапа начальной подготовки расходуется только машинное время, а это гораздо дешевле, чем если бы ту же работу вручную выполняла команда разработчиков.
Однако у этой методики есть и недостатки. Основной минус заключается в том, что для фаззинга необходимы специалисты, обладающие высоким уровнем экспертности. Специалист должен хорошо понимать особенности тестируемого продукта, разбираться в компьютерной безопасности, низкоуровневой работе программного кода и его компиляции и даже нюансах работы операционных систем. Наконец, при внедрении фаззинг-тестирования стоит учитывать затраты на аппаратные ресурсы.
Подводные камни варьируются в зависимости от языка программирования, на котором написан тестируемый код. Например, при работе с C/C++ могут возникать проблемы, связанные с несовместимостью существующей системы сборки с инструментирующим компилятором. На этапе линковки в объектных и исполняемых файлах могут появляться неопределенные (undefined) символы. Это происходит по разным причинам: от использования неподходящего набора утилит для компиляции до использования некорректных линковочных map-файлов в системе сборки или установки разных уровней видимости в программном коде.
Работа санитайзеров ошибок может мешать корректной работе тестируемого кода. Источники энтропии могут создавать проблемы со стабильностью. А если компоненты кода сильно связаны, это может усложнить отделение тестируемого кода от всего остального проекта.
Заключение
Несмотря на то, что фаззинг — это сложная, требующая высокой квалификации процедура с высоким порогом входа, ее преимущества стоят затраченных усилий на ее внедрение. Команда Axiom JDK активно использует фаззинг при тестировании своих продуктов. Он является важной частью реализации концепции безопасной разработки (Secure Development Lifecycle, SDL), в соответствии с которой разрабатываются все продукты линейки Axiom JDK.
Помимо фаззинга инженеры Axiom JDK проводит множество других испытаний, гарантирующих качество и безопасность всех сборок. Также мы предоставляем дополнительные функции безопасности для наших клиентов, такие как доверенный репозиторий с проверенными исходным кодами самых популярных Java библиотек, российские TLS-сертификаты, выпущенные Минцифры, экстренные патчи безопасности и техподдержку 24/7 на русском языке.
Более того, в линейку продуктов Axiom JDK входит Axiom JDK Certified, среда исполнения Java, сертифицированная ФСТЭК по 4 уровню доверия (УД), что позволяет обезопасить приложения на ее основе и упростить процесс их сертификации в соответствии с нормативными требованиями.
С нами вы можете быть уверены в безопасности среды исполнения ваших приложений. Свяжитесь с нами: наши инженеры расскажут про продукты Axiom JDK, предоставляют демо-версию и помогут с миграцией.
Присоединяйтесь к нашему Telegram-каналу, чтобы оставаться в курсе новостей из мира Java.
Источник: axiomjdk.ru
Home
Подход фаззинг-тестирования родился еще в 80-х годах прошлого века. В некоторых языках он используется давно и плодотворно — соответственно, уже успел занять свою нишу. Сторонние фаззеры для Go были доступны и ранее, но в Go 1.18 появился стандартный. Мы в «Лаборатории Касперского» уже успели его пощупать и тестируем с его помощью довольно большой самостоятельный сервис.
Меня зовут Владимир Романько, я — Development Team Lead, и именно моя команда фаззит баги на Go. В этой статье я расскажу про историю фаззинга, про то, где и как искать баги, а также как помочь фаззинг-тестам эффективнее находить их в самых неожиданных местах. И покажу этот подход на примере обнаружения SQL-инъекций.
Немного истории
Когда я первый раз услышал о фаззинге, сама идея прозвучала для меня довольно странно. Казалось, это магия, с помощью которой сторонняя программа может найти баги в моем коде.
Все встало на свои места, когда я узнал, как фаззинг появился. Поэтому свой рассказ я также хочу начать с интересной истории, которая, вероятно, ждет своего Кристофера Нолана для экранизации. В ней есть все необходимые компоненты отличного голливудского блокбастера: зловещая ночь с грозой, гениальный ученый, а также древний артефакт, который принял во всем этом участие. Забегая вперед, отмечу, что роль древнего артефакта исполнил модем на 1200 бод. В итоге случайное стечение обстоятельств привело к появлению хорошего изобретения.
Так что же произошло?
Тем ученым был Бартон Миллер. В 1988 году он работал профессором в университете и решил из дома подключиться через модем к своему любимому университетскому мейнфрейму. Он пытался выполнить команду Unix… История умалчивает о том, какая именно это была команда. Предположим, это был grep.
grep -R «hello world»
В этот момент где-то недалеко ударила молния. А старый модем если и имел код коррекции ошибок, тот оказался недостаточно эффективным. Вместо «Hello world» до мейнфрейма долетел мусор:
grep -R «hello ~3#зеШwкACh»
Который внезапно вызвал segmentation fault. И grep упал.
Бартон Миллер задумался, почему это произошло. Grep к тому времени уже был старой, надежной, многократно протестированной командой, у которой явно есть какие-то проверки ввода. Но тем не менее он упал. И ученому пришла в голову идея написать программу, которая специально будет генерировать мусор и отправлять на вход в различные unix-овые утилиты. Так появился первый фаззер.
Вместе со студентами Бартон Миллер нашел очень много ошибок в командах Unix. К этому моменту все эти команды уже использовались инженерами по всему миру в течение длительного времени, но тем не менее содержали ошибки. Такова суперспособность фаззера, за которую мы его и любим, — находить баги в хорошо протестированном коде.
Как фаззер находит баги
Тесты можно классифицировать по-разному. Но давайте распределим их по уровню семантического знания о коде, которое использовано при написании теста.
Больше всего знаний о тестируемом коде используется при построении тестов по тест-кейсам, например в юнит-, ручных или интеграционных тестах. Они содержат некие пред- и постусловия — конкретные проверки того, что на вход программы мы передали А, а на выходе должны получить Б. Для написания таких тестов однозначно придется изучить код и требования к нему.
Левее по этой шкале находятся property based тесты. В них уже нет конкретных входов и выходов. Данные на вход генерируются случайным образом, но проверяются определенные свойства кода (поэтому тесты и называются property based). К примеру, если мы проверяем функцию сортировки, на выходе ожидаем массив, каждый последующий элемент которого больше либо равен предыдущему.
В крайней левой части шкалы находятся фаззинг-тесты, имеющие минимальные знания о продукте. Им известно только то, что код не должен падать, зависать или отъедать какое-то безумное количество памяти. С точки зрения теста сам продукт представляет собой черный ящик.
В том, что при фаззинге используются минимальные знания, есть как плюсы, так и минусы. Если тесты по тест-кейсам позволяют находить так называемые known unknowns (т. е. проверяют неизвестное поведение в известных сценариях), то фаззинг ищет unknown unknowns (проверяет неизвестное поведение в неизвестных сценариях). Именно эта особенность позволяет фаззингу находить баги в хорошо протестированном коде — он обнаруживает сценарии, про которые разработчик никогда и не подумал бы.
Для примера приведу тесты для функции сортировки, которая принимает слайс int.
Функцию можно протестировать с помощью тестов по тест-кейсам. В этом случае на вход мы передаем (2, 1, 3) и проверяем ожидание, что на выходе будет 1, 2, 3.
В property based тестах на входе случайная последовательность, а на выходе надо убедиться, что каждый последующий элемент больше или равен предыдущему (т. е. проверяется свойство).
Фаззинг также передаст случайную последовательность, но удостоверится, что функция не упала.
Фаззинг не заменяет классическое тестирование. Он нужен, когда проверяется уже оттестированный код, а фантазия тестировщиков подходит к концу. В общем случае фаззер найдет меньше ошибок, чем тесты по тест-кейсам. Но эти ошибки будут наиболее разнообразны.
Это хорошо заметно в Go-шной реализации фаззера — go fuzz, которая модифицирует корпус входных тестовых данных так, чтобы отработали все ветви кода. Стандартный фаззер Go вообще одинаково хорошо подходит как для написания property based тестов, так и для классического фаззинга. Официальная документация Go по фаззингу не делает различия между этими видами тестов.
Как помочь фаззеру
Давайте рассмотрим нехитрую программу на Go.
Наш код объявляет неинициализированный нулевой указатель и что-то по нему пишет.
Если запустить этот код, он упадет с ошибкой. Проблема в пятой строке.
Чисто теоретически ее можно было бы проигнорировать и продолжить выполнение. Если кто-то помнит, в Visual Basic был такой режим: при ошибке программа не падала, а просто переходила к следующей строке кода. Получалось, что они надежны, но поведение этих программ непредсказуемо. Это никак не помогло бы фаззеру.
Вернемся к Бартону Миллеру. Что было бы, если бы grep проигнорировал обращение к невалидному указателю? Скорее всего, фаззер не нашел бы багов в команде. Команда обработала бы мусор на входе и выдала мусор на выходе. Никто не понял бы, что произошло нечто плохое.
Т. е. фаззер в принципе смог найти ошибку только благодаря крэшу (программа проверила свой собственный внутренний инвариант, согласно которому нельзя обращаться к некорректному указателю, и упала, когда он оказался нарушен).
Так мы приходим к выводу: падать с ошибкой полезно.
Чем больше код проверяет своих внутренних инвариантов, тем больше фаззер может найти багов.
Вот несколько примеров с проверкой внутренних инвариантов:
- Инвариант может заключаться в том, что оба потомка красного узла в красно-черном дереве — черные. Если он нарушен, код может как-то сообщить об этом фаззеру.
- Можно проверять, что количество элементов в контейнере неотрицательно — в очереди не может содержаться «-1» элемент.
- Проверка может выявлять, что в стеке количество операций Pop меньше или равно количеству Push.
- Бизнес-логика может контролировать отсутствие превышения некоего программного лимита. Тот факт, что мы обнаружим нарушение этого инварианта, будет свидетельствовать об ошибке в бизнес-логике.
- Кэш должен отсекать повторные запросы. Это особенно актуально, если база вычисляет тяжелые запросы и нужно проверить бизнес-логику, которая хранит в памяти результаты нескольких последних запросов. В этом случае фаззер может найти ошибку в логике работы с кэшем.
- SQL-запрос не должен возвращать ошибку некорректного синтаксиса. Если же мы получаем такую ошибку, в нашем коде однозначно есть проблема. Скорее всего, мы неправильно формируем тело SQL-запроса или в это тело без какой-либо санитизации попадает пользовательский ввод (SQL-инъекция).
Все эти примеры объединяет тот факт, что если проверка сработает, это однозначно указывает на некорректно написанный код. Это никак не связано с окружением. По сути это ничем не отличается от обращения к невалидному указателю, упомянутому выше.
Инвариант мало проверить, нужно еще донести до фаззера информацию о том, что есть нарушение. Самый простой способ — кинуть panic. Это можно сделать с любого уровня абстракции.
Среди Go-феров есть предубеждение против panic, поэтому можно использовать более лайтовые варианты:
- Кидать panic не всегда, а только в специальном фаззинг-режиме: ввести переменную окружения, и если она задана, при нарушении инвариантов кидать panic. Фаззинг-режим, кстати, помогает решить проблему с «дорогими» проверками инвариантов, когда на них уходит много ресурсов.
- Можно использовать специальный уровень логирования, доступный фаззеру. Когда тот увидит запись с этим уровнем, он поймет, что нарушен некий внутренний инвариант (и сделает вывод, что код написан некорректно). Этот подход требует более сложной инфраструктуры. И важно не писать в лог на этом уровне, когда наблюдаются проблемы с инфраструктурой (например, если отвалилась сеть).
При проверке инвариантов у фаззинг-теста нет никакого ожидания относительно кода. За счет этого он более стабильный — его не нужно менять при малейших изменениях. С развитием продукта его также нужно поддерживать, но усилий на это будет уходить гораздо меньше, чем в случае с тестами по тест-кейсам. Возвращаясь к примеру с grep — если мы сменим движок обработки регулярных выражений, суть теста никак не изменится.
Фаззинг имеет смысл, если функции работают достаточно быстро. Если функция выдает ответ через несколько минут после передачи в нее начальных данных, за разумное время фаззер просто не успеет ничего проверить. А проверив слишком мало вариантов, он ничего не найдет. Возможно, в этой ситуации процесс ускорят моки, но работать с ними надо всегда очень осторожно. Также можно гонять тесты на маленьких частях кода.
Помогаем фаззеру искать SQL-уязвимости
Теперь применим этот подход, чтобы обрабатывать ошибки работы с базой данных на примере SQLite.
Предположим, у нас есть нативный код, в котором присутствует уязвимость SQL-инъекции. Мы формируем строку, и если получаем ошибку, смотрим, что это было. Если это ошибка SQLite, которая говорит, что синтаксис некорректный, и при этом включена переменная окружения FUZZING, мы кидаем panic. В ином случае мы обрабатываем ошибки стандартным путем.
Каждый раз писать такой код неудобно, поэтому его можно разбить на вспомогательные функции и уже их использовать по всему коду.
Я бы предложил такой вариант разбиения.