Смекни!
smekni.com

Небезопасная безопасная JAVA (стр. 1 из 2)

Крис Касперски

Безопасность java-технологий оказалась решающим аргументом при продвижении в сферу корпоративных enterprise-приложений с конкурентом в лице с#. Однако пограничная полоса, отделяющая рекламный маркетинг от реальной жизни, оказалась довольно тернистой. Java многое обещает, но каждый раз откладывает исполнение своих обещаний на неопределенный срок. Рассмотрим модель безопасности java на макро-, микроуровнях и проанализируем сильные и слабые стороны этой технологии.

В 1994 г. порядком разросшийся коллектив программистов предпринял попытку проникновения на рынок Web-приложений. Они сфокусировались на вопросах безопасности и подготовили специальную редакцию языка HotJava (в «девичестве» WebRunner), предназначенную для встраивания в браузеры и ставшую доступной для публичного скачивания в 1995-м. Попытка оказалась успешной. И с момента поддержки HotJava браузером Netscape Web-серфинг перестал быть безопасным, а сам браузер превратился в один из основных объектов хакерских атак. Несмотря на это, Java просочилась практически во все сферы рынка и сегодня встречается повсеместно: от сотовых телефонов до enterprise-серверов и суперкомпьютеров.

Внедрение Java-технологий обычно происходит под эгидой лозунгов о повышении безопасности, а все дыры списываются на ошибки реализации конкретных виртуальных машин. Однако истинная причина в том, что Java уязвима на концептуальном уровне. Впрочем, у конкурентов дела обстоят не лучше и основной соперник Java—C# содержит еще больше лазеек, порой умышленно привнесенных разработчиками для достижения большей производительности в ущерб безопасности. Безопасность — весьма абстрактное понятие, не поддающееся измерению и не имеющее числового представления, в то время как тесты производительности — мощное маркетинговое средство. Разумеется, это еще не означает, что Java и С# должны быть с позором выброшены на свалку истории. Достаточно знать пути достижения безопасности и иметь в виду ловушки, которые подстерегают на пути.

Многоликая Java

Прежде чем приступать к обсуждению системы безопасности Java-приложений, проведем водораздел между Java-технологиями и одноименным языком программирования, с которым, собственно говоря, и ассоциируется торговая марка Java. Общеизвестно, что Java является интерпретируемым языком, но он существенно отличается от большинства других интерпретируемых языков, таких, например, как Perl, PHP или Python. Если в Реrl'е интерпретации подвергается непосредственно сам исходный код, то программа, написанная на Java, транслируется в байт-код, исполняемый на виртуальной Java-машине (далее—JVM).

Согласно терминологии, предложенной компанией Sun, реализатор JVM называется клиентом, и всякий клиент вправе исполнять байт-код так, как ему заблагорассудится (естественно, в рамках спецификации JVM). Наряду с программными реализациями JVM существуют и аппаратные, демонстрирующие производительность, ничуть не уступающую (а зачастую даже превосходящую в силу грамотной оптимизации байт-кода JVM) чистому машинному коду с процессором семейства х86, Alpha и др. С другой стороны, большую популярность завоевали JIТ-компиляторы (Just-ln-Time-компиляторы), на лету транслирующие байт-код в «нативный» (native) машинный код соответствующего процессора и сочетающие высокое быстродействие с дешевизной реализации.

Таким образом, Java-приложения представляют собой двоичные файлы, не имеющие ничего общего с исходными текстами, составленными на языке Java. Байт-код виртуальной машины предоставляет довольно богатый набор инструкций, описанный в спецификациях на JVM, что позволяет сторонним разработчикам создавать свои собственные трансляторы, работающие с отличными от Java языками программирования. Так, уже появились и завоевали популярность Жасмин (Java-ассемблер), Ephedra (компилятор, транслирующий Си/Си++ программы в байт-код JVM), Component Pascal (компилятор, транслирующий Pascal и Oberon программы в байт-код JVM). Имеются трансляторы и для других языков: Ада, Бейсик, Форт, Кобол и т. д.

Java представляет собой объективно-ориентированный язык программирования со строгим контролем типов, выполняемым на уровне JVM, что обеспечивает защиту как от «нечестных» трансляторов (не придерживающихся оригинальной спецификации), так и от прямой модификации байт-кода в hex-редакторе. В то же самое время такой подход существенно затрудняет трансляцию Си-программ, известных своим нецензурным кастингом (от английского «to cast» — явное преобразование типов) и вольным обращением с указателями. Говоря о безопасности Java, главным образом сосредоточимся нa JVM, поскольку системы контроля, встроенные непосредственно в язык программирования Java, работают лишь на стадии трансляции, страхуя Java-программистов от непредумышленных ошибок, но не спасающих от целенаправленной атаки на байт-код.

Концептуальная модель безопасности

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

Изобилие переполняющихся буферов в Си-программах (приводящих к возможности удаленного захвата управления системой) носит фундаментальный характер, обусловленный природой самого языка программирования. Си поддерживает массивы лишь формально, и реально программистам приходится работать не с массивами, а с указателями на области памяти неизвестной длины. Язык не выполняет никакого контроля границ буферов, всецело полагаясь на программистов, а тем, как известно, свойственно ошибаться. Именно поэтому принципиальная возможность создания безопасных программ на Си практически никогда не достигается в конкретных реализациях, зачастую создаваемых в жестких временных рамках и протестированных на уровне «если запускается и не падает, значит работает». Еще ни один крупный проект, написанный на Си/Си++, не избежал ошибок проектирования. Достаточно взять SendMail или IE и подсчитать количество дыр, обнаруженных за время их существования.

Java в этом смысле выглядит весьма заманчиво. Встроенный контроль типов снимает с программиста бремя постоянных проверок границ массивов, делая их переполнение достаточно маловероятным событием. Автоматический сборщик мусора снижает актуальность проблемы утечек ресурсов и появления «висячих» указателей, хотя это достается дорогой ценой — снижением производительности и невозможностью создавать приложения реального времени. К тому же подчистка мусора освобождает лишь ресурсы, уходящие из области видимости, но не способна предотвратить «локальные» утечки памяти, которые сплошь и рядом рискуют обернуться глобальными. Достаточно, например, выделять память в бесконечном цикле вплоть до полного ее исчерпания. Возьмем для наглядности FireFox, существенная часть которого написана с использованием Java, и сравним его с Оперой, реализованной на Си++. Лавинообразный рост дыр, обнаруживаемых в FireFox'e убедительно доказывает, что Java сам по себе от ошибок проектирования никак не спасает. Надежность программы в первую очередь зависит от профессионализма ее создателей, а уже потом от свойств выбранного языка программирования. Создавать надежную программу можно и на Си++, Опера—лучшее тому подтверждение. Это не только один из самых быстрых, но и один из самых надежных браузеров на сегодняшний день. Складывается парадоксальная ситуация. При всей ненадежности языка Си/Си++, написанные на нем программы, как правило, намного надежнее своих Java-собратьев, хотя по логике все должно быть наоборот. Причина в том, что большинство старых (и опытных) программистов, освоивших Си/ Си++, не видят никаких причин для перехода на Java-платформу, преимущественно выбираемую молодыми (более неопытными) программистами. И тот факт, что приложение написано на Java, еще не гарантирует его надежности.

Но оставим непредумышленные ошибки в стороне и перейдем к анализу целенаправленных атак на байт-код.

Микроуровень JVM представляет собой виртуальную машину со встроенным контролем типов, прямым аналогом которой являются «железные» процессоры с теговой архитектурой (например, наш отечественный Эльбрус) — «заветная» мечта теоретиков от программирования, абстрагирующихся от реальных концепций. На макроуровне, действительно, можно работать с объектами, не задумываясь об их внутреннем представлении, но на микроуровне неизбежно приходится сталкиваться с физическими ограничениями объективно-ориентированного подхода. Для достижения приемлемой эффективности в исполнительную машину приходится включать «нечестные» механизмы, работающие в обход обозначенной системы типов. Применительно к JVM—это прямые вызовы машинного кода и класс sun.misc.Unsafe, реализующий небезопасные методы работы с памятью — getLong (чтение двойного слова из памяти по заданному адресу) и putLong (запись двойного слова в память по заданному адресу).

Начнем с прямого вызова машинного кода, являющегося документированной особенностью JVM, во всяком случае в ее реализациях от Sun вплоть до версии 1.5.6 (начиная с 1.5.6 возможность вызова машинного кода как будто бы исключена и информацию приходится добывать путем «обратного проектирования»). С каждым методом класса связана специальная структура, одним из полей которой является указатель на машинный код (точнее, псевдоуказатель). Если он равен нулю, то выполняется «родной» байт-код, расположенный в хвосте структуры, в противном случае управление передается по псведоуказателю. Изначально этот механизм задумывался для вызова внутренних RTL-функций, критичных к производительности, и для компиляции в память JIТ-трансляторами.

Получается, что в Java изначально присутствовала дыра в безопасности. Ведь любой злоумышленник запросто может внедрить в байт-код настоящий машинный код, делающий все что угодно. На самом деле в Sun вовсе не дураки сидят: перед запуском Java-приложения среда исполнения тщательно проверяет байт-код, отбрасывая пользовательские классы с ненулевым указателем. Динамическая проверка менее щепетильна, и, хотя непосредственная модификация указателя на машинный код посредством метода putLong в большинстве случаев отлавливается средой исполнения, байт-код, откомпилированный в память, может беспрепятственно «хачить» указатели по своему усмотрению. И среда исполнения оказывается не в состоянии отличить «честную» модификацию указателя, выполненную JIТ-компилятором, от «нечестной».