Ошибки в JIТ-компиляторах
Современные процессоры достаточно быстрые, но Java-машины настолько неповоротливы, что способны выполнять только простейшие приложения, не критичные ко времени исполнения, например проверять корректность заполнения Web-форм перед их отправкой на сервер. Попытки создать на Java что-то действительно серьезное наталкиваются на неоправданно низкую производительность JVM, для преодоления которой придумали JIТ-компиляторы (Just-In-Time), транслирующие байт-код непосредственно в «наивный» (native) код целевого процессора, в результате чего Java-программы по скорости выполнения не сильно уступают своим аналогам на Си, а в некоторых случаях даже превосходят их.
Откомпилированный машинный код выполняется с минимумом проверок и верификатор из динамического вырождается в статический. В частности, если произойдет переполнение буфера, то хакер без труда сможет внедрить туда shell-код и передать ему управление, захватив все привилегии виртуальной машины, достаточно часто выполняемой с правами администратора. Отсутствие динамического анализа и скрупулезных проверок времени исполнения (их наличие сильно замедлило бы производительность) позволяет злоумышленнику сравнительно честными путями вырываться за пределы виртуальной машины, вызывая произвольные API-функции операционной системы или даже модифицируя саму виртуальную машину по своему усмотрению. К тому же JIТ-компиляторы при некоторых обстоятельствах сурово ошибаются, генерируя неправильный код. Рассмотрим пример некорректной работы Symantec JIТ-компилятора, используемого, в частности, в браузере Netscape версий 4.0—4.79 под Windows/x86. Байт-код забрасывает на вершину стека нулевую константу (команда aconst_null), после чего вызывает локальную подпрограмму командой jsr 11, где тут же выталкивает двойное слово с вершины стека в виртуальный регистр R1 и возвращается из нее обратно, переходя по адресу, содержащемуся в виртуальном регистре R1 (а в нем как раз и лежит адрес возврата из локальной подпрограммы). Так что с точки зрения верификатора все выглядит предельно корректно и у него никаких претензий нет. Что же касается JIТ-компилятора, то перед входом в функции он сохраняет регистр ЕАХ в стеке (условно соответствующий виртуальному регистру R1), далее обнуляет его (команда XOR ЕАХ.ЕАХ), но не кладет в стек, а прямо так в регистре и оставляет. Потом вызывает локальную подпрограмму (инструкция CALL I1), забрасывая на стек адрес возврата (то есть адрес первой следующей за ней команды — инструкции POP ЕСХ). В самой же подпрограмме компилятор стягивает с вершины стека двойное слово, помещая его в регистр ЕАХ (команда POP ЕАХ), что совершенно правильно. Затем, отрабатывая RET 1, вместо того, чтобы сразу прыгнуть на JMP ЕАХ, по совершенно непонятным причинам еще разлезет в стек и копирует в ЕАХ двойное слово, находящееся на его вершине (инструкция «MOV ЕАХ, [ESP]»), в результате чего реальный переход осуществляется по физическому указателю, находящемуся в регистре ЕАХ. Обычно там собирается мусор и программа (вместе с Java-машиной) просто рушится. При желании можно воздействовать на ЕАХ, засунув в него указатель на shell-код или что-то подобное. Для этого перед вызовом функции jump() достаточно выполнить последовательность команд виртуальной машины: iloadj/ireturn. Сейчас эта дыра уже закрыта.
Повышение собственных привилегий
Несанкционированное повышение привилегий актуально главным образом для Java-приложений, поступающих из ненадежных источников (например, из Сети) и выполняемых в «песочнице» (sandbox), прорыв за пределы которой приводит к плачевным последствиям. Злоумышленник получает возможность исполнять любой код, открывать порты, обращаться к локальным файлам и т. д.
В последних версиях JVM «песочницу» растащили на стройматериалы, ушедшие на создание новой системы безопасности, обеспечивающей разграничение доступа не на уровне Java-приложений (как это было раньше), а на уровне отдельных классов. Доверенные (trusted) классы могут делать все что угодно (если только не оговорено обратное). Остальные довольствуются обращением к публичным методам доверенных классов. Если атакующий сможет добраться до приватных (или защищенных) методов доверенного класса, его цель будет достигнута.
В верификаторе Java-машины, встроенной в MS IE версий 4.0,5.0 и 6.0, присутствовал коварный дефект, позволяющий создавать полностью инициализированные экземпляры классов, даже при возникновении исключения в методе super(). Метод super() похож на указатель this, поддерживаемый Java/ Си++, однако в отличие от this, указывающего на экземпляр данного класса, super() вызывает конструктор суперкласса (или базового класса, если в терминах Си++), к которому принадлежит данный экземпляр производного класса. Узнать подробнее о методах this() и super() можно по ссылке www.laas. org/docs/javap/c5/s5.html. Хорошая идея — взять доверенный класс и создать экземпляр производного класса (sub-класса) и проинициализировать его вызовом super(). Тогда злоумышленник сможет вырваться за пределы «песочницы». Единственная проблема, с которой столкнется атакующий,—Java-компилятор откажется транслировать такой код. Но если
Историческая справка
Java возникла в результате внутреннего проекта Stealth Project (позднее переименованного в Green Project), начатого в 1990 г. компанией Sun. Его целью было создание языка программирования для своей же операционной системы Green Operating System, используемой для управления встраиваемыми устройствами и бытовой электроникой. Идея создания языка принадлежит Патрику Наутону, уставшему программировать микроконтроллеры на Си/Си++, преодолевая несовместимость различных компиляторов вкупе с их привязанностью к конкретному железу. Для «отвязки» от него, он решил сделать эффективную системно-независимую виртуальную машину. Позднее к нему присоединились Джеймс Гослинг (придумавший имя Oak, но оно оказалось уже зарезервированной торговой маркой) и Майк Шеридан. Они завершили создание языка в 1992 г. и продемонстрировали успешную работу Green OS на PDA-компьютере типа Star?.
Что такое enterprise-приложения?
По сложившейся традиции enterprise-приложениями (от английского «enterprise» -предприятие) называются программы, ориентированные на промышленное применение в больших организациях. Соответственно enterprise-серверы -это серверы, обслуживающие предприятия и включающие в себя: web-серверы, серверы печати, базы данных и прочие жизненно важные для функционирования корпоративной сети службы. К ключевым характеристикам enterprise-приложений относят их отказоустойчивость, возможность быстрого восстановления после «падений» и, конечно, безопасность (подробнее - на http://en.wiki pedia. orq/wiki/Enterprise server и http://wiki. debian.org/EnterpriseServer).
Изменения JVM
Структура байт-кода и набор инструкций JVM не остаются постоянными, а меняются от версии к версии, что существенно затрудняет как создание независимых трансляторов от сторонних производителей, так и реализацию атак на байт-код. Хакеру приходится либо фокусироваться на строго определенных версиях (которых может вообще не оказаться у жертвы), либо учитывать особенности каждой отдельно взятой реализации JVM, что весьма непросто. К тому же команды виртуальной машины медленно, но неуклонно движутся к изъятию потенциально опасных инструкций. В частности, из лексикона Java SE 6 исчезли команды JSR и JSR.W, представляющие собой отдаленный аналог Бейсик-команды GOSUB, передающей управление на процедуру. Sun по этому поводу пишет: «Верификатор запрещает выполнение инструкций JSR и RET. Эти инструкции используются для вызова подпрограмм при генерации try/f inally-блоков. Вместо этого компилятор будет встраивать код программ непосредственно по месту вызова». вручную запрограммировать зловредную программу на Java-ассемблере (в качестве которого можно взять бесплатный транслятор Жасмин —jasmin.sf.net), верификатор байт-кода примет ее как родную, поскольку Java-машина, реализованная в IE, выполняет линейный анализ кода, а с этой точки зрения код вполне нормален.
Похожие ошибки содержатся и в других виртуальных машинах. В частности, в Netscape версий 4.0—4.79 вообще можно обойтись без вызовов this() и super(), заменив их ветвлениями (jsr/astore/ret).
Дыры в runtime-библиотеках и системных классах
В конце апреля 2007 г. в Apple QuickTime Player'e всех версий, вплоть до 7.1.5, обнаружилась дырка, позволяющая Java-приложениям исполнять произвольный код на удаленной системе. Достаточно зайти на Web-страничку злоумышленника... Учитывая огромную распространенность Apple QuickTime Player'a и Microsoft Internet Explorer'a, произошло своеобразное перекрестное «опыление», в результате чего пострадали сразу обе системы: вся линейка Windows NT (включая Vista) и Mac OS.
Но сама Java-машина тут не при чем. Ошибка сидит во внешнем (по отношению к ней компоненте), и потому уязвимость распространяется не только на IE, но и FireFox.
Exploit, пробивающий практически любую Java-машину, при наличии установленного Apple QuickTime Player'a с версией 7.1.5 или более ранней // инициализирует Quick-Time QTSession.openO;
// получает обработчик, указывающий на что угодно
byte b[] = new byte[l Л здесь может быть любое число V];
QTHandle h = new QTHandle(b);
// превращает обработчик в указатель на объект
// огромное отрицательное значение обходит проверку диапазона QTPointerRef p = h.toQTPointer(-2000000000 /* смещение */, 10 /* размер */);
II перезаписывает объект p.copyFromArray(0 Л смещение V, b Л источник V, О, 1 /* длина V),'
Этот пример наглядно доказывает, что говорить о безопасности Java в отрыве от надежности всех остальных компонентов операционной системы и ее окружения— наивно. Java должна либо быть «вещью в себе» и не допускать никаких внешних вызовов (так вели себя некоторые диалекты Бейсика на 8-разрядных компьютерах, из лек-сикона которых были исключены операторы CALL, PEEK и РОКЕ), либо открыто признать, что на шатком фундаменте крепости не построишь и доверять Java-приложениям даже с учетом всей многоуровневой системы безопасности на 100% нельзя. Впрочем, отказ от внешних вызовов ничего не решает, поскольку системные библиотеки, поставляемые вместе с Java-машиной, ничем не лучше прочих компонентов и могут содержать различные дефекты проектирования. Хотите пример? В начале 2007 г. в Sun JRE 5.0 Update 9 (включая и более ранние версии) была обнаружена ошибка, связанная с обработчиком заголовков GIF-файлов и содержащая уязвимость, которая приводила к возможности передачи управления на shell-код, расположенный непосредственно в самом GIF-файле. Технические подробности можно найти нa www.securityfocus. com/archive/1/457159. а код exploit'a есть нa www.securitvfocus.сom/archive/1/457638. Для нас же важен сам факт небезупречной реализации Java-машины. Получается, что в то время как теоретики от программирования старательно выводят запутанные диаграммы, иллюстрирующие «продвинутую» модель безопасности Java с многоуровневой системой защиты, хакеры дизассемблируют библиотечные файлы на предмет поиска реальных уязвимостей.