Мегаобучалка Главная | О нас | Обратная связь


Ошибки в JIТ-компиляторах



2019-05-24 201 Обсуждений (0)
Ошибки в JIТ-компиляторах 0.00 из 5.00 0 оценок




Новое - хорошо забытое старое

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

Уязвимость java-приложений

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

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

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

Допустим, для реализации атаки необходимо задействовать свойства А, В и С. Допустим также, что подобная комбинация свойств практически не используется в обычных программах. Тогда производитель добавляет фильтр, блокирующий их выполнение. Но если представить свойство В в виде прямых (или побочных) эффектов свойств D и Е, то фильтр, очевидно, пропустит такую комбинацию. Отсюда следует важный вывод: даже если производитель рапортует об успешной ликвидации дыры и пользователи уже установили заплаты, это еще не значит, что дыры действительно нет. Исправляется ведь причина, а не следствие. Хакеры просто находят другой путь для достижения того же самого следствия, «воскрешая» дыры, о которых все забыли. Уязвимости, завязанные на спецификацию Java-машины и набор исполняемых ею команд, вообще невозможно залатать без потери совместимости с уже существующими Java-приложениями. В некоторых случаях помогает установка нового Java-компилятора с последующей пересборкой исходных текстов проекта, которых, кстати говоря, у большинства пользователей просто нет. Но даже если бы они и были, эту работу должен выполнять программист, поскольку следует заранее быть готовым к тому, что в тексты придется вносить изменения, иначе программа может отказаться собираться или поведет себя непредсказуемым образом.

Обход системы типов

Жесткая типизация языка Java предотвращает множество непредумышленных ошибок программирования, связанных с присвоением одного типа другому. Защита реализована науровне виртуальной машины, а не в самом языке программирования (как, например, это сделано в Си++). Попытка совершить выход из функции, подсунув инструкции return массив или символьную строку вместо адреса возврата (типичный сценарий хакерской атаки на переполняющиеся буферы), приведет к аварийному завершению Java-приложения. На этом же держится контроль границ массивов, доступ к приватным полям классов и многое другое. Если перехитрить типизацию, можно сломать всю систему безопасности Java и делать практически все что угодно: переполнять буферы, вызывать приватные методы защищенных классов и т. д. Естественно, это уже будут умышленные «ошибки», построенные на слабости механизмов контроля типов. А хакерская атака и есть не что иное, как умышленный отход от спецификации.

Далеко не все знают, что на уровне виртуальной машины Java-платформы защищены намного слабее, чем это следует из популярных руководств для чайников. В спецификации на JVM присутствует большое количество документированных инструкций для низкоуровневого преобразования типов: i2b, i2c, i2d, i2f, i2l, i2s, I2i, I2f, I2d, f2i, f 21, f 2d, d2i, d2l, d2f и даже пара команд для работы с классами: checkcst, instanceof. Их описание выложено на http://mrl.nyu.edu/~meyer/jvmref/ref-Java.html. Более того, в пакете инструментария для разработчика Java-программ содержится официальный код, показывающий, как «правильно» преобразовать один тип к любому другому. Так что в некотором смысле это не взлом, а документированная особенность Java.

Как используется эта особенность на практике? Допустим, мы имеем два класса: trusted (исполняющийся в привилегированном интервале, а значит, владеющий всеми ресурсами виртуальной машины) и untrusted (помещенный в «песочницу»). Допустим также, что класс trusted имеет ряд приватных методов, предназначенных строго для внутреннего использования и скрытых от «внешнего мира» системой типизации. На бумаге эта схема выглядит безупречной, но в действительности ее легко обойти путем создания подложного класса (назовем его spoofed), полностью идентичного данному, но только с public-атрибутами вместо private. Явное преобразование экземпляров класса trusted и spoofed позволит обращаться к защищенным методам, вызывая их не только из других классов, но даже из untrusted-кода. Такой беспредел происходит потому, что в JVM отсутствует runtime-проверка атрибутов полей класса при обращении к ним методами getfield/putfield. А если бы они и были (сжирая дополнительные процессорные такты), злоумышленнику ничего бы не стоило хакнуть атрибуты путем прямой модификации структуры класса двумя замечательными инструкциями JVM — getLong и putLong, специально предназначенными для низкоуровневого взаимодействия с памятью виртуальной Java-машины. Однако в этом случае атакующему придется учитывать версию JVM, поскольку «физическая» структура классов не остается постоянной, а подвержена существенным изменениям. Теперь перейдем к переполняющимся буферам. Как уже было сказано выше, Java контролирует выход за границы массивов на уровне JVM и потому случайно переполнить буфер невозможно. Однако это легко сделать умышленно—достаточно воспользоваться преобразованием типов, искусственно раздвинув границы массива. При записи в буфер JVM отслеживает лишь выход индекса за его границы, но (по соображениям производительности) не проверяет, принадлежит ли записываемая память кому-то еще. Учитывая, что экземпляры классов (они же объекты) располагаются в памяти более или менее последовательно, можно свободно перезаписывать атрибуты, указатели и прочие данные остальных классов (в том числе и доверенных).

Естественно, для реализации данной атаки необходимо написать зловредное Java-приложение и забросить его на целевой компьютер. Атаковать уже установленные приложения не получится, поскольку хакер не в состоянии осуществлять удаленное преобразование типов. Но и тут кое-какие зацепки есть. Некоторые программисты при переносе Си-программ на Java испытывают потребность в работе с указателями на блок «неразборчивых» данных заранее неизвестной длины. Java таких фокусов не позволяет, приходится выкручиваться путем явных преобразований. Фактически это означает, что программист сознательно отказывается от жесткой типизации и говорит Java-машине, что не надо проверять типы, границы массивов и т. п.

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

Обход верификатора

Верификатор относится к одному из самых разрекламированных компонентов JVM. Весь Java-код (в том числе и добавляемый динамически) в обязательном порядке проходит через сложную систему многочисленных фильтров, озабоченных суровыми проверками на предмет корректности исполняемого кода. В частности, если тупо попытаться забросить на вершину стека массив командой aloadj, а потом уйти в возврат из функции по ireturn, верификатор немедленно встрепенется и этот номер не пройдет. Как и любая другая достаточно сложная программа, верификатор несовершенен и подвержен ошибкам. Первую ошибку обнаружил сотрудник Маргбурского университета Карстер Зор (Karsten Sohr) в далеком 1999-м, он обратил внимание на то, что верификатор начинает «буксовать» в случае, если последняя проверяемая инструкция находится внутри обработчика исключений. Что позволяет, в частности, осуществлять «нелегальное» преобразование одного класса к любому другому.

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

Другой интересный момент—атака на отказ в обслуживании. Обычно для проверки метода, состоящего из N инструкций виртуальной машины, верификатору требуется совершить N итераций, в результате чего сложность линейно растет с размером класса. Если же каждая инструкция метода взаимодействует со всеми остальными (например, через стек или как-то еще), то верификатору уже требуется N в квадрате итераций и сложность соответственно возрастает. Подсунув верификатору метод, состоящий из десятков (или даже сотен) тысяч инструкций, взаимодействующих друг с другом, можно ввести его в глубокую задумчивость, выход из которой конструктивно не предусмотрен, и пользователю придется аварийно завершать работу Java-программы вместе с Java-машиной в придачу. Лекарства от данной «болезни» нет. И хотя Sun делает некоторые шаги в этом направлении, пересматривая набор команд JVM и выкидывая из него все «ненужное», сложность анализа байт-кода остается такой же. Особенно эта проблема актуальна для серверов, встраиваемых устройств, сотовых телефонов—там, где снятие зависшей Java-машины невозможно или сопряжено с потерей времени/данных.

Ошибки в 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. Сейчас эта дыра уже закрыта.



2019-05-24 201 Обсуждений (0)
Ошибки в JIТ-компиляторах 0.00 из 5.00 0 оценок









Обсуждение в статье: Ошибки в JIТ-компиляторах

Обсуждений еще не было, будьте первым... ↓↓↓

Отправить сообщение

Популярное:
Как вы ведете себя при стрессе?: Вы можете самостоятельно управлять стрессом! Каждый из нас имеет право и возможность уменьшить его воздействие на нас...
Почему двоичная система счисления так распространена?: Каждая цифра должна быть как-то представлена на физическом носителе...



©2015-2024 megaobuchalka.ru Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. (201)

Почему 1285321 студент выбрали МегаОбучалку...

Система поиска информации

Мобильная версия сайта

Удобная навигация

Нет шокирующей рекламы



(0.008 сек.)