JIT SPRAY мертв! Да здравствует JIT SPRAY!Рабочий эксплойт за 6 секундИ вновь на страницах нашего журнала тема JIT SPRAY. В этот раз мы не будем использовать Flash-плеер и ActionScript, а рассмотрим возможности браузера Safari и его JIT-движка для JavaScript. Покажем, как в течение 6 секунд можно обойти DEP и ASLR в Windows 7 и проэксплуатировать уязвимость в самом браузере или его плагине. Если не надоело...Итак, почему же мы вновь вернулись к этой теме? Дело в том, что при подготовке к прошедшей в прошлом месяце конференции «Hack In The Box» в Амстердаме и ускорении времени работы JIT SPRAY (с 8 минут до 6 секунд) с помощью модифицированного EGG-HUNTER шеллкода (про этот вид шеллкода можно было прочитать в статье Алексея Тюрина) в контексте компилятора JIT выяснилось, что в новом релизе Flashплеера версии 10.1 изменилась модель компилятора, и JIT SPRAY больше не работает. За три недели до начала конференции мне надо было что-то показать коллегам, чтобы не ехать в страну мельниц и тюльпанов совсем уж с баяном, отсюда и этот материал, но начнем по порядку... Зачем?Очень большая часть темного бизнеса держится на уязвимостях в браузерах и плагинах к нему. Но с ростом популярности Windows 7 возникает проблема грамотной эксплуатации уязвимостей, так как защитные механизмы DEP (предотвращает выполнение кода из неисполняемой области памяти) и ASLR (делает адреса модулей и библиотек случайными) делают классические методы вроде HEAP SPRAY неэффективными. Тем не менее, исследователи показывают слабости в этих механизмах, дабы разработчики не расслаблялись и знали, что этого недостаточно. Благодаря работе Диониса, показавшего JIT SPRAY в Flash-плеере до того, как это сделали черные шляпы, Adobe смогла исправить код JITкомпилятора и тем самым снизила уровень угрозы для своих пользователей. Так что же такое JIT? JUST-IN-TIMEJust-In-Time компилятор преобразует код, написанный на языке высокого уровня, в машинный код, и сохраняет этот код в оперативной памяти, чтобы приложение смогло исполнить его, так сказать, на лету. Алгоритмы JITкомпилятора могут быть разными, и разные JIT-компиляторы работают поразному (прости за тавтологию :). Дионис Блазакис в своей работе нашел, что компилятор для ActionScript (скриптовый язык для Flash-приложений) сохраняет значения в памяти как есть. Мне же удалось обнаружить аналогичную ситуацию в компиляторе JavaScript в браузере Safari для Windows. Напишем простейший код на JavaScript, выполняющий операцию XOR:
Чтобы высчитать значение Y, движок JavaScript в браузере должен посчитать XOR, поэтому в памяти процесса создается страница, и туда заносится скомпилированный JIT-код, которому, в свою очередь, передается управление при вызове функции jit_(): Данный код в памяти появляется сразу после вызова jit_(). Первая колонка — адрес, вторая — машинные коды, третья — код на ассемблере (все значения в HEX’ах). Как видно, аргументы оператора XOR передаются в память как есть, сама память помечена как доступная для чтения, записи и исполнения — RWX (в Flash было только RX). Но главное разочарование — аргументы в памяти находятся далеко друг от друга, на расстоянии 20 байт. В Flash-плеере же разница в один байт. Итак, как это можно использовать для обхода DEP и ASLR? Для начала выберем уязвимость, на которой будем проводить опыты. УязвимостьПовторное использование освобожденной памяти в Safari 4.0.5. Эту уязвимость я уже описывал в одном из прошлых обзорах, суть ее проста — удаляем родительский объект, при этом сохраняя указатель на него, потом вызываем метод, используя сохраненный указатель. В итоге берется значение из поврежденной памяти (которая освободилась). При этом значение указателя из таблицы vtable оказывается переписанным аргументом метода. Так мы захватываем контроль, а именно — регистр EIP:
JIT SPRAYНу вот, мы имеем уязвимость и можем передавать управление по любому адресу. Обычно управление передают туда, где лежит шеллкод. Для этого раньше память заполняли большими блоками в памяти (в куче) с шеллкодом. Если таких кусков очень много, то угадать адрес было легко. Только вот с появлением DEP все это бесполезно, так как шеллкод из кучи не будет исполняться. Методика обратно-ориентированного программирования также неприменима, потому что все модули поддерживают ASLR, и адреса нужных инструкций нам неизвестны. Для таких безвыходных ситуаций применим возможности JIT-компилятора, которые мы описали выше. Если написать несколько сотен функций jit_() и выполнить их, то память процесса заполнится блоками памяти с JIT-кодом. Память эта исполняема, но как угадать ее адрес? Если XOR-строка будет достаточно большой, память для каждого JIT-блока будет выделяться по адресу с предсказуемыми младшими разрядами в адресе — 0x0000. Это значит, что по адресу 0xXXYY0000 будет начинаться скомпилированный JIT-блок с нашими аргументами. Если таких блоков будет достаточно много, то 0xXXYY мы можем легко угадать, как в аналогии с Heap Spray. Но почему же не работает ASLR? Почему 0xXXYY не такие случайные? На самом деле ASLR работает, но только когда Safari пытается выделить первый блок для самой первой jit_() функции. Все последующие выделения памяти идут последовательно друг за другом. Это позволяет нам заполнить с вероятностью 99% середину памяти процесса, скажем, адреса 0x0606000, 0x07070000 и т.д. (см. рисунок). Если каждый JIT-блок будет размером меньше чем 0x010000, тогда память будет расти равномерно, то есть, к примеру, адрес блока N есть 0x06060000, тогда адрес блока N+1 будет 0x06070000 и т.д. Осталось только определить младшие разряды адреса, чтобы указывать в аккурат на наш аргумент. Тут тоже все просто, так как смещение от начала блока до начала кода с XOR всегда постоянное. Дабы избежать нулевых байт в адресе ищем первый аргумент, в младших разрядах адреса не будет нулевых байтов, и первый такой адрес оказался 0x0104 со значением десятого аргумента для XOR. В итоге окончательный указатель будет 0xXXYY0104, где XXYY могут быть любым значением из центральной части карты памяти, например 0x0607 или 0x0808 и т.д. Таким образом, ASLR побежден. Инъекция кодаДопустим, мы передали управление по адресу 0x07070104, там у нас значение 10-го аргумента из XOR-строки. Предположим, десятый параметр для XOR равен 0x01020304, а одиннадцатый — 0x1a1b1c1d, тогда по адресу 0x07070104 будет следующий код: Так как у нас LITTLE-ENDIAN система, то процессор берет значения «задом наперед», и вместо 0x0304 мы можем написать любые команды в оп-кодах. А вот 0x0201 мы должны заменить строго на 14EB (EB14 = JMP +0x14), чтобы передать управление на одиннадцатый параметр. Когда мы говорили про Flash, было легче, так как там параметры шли один за другим, тут же у нас разрыв в 20 (0x14) байт, поэтому последние два байта надо «тратить» на переход между параметрами. К примеру, десятый и одиннадцатый параметр: Будут скомпилированы в: У нас получилась связь между параметрами и выполнение кода, сначала пустые операторы, а затем прерывание. Итак, DEP мы тоже обошли... JIT-шеллкодТеперь нам надо написать связанный шеллкод, который мы будем внедрять в память с помощью XOR в JavaScript. Дело кажется очень трудным, так как у нас есть всего два байта на команду. В Flash мы могли использовать и трех- и пятибайтные команды, но тут у нас жесткое ограничение. Кажется, что дело — труба, но вспомним еще одно важное отличие между JIT SPRAY в Flash и Safari — права на память. В Flash они были «R-X» (чтение и исполнение), а вот в Safari — «RWX», то есть мы можем еще и писать в память. Никогда не оставляй исполняемую память доступной на запись, ибо этот, казалось бы, маленький недочет делает возможным внедрение шеллкода. Поясню: мы заполнили память RWXблоками и, допустим, передали управление на JIT-блок 07070000. Там у нас будет связанный шеллкод с командами по два байта. Двух байт вполне достаточно для операции копирования — MOV [ECX], EAX (0x8901 в оп-кодах). Если ECX будет указывать на следующую страницу — 0x07080000, а в EAX будут содержаться первые четыре байта от шеллкода из метасплойта, то произойдет копирование шеллкода в RWX-память (W позволит нам делать это). Когда таким образом мы скопируем весь метасплойт-шеллкод кусками по четыре байта через регистр EAX, по адресу 0x0708000 будет лежать боевой шеллкод, причем в исполняемой памяти. Далее делаем JMP ECX (0xFFE1 — также два байта) на эту память, и код оттуда исполнится без лишних вопросов. Два байта болиМы умеем копировать и передавать управление с помощью регистров. Двух байт для этого достаточно, но прежде всего надо научиться с помощью этих двух байт заносить произвольные значения в регистры. MOV REG, VALUE занимает 5 байт. С помощью двух байт мы можем заносить значения только в младшие разряды регистра. Для этого будем использовать MOV AL, 01 (0xB001) и MOV AH, 02 (0xB402), так мы занесли в младшие разряды EAX 0x0201, и регистр в общем содержит 0xXXYY0201. Как же можно занести значение в старшие разряды? Первое, что мне пришло в голову — это побитовый сдвиг влево — SHL EAX, 1 (0xD1E0). Если мы хотим занести в регистр ECX-адрес блока, куда хотим копировать шеллкод, то заносим сначала в младшие регистры значение старших, потом делаем шестнадцать операций сдвига влево... и в итоге значения младших разрядов сдвигаются на место старших. Потом заносим еще раз уже значения младших разрядов. План хорош, да оказалось, что не очень — дело в том, что шеллкод получается очень большой, и из-за этого долго компилируется JIT-компилятором, но это не главное. Хуже всего то, что размер блока с компилированным кодом становится больше 0x10000 байт, а это значит, что блоки идут друг от друга с двойным разрывом: адрес блока N есть 0x06060000, тогда адрес блока N+1 будет 0x06080000 (а должно быть 0x06070000). Таким образом, работоспособность JIT SPRAY зависит от четности третьего разряда. Короче говоря, стабильность работы — 50%. Очевидно, что проблема в шестнадцати операциях сдвига на каждые четыре байта копируемого шеллкода. Надо искать другой путь. Можно, например, заменить все сдвиги на одну операцию умножения. То есть 0x0000ABCD*0x10000=0xABCD0000. Значит, можно написать JIT-шеллкод, который заносит произвольные значения в регистры ECX и EAX, а потом копирует по адресу ECX значение EAX. Затем добавляем к ECX четыре байта и заносим в EAX следующую часть метасплойтовского шеллкода. АвтоматизацияТеперь пора написать генератор шеллкода, который согласно вышеупомянутому алгоритму будет генерить JIT-шеллкод. Начнем труд! В метасплойте генерируем шеллкод в формате Perl. Я выбрал запуск калькулятора, без всяких кодировок шеллкода. Нам это ни к чему, во-первых, потому что JIT-шеллкод и так неслабо перекодирует оригинальный шеллкод — ни один антивирус не узнает. Во-вторых, размер кода меньше. Итак, есть шеллкод в формате Perl, который мы запихнем в переменную $shellcode. Зададим старшие байты страницы, куда будем копировать этот шеллкод. Я выбрал 0x080A0000. Так как младшие байты нас не интересуют, задаю только старшие:
Поскольку весь шеллкод копируем по четыре байта, необходимо выровнять его, для этого считаем размер шеллкода и делим с остатком на 4. Если остаток 1, 2 или 3, то добавляем к концу шеллкода мусора: Забудем на время про шеллкод, необходима подготовка; как мы помним, для того, чтобы 0xXXYY0104 указывал на начало кода, необходимо, чтобы JIT-код начинался с десятого аргумента. Поэтому прежде всего забьем первые девять аргументов:
Как видишь, я задаю XOR в виде строки для JavaScript, поэтому добавляю зафильтрованные кавычки и символ +, что есть конкатенация для строк в JavaScript. Потом я просто выполняю eval() для этой строки, и JavaScript поймет, что эту строку следует скомпилировать. После сдвига до десятого параметра необходимо начать подготовку к копированию. Для начала надо иметь в регистре ESI множитель 0x10000 для того, чтобы потом можно было быстро переносить значения из младших разрядов в старшие:
Параметр идет задом наперед, чтобы потом правильно интерпретироваться процессором. То есть, аргумент для XOR 0x14EBC031 и есть наш десятый параметр. При перехвате контроля мы перепишем EIP 0x07070104, что будет указывать прямо на 0x31C0EB14. 0x31С0 — это «XOR EAX, EAX». Так мы обнулим регистр, а следующая команда — 0xEB14 — сделает JMP +14 байт на следующий параметр — 0x14EB01B4. Читая задом наперед, получаем «MOV AH, 01 / JMP 14». То есть, в регистр AH заносится единица, а далее — прыжок на следующий параметр, там уже обнуляется AL. Таким образом, EAX=00000100. Тринадцатый параметр делает «MUL EAX», то есть 0x100 умножает на 0x100 и в EAX заносится результат — 0x00010000. Далее опять прыжок, и в четырнадцатом параметре происходит копирование EAX в ESI. Теперь в ESI у нас требуемый множитель. Далее необходимо, чтобы ECX указывал на память, куда мы копируем шеллкод. Он у нас в переменной $addr.
Этот код генерирует пятнадцатый и шестнадцатый параметр, занося первый и второй разряд $addr в регистры AH и AL. Выходит, что EAX = 0001080A. Единица в третьем разряде осталась после предыдущих операций, но она нам не мешает. Теперь переносим значения из младших разрядов EAX в старшие и копируем значение в ECX:
Понятно, что значение 0xEB14 всегда будет в наших параметрах, чтобы передать управление следующим. Теперь у нас в ECX адрес 080A0000, осталось занести в EBX шаг копирования — четыре байта: На этом подготовка завершена. В ECX — указатель на страницу памяти, куда будем копировать шеллкод, в ESI — множитель, в EBX — шаг сдвига. Начнем копировать шеллкод. Сначала скопируем байты задом наперед.
Так вот, мы копируем байты в переменные $byteX. Потом можно удобно заносить их в EAX:
Сначала обнуляем EAX, потом копируем $byte1 в AH, а $byte2 в AL. После чего делаем MUL ESI, другими словами, EAX=EAX*ESI. В ESI у нас множитель, который сделает так, что $byte1 и $byte2 окажутся на месте старших разрядов регистра EAX. После этого заносим в младшие разряды EAX (регистры AH и AL) $byte3 и $byte4. После этого мы имеем в регистре EAX четыре байта из шеллкода. Выполняем копирование в исполняемую память:
По указателю ECX (0x080A0000) копируем первые четыре байта шеллкода. Затем увеличиваем ECX на EBX, то есть на четыре. Так как у нас цикл по всей длине шеллкода, разбитой на 4 байта, то на второй итерации в EAX будут уже следующие четыре байта шеллкода, а ECX уже увеличен размер шага, и будет увеличиваться в конце каждого цикла. Все это (в переменной $copyJit) обеспечит нам копирование шеллкода в память по адресу 0x080A0000 (из переменной $addr). После акции копирования надо передать управление:
Просто обнуляем младшие разряды ECX (CH и CL), в итоге ECX у нас опять равен 0x080A0000, и делаем JMP ECX, после чего должен исполняться код по адресу ECX, а там, как известно, шеллкод из метасплойта. Соберем же весь конструктор, для этого в переменную $page запишем начало генерируемой HTML-странички с эксплойтом:
Напомню, что при заносе в переменную кавычки экранируем. В общем, мы объявили переменную SPRAY и JIT. В JIT мы занесли открытие блока и переменную Y. Далее сделаем конкатенацию переменной $page и нашего сгенерированного JIT-кода: $offsetJi t.$initJit.$copyJit.$jumJit. Таким вот образом мы как бы занесли в переменную Y для JavaScript весь JIT-шеллкод, который заранее генерировали в Perl’е. Собственно, закрываем переменную и блок в переменной $endPage:
В конце блока мы еще добавили «return y;», так как этот блок являет ся кодом функции. Нам нужно много функций, чтобы обеспечить заполнение памяти и обойти ASLR, поэтому генерируем множество функций с таким кодом и вызываем их:
JavaScript сгенерирует код в переменной SPRAY с телом 800 функций zlo_X() и вызовом. Вместо X будет порядковый номер функции. Но все это скрыто от глаз, так как строка генерируется и исполняется на лету... Кстати, для исполнения нужно добавить вызов eval():
После eval(), которая заполонит память нашими страницами с кодом, вызываем fff() — эта функция эксплуатирует описанную в начале статьи уязвимость, переписывая регистр EIP значением 0x007070104, что передаст управление нашему коду, который скопирует метасплойтовский шеллкод по адресу 0x080A0000 и передаст уже ему управление. Время работы эксплойта с JIT SPRAY — от 6 до 10 секунд. ВыводыОчевидно, что разработка JIT-компилятора должна учитывать многие нюансы. Следует не только избегать ошибок, приводящих к перехвату управления, но и делать код таким, чтобы он не сводил функционал защиты ОС к нулю. Выполнение простых правил вроде «не оставлять память доступной для записи и исполнения и не заносить значения пользователя в исполняемую память» сделали бы невозможным использование JIT SPRAY. Так что безопасность — это не только «безопасное программирование», но и безопасная архитектура. Отмечу, что компания VUPEN через неделю после доклада о JIT SPRAY в браузере Safari выпустила 0day-эксплойты для своих клиентов с использованием этой методики. Кроме того, я не имею возможности проверить JIT-движок Safari в Mac OS или iPhone/iPad, но если там архитектура аналогичная, то значит, что эти платформы в наибольшей опасности, так как браузер по обыкновению используется именно на них. |