Впрочем, даже не прибегая к машинному коду с одними лишь методами getLong/ putLong, можно существенно пошатнуть модель безопасности Java, произвольным образом модифицируя внутренние данные классов и меняя типы переменных вместе с атрибутами классов (public, final и т. п.). Что позволяет реализовать тот самый «нецензурный кастинг», приводящий к ошибкам переполнения (к умышленным, разумеется) и возможности удаленного захвата управления машиной с передачей управления на shell-код (только для функций, откомпилированных в память). Важно понять, что методы getLong/putLong являются не функциями, поставляемыми вместе с библиотекой времени исполнения, а командами JVM. То есть заблокировать их вызов напрямую не получится, а если бы и получилось, многие штатные библиотеки тут же бы отказали в работе. Вообразить набор инструкций исполнительной машины без возможности низкоуровневой работы с отдельными ячейками памяти — нельзя! А раз так, у нас есть все основания полагать, что инструкции getLong/ putLong—это надолго (если не навсегда) и потому следует присмотреться к ним повнимательнее.
Макроуровень
Начиная с версии 1.0, в JVM появилась метафора «песочницы» (sandbox) — изолированной среды, в которую помещаются потенциально опасные программы (например, Web-приложения, полученные из ненадежных узлов). Песочница как бы отрезана от файловой системы и может общаться только с тем узлом, с которого было загружено данное Java-приложение. «Как бы» — потому что не существует ни одной реализации JVM, отвечающей этому требованию не только на бумаге. Ряд атак на IE и FireFox как раз основан на возможности прорыва за пределы «песочницы» и перезаписи локальных файлов.
Решение проблемы заключается в запуске IE/FireFox от имени пользователя, которому недоступны никакие файлы, кроме тех, что требуются для работы браузера. Тем не менее атакующему по-прежнему доступны cookies, кэш страниц и другие данные, утечка которых крайне нежелательна, а в некоторых случаях недопустима и влечет к потере контроля над своими аккаунтами. Поэтому многие компании отказываются от Java, запрещая выполнение Java-приложений в браузере.
Хуже с Java-приложениями, находящимися на локальном диске. Они по умолчанию считаются безопасными и им доступны все ресурсы JMV, в том числе файлы, сетевые соединения и т. д. Создание компьютерного вируса на Java не только возможно, но и не сильно отличается от создания вирусов, написанных на остальных языках программирования (Си, Паскале, Ассемблере). Сказанное относится и к Web-страничкам, сохраненным на диск. При последующем открытии они уже считаются «безопасными» со всеми вытекающими отсюда последствиями. То есть для успешной атаки злоумышленнику достаточно заманить жертву на страницу с вредоносным Java-приложением и мотивировать сохранить данные на диск для последующего запуска. Вообще-то, при желании настройки браузералег-ко изменить, но тогда перестанут работать и все действительно безопасные приложения, нуждающиеся в доступе к файлам/сетевым соединениям.
Осознавая ущербность предложенной модели безопасности, компания Sun уже в JVM 1.1 ввела поддержку электронной подписи, благодаря которой вредоносный код потерял все шансы. Но опять только на бумаге, а в реальной жизни... Чтобы не терять совместимость с уже написанным программным обеспечением, проверка цифровой подписи по умолчанию была либо выключена, либо при загрузке неподписанного Java-приложения выдавала запрос на подтверждение, на который большинство пользователей отвечало утвердительно. Java-скриптов цифровая подпись вообще никак не коснулась, и при открытии сохраненной Web-страницы с диска они по-прежнему получали все права.
В следующей версии виртуальной машины политика безопасности была пересмотрена и существенно расширена. Деление на «ненадежные» и «надежные» приложения исчезло, уступив место правам доступа. Теперь приложения могли обращаться только к определенному перечню ресурсов, задаваемому администратором системы, что само по себе огромный прогресс, поскольку концепция «все или ничего» (т. е. «песочница» или «живая» среда) оказалась крайне негибкой. Трудно представить себе полновесное приложение, довольствующееся «песочницей». С другой стороны, если делегировать всем Java-приложениям права доступа ко всем ресурсам, то о какой «безопасности» может идти речь.
Введение селективного контроля за ресурсами потребовало реализации «контекста выполнения»— проверяя права доступа объекта к ресурсу, JVM вынуждена анализировать не только данный объект, но и предыдущие элементы стека вызовов, предоставляя доступ тогда и только тогда, когда нужным правом владеют все объекты в стеке (в терминологии Sun это называется принципом минимизации привилегий). Принцип минимизации привилегий, как легко видеть, вступает в противоречие с принципом инкапсуляции. Объект too, опирающийся на объект bar, в «правильных» ООП-языках не знает о внутреннем устройстве bar, и потому bar может (при необходимости) пользоваться ресурсами, недоступными для too. Классическим примером тому является системный вызов операционной системы, осуществляемый прикладной программой. Объект «файл», имеющий прямой доступ к диску, предоставляет остальным объектам набор методов для создания/удаления/чтения и записи файлов, гарантируя, что никакой другой объект не разрушит данные на диске. Если же следовать принципу минимизации привилегий, то прямой доступ к диску необходимо предоставить всем объектам, что абсурдно. Другими словами, если объектно имеет право вызывать данный метод объекта bar с заданными аргументами, то bar обязан обслужить вызов, в противном случае пришлось бы учитывать возможный граф вызовов объектов, что требует огромных затрат памяти и процессорного времени. Механизм, реализованный в JVM, обходит эту проблему путем создания привилегированного интервала, при выполнении которого контекст (т. е. предыдущие вызовы объектов) игнорируется, в результате чего становится возможным появление программ, нарушающих делегированные им права доступа (не важно, сознательно или нет). На это еще можно было бы закрыть глаза, если бы не тяжеловесность реализации и высокие накладные расходы. Как женщина не может быть «слегка» беременной, так и система не бывает «практически» безопасной.
Заключение
Несмотря на недостатки, присущие Java, следует признать, что подобного уровня защищенности на сегодняшний день не обеспечивает ни один из языков программирования. Можно долго критиковать Java, но новых инструментов от этого не прибавится. Тем не менее пользователям всегда следует помнить, что выбор программного обеспечения должен осуществляться на основании реальных данных об их надежности, а не только потому, что они написаны на Java. Программистам же не следует забывать о том, что Java лишь уменьшает вероятность появления некоторых ошибок проектирования, а не исключает их.
Верификатор
Верификатор байт-кода - неотъемлемая часть JVM, он проверяет каждую выполняемую инструкцию виртуальной машины (в том числе и добавленную динамически) для предотвращения поступления заведомо некорректной информации. Считается, что верификатор предотвращает следующие операции:
- подделка указателей, например, получение указателя как результат выполнения арифметической операции (хотя инструкции getLong/putLong позволяют обращаться к любой ячейке памяти и верификатор им не помеха);
- нарушение прав доступа к компонентам классов, в частности, присваивание переменной, снабженной описателем final (инструкции gettong/putLong без труда обходят это ограничение);
- использование объектов в каком-либо другом качестве, например, применение к объекту метода другого класса (инструкции getLong/putLong позволяют обойти систему контроля типов). Фактически, верификатор решает более скромные задачи, препятствуя:
- вызову методов объектов с недопустимым набором параметров;
- вызову инструкций JVM с недопустимым набором параметров;
- некорректной операции с JVM-регистрами;
- переполнению или исчерпанию стека. Для достижения максимальной производительности верификатор совершает ряд допущений, смягчая проверку «мертвого» кода (кода, который по мнению верификатора никогда не получит управление). А реализация верификатора в JIТ-компиляторах из динамической (выполняемой на каждом шаге) и вовсе вырождается в статическую (выполняемую до компиляции). В частности, проверка границ массивов, отнимающая много времени, опускается всякий раз, когда JIТ-компилятор считает, что нарушения доступа на данном участке кода заведомо не происходит. Сравнивая реализации JVM от Sun и Microsoft, нельзя не заметить, что реализация Microsoft работает существенно быстрее, а реализация Sun выполняет намного больше проверок. А реализация JVM корпорации IBM обеспечивает достаточно высокую производительность без существенного ущерба для безопасности.
Список литературы
IT спец № 07 ИЮЛЬ 2007