Содержание


Тестирование методом "черного ящика"

Атакуйте свои программы до того, как это сделает кто-нибудь другой

Comments

На протяжении многих лет меня поражает количество поврежденных файлов, которые могут вызвать сбой Microsoft Word. Несколько несоответствующих байтов -- и работа приложения более невозможна. Раньше, в эпоху операционных систем без механизмов защиты памяти, из-за этого могла прекратиться работа и всего компьютера. Почему Word не может распознать, что полученные данные не годятся, и просто выдать сообщение об ошибке? Почему это приложение повреждает свой собственный стек и прочие области памяти только из-за того, что поменялись несколько битов? Вряд Word -- это единственная программа, которая ведет себя столь ужасно при встрече с неверно сформированными файлами.

Эта статья знакомит с технологией, которая пытается предотвратить неприятности именно такого рода. При тестировании методом "черного ящика" вы атакуете программу случайными неверными данными (которые называются искажением (fuzz)), а затем ждете и смотрите, что сломалось. Хитрость такого метода заключается в том, что тестирование нелогично. Вместо того, чтобы пытаться угадать, какие данные могут спровоцировать ошибку (как обычно поступают люди, занимающиеся тестированием), автоматизированный тест просто выдает программе как можно более случайный "мусор". Ошибки, определяемые подобным тестированием, обычно шокируют программистов, поскольку ни один логически мыслящий человек не мог их даже ожидать.

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

Как работает тестирование по методу "черного ящика"

Тестирование по этому методу реализовать довольно просто:

  1. Подготовьте корректный файл, предназначенный для ввода в программу;
  2. Замените некоторые части этого файла случайными данными;
  3. Откройте файл в программе;
  4. Посмотрите, что идет не так.

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

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

Хотя тестирование по методу "черного ящика" обычно требует некоторого ручного написания кода, существуют инструментальные средства, которые могут помочь в этом. Например, Листинг 1 демонстрирует простой Java-класс,™ который случайным образом модифицирует определенную длину файла. Обычно я предпочитаю менять файл где-нибудь после первых нескольких байт, поскольку программы имеют тенденцию определять ошибку в самом начале данных, а не потом. (Вы же хотите найти ошибки, которые программа не проверяет, а не те, с которыми она справляется.)

Листинг 1. Класс, который заменяет часть файла случайными данными
[CDATA[import java.io.*;
import java.security.SecureRandom;
import java.util.Random;

public class Fuzzer {

     private Random random = new SecureRandom();
     private int count = 1;

     public File fuzz(File in, int start, int length) throws IOException  
{

         byte[]

Исказить файл легко. Передать его приложению обычно ненамного труднее. Для написания этой части теста часто наилучшим выбором будут такие языки сценариев, как AppleScript или Perl. Для программ с графическим интерфейсом самой трудной частью может быть распознавание того факта, что приложение указало правильный режим ошибки. Иногда проще всего посадить кого-нибудь перед монитором и заставить его помечать каждый тест как удачный или неудачный. Обязательно отдельно называйте и сохраняйте все сгенерированные случайные контрольные примеры, чтобы потом можно было воспроизвести все ошибки, обнаруженные с помощью этой процедуры.

Написание безопасного кода

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

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

Наиболее частой ошибкой является предположение, что, поскольку экземпляр вашей программы записал какие-то выходные данные, то можно снова считать эти данные обратно без их проверки. Это опасно! Данные могли быть переписаны на диске другой программой. Они могли быть повреждены из-за сбоя диска или неправильной передачи по сети. Они могли быть изменены другой программой в результате ошибки. Они даже могли быть преднамеренно изменены с целью разрушить систему безопасности вашей программы. Ничего не предполагайте. Все проверяйте.

Конечно, обработка ошибок и проверка -- это неприятно, неудобно и раздражает, и поэтому это презирается программистами по всему миру. Уже шестьдесят лет мы живем в эпоху компьютеров, а такие простые вещи, как успешность открытия файла или выделения памяти, по-прежнему не проверяются. Кажется, что просить программистов проверять каждый байт и каждый инвариант при чтении файла бесполезно. Но не делать этого -- значить оставить программы уязвимыми для неверных данных. К счастью, этому можно помочь. При правильном использовании современные средства и технологии могут значительно облегчить головную боль по защите приложений от подобных ошибок. В частности, напрашиваются три метода:

  • Контрольные суммы;
  • Подобные XML форматы на основе грамматики;
  • Код с проверкой, например, Java.

Проверка искажения данных с помощью контрольных сумм

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

Такие надежные алгоритмы вычисления контрольной суммы, как MD5 и SHA, проделывают гораздо более сложные вычисления, чем просто взятие остатка от деления на 256. В языке Java есть классы java.security.DigestInputStream и java.security.DigestOutputStream, которые предоставляют удобные средства добавления к данным контрольной суммы. Использование одного из этих алгоритмов получения контрольной суммы снижает вероятность случайного повреждения до соотношения менее чем один к миллиарду (хотя по-прежнему остается возможность преднамеренных атак, как вы увидите далее).

Хранилище XML и проверка данных

Прекрасным способом избежать проблем, связанных с повреждением данных, является хранение данных в формате XML. Хотя язык XML изначально предназначался для Web-страниц, книг, поэм, статей и подобных документов, он нашел широкое применение практически во всех областях, начиная с финансовых данных и заканчивая векторной графикой и последовательными объектами.

Основной характеристикой, которая делает XML-форматы к искажению данных, является то, что анализатор XML не делает никаких предположений о входных данных. Это именно то, что нужно для надежного формата файлов. Синтаксические анализаторы XML спроектированы таким образом, что любые входные данных (правильно сформированные или неправильно, верные или нет) обрабатываются строго определенным способом. Синтаксический анализатор XML может обрабатывать любой поток байтов. Если данные сначала проходят через синтаксический анализатор XML, то достаточно быть готовым к тому, что может передать анализатор. Например, вам не нужно проверять данные на наличие null-символов, поскольку анализатор XML никогда их не передаст. Если синтаксический анализатор XML видит во входных данных такой символ, он обрабатывает эту исключительную ситуацию и прекращает обработку данных. Конечно, вам по-прежнему нужно обрабатывать эту исключительную ситуацию, но написать блок catch для обработки обнаруженной ошибки гораздо проще, чем писать код для обнаружения всех возможных ошибок.

В качестве дополнительных мер безопасности можно проверить документ с помощью DTD и/или схемы. Это проверка не только того, что XML правильно сформирован, но и того, что файл, как минимум, близок к ожидаемому. Такая проверка редко скажет вам все, что нужно знать о документе, но она облегчает написание большого количества простых проверок. С помощью XML очень просто строго ограничить принимаемые документы теми форматами, которые вы можете хорошо обрабатывать.

Однако по-прежнему найдутся части кода, которые невозможно проверить с помощью DTD или схемы. Например, нельзя таким образом проверить, совпадает ли цена в счете с ценой этого же товара в инвентаризационной базе данных. При получении от клиента заказа, который содержит цену, в XML или любом другом формате, всегда необходимо проверять, что заказчик не изменил цену при ее указании в документе. Однако эти последние проверки можно реализовать с помощью специального кода.

Форматы на основе грамматики

Одной из характеристик, которая делает XML столь устойчивым к неправильным данным, является то, что этот формат тщательно и формально определен с использованием грамматики в форме Бэкуса-Наура (BNF - Backus-Naur Form). Многие синтаксические анализаторы созданы напрямую из этой грамматики с помощью таких средств, как JavaCC или Bison. Смыслом подобных инструментов является чтение произвольного входного потока и определение того, удовлетворяет он грамматике или нет.

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

Проверка Java-кода

Многие ошибки при тестировании методом "черного ящика" являются прямым следствием ошибок выделения памяти и переполнения буфера. Создание приложений на безопасном языке с функциями "уборки мусора", который выполняется в виртуальной машине (например, Java или управляемый C#) исключает многие потенциальные проблемы. Даже если вы пишете свой код на C или C++, необходимо использовать надежную библиотеку для очистки памяти. В 2006 году никто из программистов настольных или серверных программ не должен сам организовывать управление памятью.

Рабочий цикл Java обеспечивает дополнительный уровень защиты собственного кода. Перед загрузкой файла .class в виртуальную машину он анализируется механизмом проверки байтового кода и, опционально, системой SecurityManager. Java не предполагает, что компилятор, который создал файл .class, был лишен ошибок или отработал корректно. Язык Java был с самого начала спроектирован таким образом, чтобы выполнение потенциально вредоносного кода, которому нельзя доверять, осуществлялось в отдельной безопасной ограниченной среде. Эта среда даже не доверяет тому коду, который сама и скомпилировала. Ведь как-никак, а кто-нибудь мог бы изменить байтовый код вручную с помощью редактора шестнадцатеричных символов и попытаться вызвать переполнение буфера. У нас у всех должна быть подобного рода паранойя относительно входных данных для наших программ.

Думайте как враг

Каждая из предыдущих технологий имеет большое значение для предотвращения случайных сбоев. Если их собрать вместе и правильно применить, то они сокращают шанс необнаруженной непреднамеренной ошибки практически до нуля. (Ну, не совсем до нуля, но до величины того же порядка, как вероятность того, что случайные космические лучи заставят центральный процессор получить 3 в результате сложения 1+1.) Но не все повреждения данных являются непреднамеренными. Что, если кто-нибудь специально предоставит неверные данные в надежде пробить брешь в системе безопасности вашей программы? Думать, как взломщик -- вот следующий шаг в защите кода.

Давайте переключимся обратно на образ мышления атакующего и предположим, что приложение, которое мы атакуем, написано на языке программирования Java, использует сторонний код и хранит в XML все внешние данные, которые тщательно проверяются до передачи приложению. Можно ли по-прежнему успешно атаковать такое приложение? Да, можно. Однако наивный подход по случайному изменению байтов в файле вряд ли приведет к успеху. Вам нужен более изощренный подход, который учитывает встроенные в программу механизмы обнаружения ошибок и обходит их.

При тестировании устойчивого к неверным данным приложения произвести чистое тестирование по методу "черного ящика" нельзя, но, хотя и с некоторыми очевидными модификациями, основные идеи по-прежнему будут работать. Например, рассмотрим контрольные суммы. Если формат файла содержит контрольную сумму, то вы просто модифицируете ее так, что она совпадает со случайными данными еще до того, как файл будет передан приложению.

В случае XML вместо выбора случайного сегмента документа и замены в нем байтов попробуйте изменить содержимое отдельных элементов и значений атрибутов. Обязательно заменяйте данные разрешенными XML-символами, а не случайными байтами, потому что даже сотня байтов случайных данных почти наверняка нарушит формат документа. Также можно поменять названия элементов и атрибутов. Конечно, при этом обязательно надо обеспечить правильность формата итогового документа. Если XML-документ проверяется на соответствие очень ограничивающей схеме, то необходимо выяснить, что не проверяет схема, и определить, где можно продуктивно подменить данные.

По-настоящему ограничительная схема, объединенная с проверкой оставшихся данных на уровне кода, может не оставить вам никакого места для маневра. Это то, чего вы должны добиваться как разработчик. Приложение должно иметь возможность осмысленно обрабатывать любой поток байтов, которые ему посланы и не отброшены как "де-юре" негодные.

Заключение

Тестирование по методу "черного ящика" может обнаружить наличие ошибок в программе. Оно не доказывает, что таких ошибок в программе нет. Однако проведение такого тестирования значительно повышает уверенность в том, что приложение надежно и безопасно по отношению к непредвиденным входным данным. Если тестирование программы по этому методу проводилось в течение 24 часов и она по-прежнему работает, то вряд ли дальнейшие атаки подобного рода вызовут ошибку. (Заметьте: это не невозможно, а просто менее вероятно.) Если тестирование все-таки обнаружило ошибки в программах, их надо исправить. Вместо того, чтобы исправлять случайно обнаруженные ошибки по мере их появления, более продуктивным может быть фундаментальное исправление формата файла на предмет разумного использования контрольных сумм, XML, очистки памяти и/или форматов файлов на базе грамматики.

Тестирование по методу "черного ящика" является важным средством нахождения в программах реальных ошибок. Все программисты, которые настроены на обеспечение безопасности и надежности своих программ, должны пользоваться этим средством.


Ресурсы для скачивания


Похожие темы

  • Оригинал статьи Fuzz testing;
  • В погоне за качеством кода: Хотите больше? Есть целая серия статей developerWorks, посвященная современным методикам тестирования кода;
  • "API для проверки Java XML" (Эллиотт Расти Харольд (Elliotte Rusty Harold), developerWorks, август 2006 г.): Узнайте, что для вас может сделать API для проверки Java 5 XML;
  • "Тестируйте до написания кода" (Гэри Поллайс (Gary Pollice), developerWorks, июнь 2004 г.): Полезный обзор концепций и методов заблаговременного тестирования;
  • "Статьи о Macintosh: Обезьяна жива" (Энди Хертцфельд (Andy Hertzfeld), Фольклор): Описывает тестирование первого компьютера Macintosh по методу "черного ящика" (хотя сам этот термин еще предстояло изобрести заново);
  • "Тестирование надежности приложений по методу "черного ящика"": Исходный Web-сайт, посвященные тестированию по методу "черного ящика";
  • "Искажение XML" (Эллиотт Расти Харольд (Elliotte Rusty Harold), Extreme Markup Languages, 2005 г.): Эта статья представляет инструмент для придания XML-данным случайного характера при одновременном сохранении правильного формата. Целью была помощь по созданию отчетов об ошибках, но те же методы можно использовать для искажения тестовых XML-файлов;
  • Java I/O, глава 12 (Эллиотт Расти Харольд (Elliotte Rusty Harold), O'Reilly, 2006 г.): Обсуждение вычисления контрольных сумм в Java с использованием классов DigestInputStream и DigestOutputStream;
  • Wikipedia: тестирование по методу "черного ящика": Как обычно, прекрасное введение;
  • Исказители: полный список: Различные средства тестирования по методу "черного ящика" с открытым исходным кодом, собранные Джеком Козиолом (Jack Koziol).
  • Зона технологии Java: Сотни статей по каждому аспекту программирования на Java;

Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=185333
ArticleTitle=Тестирование методом "черного ящика"
publish-date=12222006