Браузери і app specific security mitigation. Частина 2. Internet Explorer і Edge

Internet Explorer & Edge
Метою даної статті є огляд специфічних механізмів захисту від експлойтів, інтегрованих в браузери Interner Explorer і Edge.
Ми вирішили об'єднати огляд механізмів безпеки IE і Edge в одну статтю, оскільки, по-перше, обидва вони є продуктами відомої компанії Microsoft, а, по-друге, такий підхід дозволяє відстежити, як змінювався підхід до захисту і, відповідно, саме розвиток механізмів захисту у даних браузерів. Ну і також з тієї причини, що в IE і Edge загальна кодова база.
ie_success_story

Малюнок 1 — Розвиток механізмів безпеки в браузері IE (источник)
Наведемо дані механізми у вигляді списку. Для IE перелік виглядає так:
  • Isolated Heap — окрема «купа» для HTML DOM об'єктів;
  • Memory Protection — механізм «відстроченого» звільнення пам'яті;
  • Memory Garbage Collector (MemGC) — поліпшення механізму Memory Protector, по суті, його спадкоємець;
  • Sandbox – механізм «пісочниці»;
  • Блокування застарілих ActiveX-компонентів;
  • JIT Hardening.
Для Edge:
  • Memory Garbage Collector (MemGC);
  • Sandbox;
  • Attack surface reduction — проведено видалення застарілого коду (toolbars, JScript, VBScript, ActiveX, BHO, VML, legacy document models);
  • Цілісність коду, обмеження завантаження модулів — блокування завантаження DLL, які не є компонентами Windows або підписаними драйверами пристроїв. Бінарні модулі не можуть бути завантажені з віддалених ресурсів;
  • Kernel Attack Protection — скорочення кількості ядерних компонент, доступних для звернення з браузера;
  • JIT Hardening.
У рамках даної статті ми розглянемо наступні механізми:
  1. Isolated Heap & Memory Protection;
  2. MemGC;
  3. Реалізація Sandbox в IE і Edge;
  4. JIT Hardening.
Isolated Heap & Memory Protection & MemGC
Одним з найбільш поширених класів вразливостей в розглянутих браузерах, так і в інших браузерах теж, є UAF (use-after-free).
Будь-яку експлуатацію UAF-вразливостей можна описати у вигляді такої схеми:
uaf_scheme
Малюнок 2 – Схема експлуатації UAF-вразливостей
тобто експлуатація UAF-вразливостей проводиться в 2 кроки:
  1. operation Free – триггерится умова, при якому проводиться звільнення пам'яті об'єкта, при тому, що залишається вказівник на цей об'єкт – «висить вказівник» або dangling pointer. Він буде використаний надалі для звернення до вже неіснуючого об'єкту, наприклад, для виклику методу або запису якого-небудь значення.
  2. Realloc operation – проводиться реаллоцирование пам'яті і запис у неї потрібних значень. На місці звільненого старого об'єкта створюється новий з підготовленими даними, які будуть помилково інтерпретовані як компоненти старого об'єкта – вже після його звільнення.
Наприклад, якщо викликається метод об'єкта після його передчасного звільнення, яке робить можливим зайняти звільнену пам'ять і перезаписати покажчик на таблицю віртуальних функцій, таким чином, підмінивши адреса методу, який буде викликаний. В результаті, це може бути використано для передачі керування по заданому адресою.
Помилкове читання даних з вже звільненого об'єкта залишає можливість взяти з пам'яті вже своєчасно підкладені туди значення, наприклад, покажчик на яку-небудь функцію в кодовій секції релоцируемого ASLR модуля, тобто здійснити витік адреси і – далі – обхід ASLR
Розглянемо механізми, за допомогою яких здійснюється захист від подібного роду вразливостей в браузерах IE і Edge.
Isolated Heap
Захист Isolated Heap з'явилася в IE разом з червневим оновленням 2014 року (MS14-035). Основна її мета – це захист від UAF-вразливостей. Тепер, при виділенні пам'яті для об'єкта, вона виділяється не з купи процесу, а спеціальної «ізольованою купи» (що зрозуміло з назви):
1
Малюнок 3 — Використання ізольованою «купи» при створенні елемента
CImgElement

Практично всі HTML і SVG DOM-об'єкти (CXXXElement) використовують ізольовану «купу». Вони з високою часткою ймовірності можуть мати UAF-уразливості, тому вкрай важливо ізолювати дані об'єкти.
Завдяки впровадженню окремої «купи», у атакуючого з'являються проблеми з другим кроком експлуатації UAF-вразливостей, а саме з реаллоцированием пам'яті і записом в неї своїх контрольованих значень або коду, тому що такі «зручні» об'єкти, як рядки, виділяються в інший «купі» і не можуть бути використані для підміни значень в уразливому об'єкті.
Як видно на малюнку нижче, покажчик на ізольовану «купу» зберігається в глобальній змінній, ініціалізація цієї «купи» відбувається у функції DllProcessAttach():
11
Малюнок 4 – Ініціалізація ізольованою купи
Якщо відстежити XREF-и до _g_hIsolatedHeap, можна побачити 2 функції-аллокатора, які її використовують:
  1. _MemIsolatedAlloc()
    ;
  2. _MemIsolatedAllocClear()
    .
Другий варіант викликає HeapAlloc з прапором HEAP_ZERO_MEMORY, що має запобігати від експлуатації вразливостей, які використовують неинициализированную пам'ять.
Незважаючи на те, що застосування ізольованої «купи» допомагає домогтися зменшення ймовірності успішної експлуатації UAF-вразливостей, не все так райдужно, як здається.
По-перше, досі присутні об'єкти, які використовують «купу» процесу – наприклад, CStr. По-друге, є теоретичний шлях обходу, пов'язаний з тим, що імплементація ізольованою «купи» точно така ж, як і «купи» процесу (немає перевірки типів об'єктів при виділенні їм пам'яті), а також з тим, що виділені об'єкти зберігаються в окремому місці.
тобто для обходу Isolated Heap атакуючому необхідно виконання наступних умов:
  1. Знайти об'єкт, який аллоцируется в ізольованій «купі»;
  2. Даний об'єкт повинен мати приблизно той же розмір, що звільняється UAF-об'єкт;
  3. Атакуючий повинен легко контролювати вміст даного об'єкта.
Хлопці з ZDI в 2015 році представили доповідь на Black Hat і пэйпер [1], в якому запропонували кілька технік обходу Isolated Heap. Вони мають під собою одну спільну основу, яка як раз відповідає описаним вище умовам.
Різниця між техніками лише в деталях, а саме в попередній роботі з «купою».
Розглянемо основну техніку.
Техніка обходу, заснована на використанні об'єкта іншого типу (Type Confusion Bypass Techique)
Як було сказано раніше, ізольована «купа» точно така ж, як і звичайна «купа» процесу, тобто при виділенні пам'яті в ній не враховується тип створюваного об'єкта. З-за цього у атакуючого є варіант заповнити звільняється об'єкт об'єктом іншого типу. Перезапис звільненого об'єкта об'єктом іншого типу призведе до умові type confusion.
Такий об'єкт, по відношенню до якого можливе створення умови type confusion, може мати більший розмір або навіть менший, ніж розмір об'єкта, яким ми збираємося перезаписувати пам'ять. Це важливий факт, який дозволяє атакуючому контролювати певні зміщення всередині повторно використовуваного об'єкта. Наприклад, якщо ми знаємо, що UAF-дефект виникає при разіменуванні покажчика, по зсуву 0x30, то все, що нам потрібно, це замінити звільняється об'єкт таким, який містить значення по зсуву 0x30, яке ми можемо контролювати.
Таким чином, послідовність дій така:
  1. Триггерим умова звільнення пам'яті «атакується» об'єкта;
  2. Замінюємо об'єкт іншим об'єктом, застосовуючи heap spray;
  3. Триггерим повторне використання «атакується» об'єкта.
PoC даної техніки можна подивитися у хлопців з ZDI на github.
Memory Protection
Якщо ж впровадження Isolated Heap ускладнює життя атакуючому експлуатації UAF на другому кроці кроці реаллоцирования і перезапису пам'яті, то наступний механізм ускладнює виконання першого кроку, а саме-звільнення пам'яті об'єкта. Називається дана захист Memory Protection.
Суть цього механізму в тому, що він запобігає звільнення ділянки пам'яті до тих пір, поки на нього є посилання в стеці або в регістрі (поки є «висять покажчики»). Даний механізм вперше з'явився в IE з липневим оновлення безпеки в 2014 році (MS14-037). Перевірку регістрів додали в серпні 2014 року.
Для відстеження ділянок пам'яті, які потрібно звільнити, IE використовує об'єкт CMemoryProtector. Він створюється для кожного потоку, і покажчик на нього зберігається в TLS (thread local storage).
Ініціалізація даного об'єкта проводиться у функції MemoryProtection::CMemoryProtector::ProtectCurrentThread() (варто відзначити, що при ініціалізації всі поля об'єкта встановлюються в 0).
12
Малюнок 5 — Функція
ProtectCurrentThread()

Об'єкт CMemoryProtector має наступну структуру (малюнок 6) [2]
CMemoryProtector
Малюнок 6 — Структура об'єкта
CMemoryProtector

Об'єкт CMemoryProtector складається з наступних полів:
  1. BlocksArray – структура типу SBlockDescriptorArray, яка містить інформацію про звільняються ділянках пам'яті (wait-list);
  2. IsForceMarkAndReclaim – прапор, який застосовується для визначення того, чи потрібно викликати операцію Reclamation Sweep (про це нижче);
  3. StackHighAddress – найбільший адресу стека в потоці. Використовується для операції позначки (mark operation) ділянки при вибірці значень покажчиків з стека;
  4. StackMarkerAddress – найбільший адресу стека, коли викликається функція
    MemoryProtection::CMemoryProtector::ProtectCurrentThread()
    . Застосовується для визначення того, повністю чи стек пройдено, і, отже, буде виконується операція Reclamation Sweep (про це нижче).
Структура SBlockDescriptorArray містить такі поля:
  1. Blocks – це список звільнених ділянок (wait-list). Кожен елемент wail-list – це дескриптор (SBlockDescriptor), що містить інформацію про блок пам'яті: його базову адресу, розмір, міститься він в ізольованою купі або у звичайної.
  2. TotalSize – це загальний розмір усіх звільняються ділянок, що перебувають у wait-list. Використовується для визначення, чи потрібно виконати операцію Reclamation Sweep;
  3. Count – кількість звільнених ділянок, які перебувають у списку Blocks;
  4. Max Count – максимальна кількість елементів у wait-list;
  5. IsSorted – прапор, який визначає відсортований список Blocks.
Memory Protection при звільненні пам'яті використовує функцію
MemoryProtection::CMemoryProtector::ProtectedFree()
замість
HeapFree()
.
void __userpurge MemoryProtection::CMemoryProtector::ProtectedFree(void *a1@<ecx>, void *Dst, unsigned __int32 a3, void *a4)
{
void *v4; 
MemoryProtection::CMemoryProtector *v5; 
unsigned int v6; 
MemoryProtection::CMemoryProtector *v7; 
size_t Size; 

v4 = a1;
if ( Dst )
{
if ( MemoryProtection::CMemoryProtector::tlsSlotForInstance != -1
&& (v5 = (MemoryProtection::CMemoryProtector *)TlsGetValue(MemoryProtection::CMemoryProtector::tlsSlotForInstance),
(v7 = v5) != 0) )
{
MemoryProtection::CMemoryProtector::ReclaimMemory(v5, v6); //(1)
Size = 0;
if ( MemoryProtection::SBlockDescriptorArray::AddBlockDescriptor(v7, Dst, v4 != MemoryProtection::g_heapHandles, &Size) )
{
MemoryProtection::SAddressFilter::AddBlock((MemoryProtection::CMemoryProtector *)((char *)v7 + 32), Dst, Size); //(2)
memset(Dst, 0, Size);
}
else
{
RaiseFailFastException(0, 0, 0);
}
}
else
{
HeapFree(v4, 0, Dst);
}
}
}

Функція
ProtectedFree()

ProtectedFree()
з деякою періодичністю та при деяких умовах здійснює процедуру Reclamation Sweep (1). Важливо відзначити, що вона здійснюється перед тим, як звільняється блок потрапить в Wait-List.
Суть процедури Reclamation Sweep:
  1. Перевірка сумарної кількості ділянок пам'яті, що перебувають у Wait-List, їх сумарного об'єму;
  2. Якщо сумарна кількість дільниць більше, ніж CMemoryProtector.BlocksArray.TotalSize або сумарний обсяг >= 100 000 байт або ж встановлено прапорець IsForceMarkAndReclaim, провести процедуру звільнення пам'яті для блоків, які перебувають у Wait-List.
    2.1. Якщо на блок пам'яті є вказівник стеку або в регістрі, то пропустити його;
    2.2. Якщо на блок пам'яті покажчиків ніде немає, то звільнити його.
Покажчиком на блок пам'яті вважається як покажчик на початок блоку, так і на будь-яке місце всередині нього.
За процедуру Reclamation Sweep відповідають кілька функцій, які викликаються всередині функції
MemoryProtection::CMemoryProtector::ReclaimMemory()
(там же і перевіряються умови для запуску даної процедури):
  • MemoryProtection::CMemoryProtector::MarksBlocks()
    .
Дана функція спочатку сортує ділянки пам'яті в порядку збільшення адрес. Потім, вона перевіряє, чи існують покажчики на дані ділянки в стеку потоку або у регістрах. У разі, якщо такий покажчик існує,
MarksBlocks()
позначає такий блок.
  • MemoryProtection::CMemoryProtector::ReclaimUnmarkedBlocks()
Дана функція здійснює простий обхід wait-листа та звільняє всі непозначені ділянки пам'яті. Лічильник в об'єкті
CMemoryProtection
також оновлюється в процесі.
Після проведення процедури Reclamation Sweep, звільняється блок додається в wait-list і заповнюється нулями.
protectedFree_algo
Малюнок 7 — Алгоритм функції
ProtectedFree()
джерело [1])

Варто відзначити, що раніше існувала безумовна процедура звільнення всіх ділянок пам'яті
MemoryProtection::CMemoryProtector::ReclaimMemoryWithoutProtection()
– це відбувалося щоразу, коли викликалася функція
mshtml!GlobalWndProc()
в результаті отримання повідомлення від головного вікна потоку. Але пізніше цю можливість прибрали.
Memory Protection — вкрай ефективна техніка проти UAF-вразливостей, коли вказівник на звільнену пам'ять залишається в стеці або в регістрі, оскільки Memory Protection гарантує, що цей блок буде залишатися в wait-list до тих пір, поки його знову не використовують (при виділенні пам'яті) і буде перебувати в wait-list, заповнений нулями.
Memory Protection також зменшує ймовірність експлуатації інших UAF-вразливостей, не потрапляють в категорію, описаних вище. В цьому випадку, коли «висячого покажчика» немає ні в стеку, ні в регістрі, атакуючий повинен вирішити наступні завдання:
1) Затримка звільнення пам'яті
Як було описано вище, звільнення пам'яті може пройти з деякою затримкою, до тих пір, поки не буде здійснена процедура Reclamation Sweep.
2) Невизначеність з-за «сміття» в стеку
Блок пам'яті може залишитися в wait-list при процедурі Reclamation Sweep, оскільки може так вийти, що стек зберігає значення, яке дорівнює адресою де-небудь всередині звільняється блоку. Необов'язково, що дане значення є дороговказом, але Memory Protection сприйме його саме так.
3) Велика складність у визначенні часу, коли відбудеться звільнення ділянки пам'яті.
Процедура Reclamation Sweep здійсниться лише в тому випадку, якщо обсяг ділянок пам'яті в wait-list перевищить 100 000 байт. Це не може відбутися доти, поки в wait-list не потрапить дійсно великий блок пам'яті.
4) Більш складна поведінка менеджера купи при звільненні пам'яті
Узагальнюючи все вищесказане, варто враховувати, що звільняється блок пам'яті спочатку потрапляє в wait-list і знаходиться там, поки обсяг wait-list не перевищить 100 000 байт, а тільки потім звільняється при тому, що на нього немає вказівників в стеці або в регістрах. При цьому, неможливо передбачити стан купи після звільнення одночасно великої кількості ділянок пам'яті різного розміру з wait-list.
Незважаючи на ці складності, ті ж самі хлопці з ZDI придумали кілька технік обходу механізму Memory Protection [1] – деякі з них очевидні і прості, а деякі ні.
Розглянемо їх далі.

Елементарні техніки

Найбільш очевидним рішенням є застосування так званого «memory pressure» для того, щоб виконалася процедура Reclamation Sweep, тобто необхідно аллоцировать і потім відразу звільнити пам'ять розміром 100 000 байт.
// Тут код для звільнення пам'яті деякого об'єкта 
... 
// Кінець коду звільнення пам'яті 
// Цикл для форсування очищення wait-list 
var n = 100000 / 0x34 + 1; 
for (var i = 0; i < n; i++) 
{
document.createElement("div"); 
} 
CollectGarbage(); 
// Код, який знову використовує звільнений об'єкт
...
//

Але такий підхід не звільнить від усіх проблем – ми вирішимо проблему з затримкою звільнення пам'яті, але не позбудемося всіх інших. Разом з нашим об'єктом будуть звільнені багато інші об'єкти, причому непередбачуваним чином. Дане невизначений поведінка веде до зменшення надійності при спробі до встановлення контролю за вмістом визволеної пам'яті.
Також раніше було можливо друге очевидне рішення – це використання безумовної процедури звільнення всіх ділянок пам'яті, яка здійснюється, коли триггерится функція
GlobalWndProc()
.
Ми можемо перервати виконання експлойта затримкою настільки великий, щоб точно стався новий виклик функції
GlobalWndProc()
, потім Memory Protection звільнить всі блоки в wait-list.
function step1() { 
// Попередній код починається тут... 
... 
// А закінчується тут
// Встановлюємо затримку для наступного кроку, щоб WndProc заново спрацювала,
// і сталася очищення wait-list
window.setTimeout(step2, 3000); 
} 
function step2() { 
// Код звільнення деякого об'єкта починається тут... 
... 
// А тут закінчується...
// Встановлюємо затримку для наступного кроку, щоб WndProc заново спрацювала,
// сталася очищення wait-list, і деаллоцируем наш об'єкт
window.setTimeout(step3, 3000);
} 
function step3() {
// Код для повторного використання об'єкта 
...
}

Дане рішення дозволяє мінімізувати число сторонніх об'єктів, які будуть звільнятися разом з нашим. Але воно має свої мінуси – використання
setTimeout()
створює можливість для появи додаткової непередбачуваною гілки коду, що виконується в поточному потоці. В результаті можна отримати непередбачену й небажану модифікацію «купи».

«Просунута» техніка

Дослідники на цьому не зупинилися, подумали і вирішили, що для обходу Memory Protection необхідно стабілізувати wait-list — потрібно побудувати послідовність таких дій скрипту, щоб контролювати і знати стан wait-list. Створивши таку послідовність, ми вже зможемо проводити експлуатацію UAF-вразливостей.
Для початку припустимо, що ми можемо виділити буфер А розміром 100 000 байт. Потім ми намагаємося цей буфер звільнити. У підсумку, Memory Protection помістить наш буфер А в wait-list, і розмір wait-list буде точно >= 100 000 байт.
13
Малюнок 8 — Стан wait-list після додавання в нього буфера A (джерело [1])
Далі, ми знову виділяємо і звільняємо буфер B вже потрібного нам розміру s. Під час виклику
ProtectedFree()
Memory Protection побачить, що розмір wait-list такий, що пора б почати звільняти (реально звільняти) його елементи. Після даної процедури наш буфер розміром s буде в wait-list.
14
Малюнок 9 — Стан wait-list після додавання в нього буфера B (джерело [1])
Цілком можливо, що не всі ділянки з wait-list будуть звільнені і покинуть його – на деякі з них можуть бути покажчики в стеку (назвемо такі Wi). Але, по-перше, ми можемо з упевненістю сказати, що їх сумарний розмір набагато менше 100 000 байт. По-друге, неважливо, яке їх загальна кількість і сумарний розмір для подальших кроків. Ми точно знаємо, що поки на них є покажчики в стеку, ці дільниці будуть wait-list.
Отже, на даному етапі, ми знаємо приблизний стан wait-list і готові виконати потрібні дії з «купою» для експлуатації вразливостей.
Припустимо, що ми хочемо звільнити блок пам'яті за адресою C. Цілком можливо, що навіть з метою триггернуть UAF за цією адресою. Для того, щоб блок пам'яті за цією адресою точно звільнився, і зробив це передбачуваним чином:
  1. Викликаємо
    ProtectedFree()
    для блоку C. Блок C потрапляє в wait-list і знаходиться там разом з блоками Wi і блоком B розмір s;
  2. Виділяємо пам'ять під великий блок пам'яті D розміром 100 000 байт і відразу ж його звільняємо. Тепер цей блок пам'яті потрапляє в wait-list, і розмір wait-list стає >= 100 000 байт;
  3. Виділяємо пам'ять під блок пам'яті E розміром s і звільняємо його. Після виклику
    ProtectedFree()
    для блоку E, наш потрібний блок C блоки B і D потраплять під процедуру Reclamation Sweep і будуть звільнені.
15
Малюнок 10 – Підсумковий стан wait-list (джерело [1])
Тепер перенесемо практики в дану теоретичну стратегію. Необхідно визначити об'єкт, який виділяє буфери довільного розміру та звільняє їх через
ProtectedFree()
. Хлопці з ZDI вибрали об'єкт
CStr
– він має динамічний розмір і його можна аллоцировать з поза. Але завжди можна спробувати обрати інший об'єкт, на благо ProtectedFree 1100 xref-ів.
Також, анализуя MSHTML, вони знайшли, що
CStr
використовує метод
CElement::var_getElementsByClassName
. Дістатися до нього можна через DOM-метод
getElementsByClassName
на будь-якому елементі HTML (що прямо те, що потрібно).
Під час виконання, даний метод створює
CStr
, що містить рядкові дані, які передаються в якості параметра
getElementsByClassName
, а потім видаляє
CStr
.
getElementsByClassName
Малюнок 11 – Код функції
CElement::var_getElementsByClassName

Таким чином, одним викликом
getElementsByClassName
можна досягти мети виділення і звільнення буфера довільного розміру.
Одне невелике обмеження полягає в тому, що мінімальний розмір буфера з рядковими даними, який ми можемо передати
getElementsByClassName
, становить 0x28 байт. Тому
CStr
займає 0x28*2 + 6 = 0x56 байт (по два байти на символ, плюс 6 додаткових байт).
var oDiv1 = document.createElement('div');
// Аллоцируем/ звільняємо через ProtectedFree буфер розміром string1 
oDiv1.getElementsByClassName(string1);
// ...
// Аллоцируем/ звільняємо через ProtectedFree буфер розміром string1
oDiv1.getElementsByClassName(string1);
// ...
// Аллоцируем/ звільняємо через ProtectedFree буфер розміром string2
oDiv1.getElementsByClassName(string2);

Таким чином, використовуючи техніку, описану вище, атакуючий може визначити будь-який необхідний йому патерн виділення і звільнення пам'яті на «купі». Складність поведінки Memory Protection при звільненні пам'яті усунена.
Заради інтересу, вкрай рекомендується почитати в [1] як можна, використовуючи механізм роботи Memory Protection, обійти ASLR.
Memory Garbage Collector (MemGC)
Наступний механізм, який ми розглянемо, можна назвати спадкоємцем Memory Protection. Він називається Memory Garbage Collector (MemGC), і був представлений в новому движку Edge в Win10. Згодом з'явився і в IE11.
Мета впровадження MemGC така ж, як і у MP – захист від експлойтів типу UAF.
Хлопці з Microsoft пишуть [3], що MemGC працює в тому ж дусі, що і MP, але ще і переглядає «купу», крім стека і регістрів на предмет посилань на захищаються типи об'єктів.
Але, звичайно, вони трохи лукавлять, і відмінність не тільки в додатковому перегляді купі, але ще і в реалізації. Розглянемо її докладніше [4].
MemGC використовує окремо керовану «купу», яка називається MemGC Heap (нехитру назву). Вона використовується для виділення пам'яті об'єктів і складання сміття – по суті, та ж операція Reclamation Sweep, тільки ще й освобождются такі ділянки пам'яті, на які немає посилань у MemGC Heap. Реалізація MemGC Edge залежить від JavaScript-движка Chakra (новий JS-движок, про нього в наступному розділі). Залежність проявляється у тому, що всі функції для роботи з пам'яттю знаходяться в ньому.
до Речі, Microsoft виклала у відкритий доступ вихідні коди ядра движка Chakra. Кому цікаво, можете подивитися тут.
Схема виділення пам'яті, що використовується MemGC, включає в себе створення шматків пам'яті, званих «сегменти» (Segments), і потім поділ даних «сегментів» на сторінки (Pages) розміром 4096 байт. Далі, сторінки об'єднуються в блоки (Blocks), а ті, в свою чергу, використовуються для виділення пам'яті об'єктах відповідного розміру:
MemGC_Heap_Diagram_V2
Рисунок 12 – Схема MemGC Heap (джерело [4])
EdgeHTML/MSHTML DOM об'єкти і багато інші внутрішні елементи, які рендеряться движком, управляються MemGC. І оскільки MemGC використовує окрему керовану «купу», Isolated Heap, описана вище, стає не потрібна і не використовується.
тобто фактично, MemGC замінює собою і Isolated Heap, і Memory Protector, і ускладнює експлуатацію UAF-уразливостей на обох кроках (дивись малюнок 1, якщо забув про що мова).
MemGC проводить наступні операції:
  1. Виділення пам'яті;
  2. Звільнення пам'яті;
  3. «Збирання сміття» (Garbage Collector).
Розглянемо кожну операцію детальніше.
1. Виділення пам'яті.
В імплементації MemGC в EdgeHTML, коли об'єкта, керованого MemGC, необхідно виділити пам'ять, виконується виклик функції
edgehtml!MemoryProtection::HeapAlloc<1>()
або
edgehtml!MemoryProtection::HeapAllocClear<1>()
, яка, в свою чергу, справляє виклик
chakra!MemProtectedHeapRootAlloc()
.
MemGC_HeapAlloc
Малюнок 13 – Код функції
MemoryProtection::HeapAlloc<1>()

chakra!MemProtectedHeapRootAlloc()
виділить шматок (chunk) з блоку (Block) у відповідному «відрі» (Bucket), і потім на такий шматок пам'яті ставиться прапор «root». «Root»-прапор, в термінології MemGC, такий об'єкт/шматок пам'яті, що адресується безпосередньо (directly referenced) програмою, і тому не повинен потрапити під «збирання сміття».
2. Звільнення пам'яті.
Коли необхідно звільнити пам'ять об'єкта, викликається функція
edgehtml!MemoryProtection::HeapFree()
, яка, в свою чергу, викликає
chakra!MemProtectHeapUnrootAndZero()
.
MemGC_HeapFree
Малюнок 14 – Код функції
MemoryProtection::HeapFree()

chakra!MemProtectHeapUnrootAndZero()
намагається визначити блок (Block), в якому знаходиться шматок визволеної пам'яті об'єкта, обнуляє його й скидає прапор «root». Скидання прапора «root» для ділянки пам'яті робить його кандидатом, потрапляють під «збір сміття» і буде звільнений, якщо MemGC не знайде посилань на нього.
3. Збір сміття (Garbage Collection)
В той момент, коли розмір всіх ділянок пам'яті, у яких скинутий прапор «root» перевищить динамічно обчислюване порогове значення, в справу вступить збирач сміття, який стриггерится функцією
chakra!MemProtectHeap::Collect()
. При «збиранні сміття» проводиться операція Reclamation Sweep, коли ділянки пам'яті зі скинутим прапором «root» і на які немає покажчиків, будуть звільнені. Деякі частини операції Reclamation Sweep виконуються в окремому потоці (
chakra!Memory::Recycler::ThreadProc
), який отримує повідомлення від
chakra!Memory::Recycler::StartConcurrent()
.
Перша фаза – це фаза пошуку посилань на звільнені ділянки пам'яті. Біт маркування для всіх ділянок скидається, а потім все «root»-ділянки маркуються (все це відбувається в
chakra!Memory::Recycler::BackgroundResetMarks()
).
Потім, «root»-ділянки (тобто ті, що знаходяться в купі), регістри і стек перевіряються на наявність посилань на звільняється ділянку. Це здійснюється за допомогою функцій
chakra!Memory::Recycler::ScanImplicitRoots()
та
chakra!MemProtectHeap::FindRoots()
). Якщо звільняється ділянку знайдено посилання, то він позначається. Ділянки, які в підсумку виявилися непомеченными, звільняються і стають доступними для реаллокации.
Більш детально, з усіма структурами і кодом (з блекджеком і ....), опис цих операцій можна подивитися в [5].
Варто відзначити, що механізм MemGC в IE реалізовано точно так само, як і в Edge, тільки всі функції розташовуються в лібе mshtml.dll.
Отже, узагальнимо відмінності між MemGC і Memory Protection:
  1. MemGC має свою, окрему від «купи» процесу, зі складною структурою, і управляє операціями і виділення і звільнення пам'яті – тобто має свій менеджер «купи»;
  2. MemGC сканує не тільки стек і регістри, але й саму «купу» на предмет посилань на звільняється блок пам'яті (швидше за все, у відповідь на техніку обходу хлопців з ZDI :) );
  3. Порогове значення, коли стартує процедура Reclamation Sweep, визначається динамічно, на відміну від Memory Protection, де граничне значення має константне значення. Також сама процедура очищення пам'яті частково виробляється в іншому потоці.
  4. MemGC ускладнює експлуатацію UAF і на етапі звільнення пам'яті об'єкта, і на етапі реаллокации і перезапису пам'яті.
Що стосується способів обходу MemGC, то на даний момент невідомі (принаймні публічні) техніки для обходу цього механізму.
Sandbox
Наступний механізм, який ми розглянемо – «пісочниця», коли процес запускається з жорстко обмеженими привілеями. Робиться це для зниження здатності атакуючого писати, редагувати, читати дані користувача машині і встановлювати шкідливий код.
В IE даний механізм вперше з'явився в 7 версії під Windows Vista, і називався Protected Mode. Потім, в IE10 під Win8, Protected Mode отримав своє нове розвиток під новим ім'ям – Enchanced Protected Mode (EPM).
Розглянемо, як влаштований EPM зсередини.
Архітектура
IE і Edge у своїй архітектурі «пісочниці» слідують Loosely-Coupled IE (LCIE) моделі, яка була представлена ще в 8 версії IE. Виконання браузера відбувається в окремих процесах. Є процес-брокер, який породжує дочірні процеси для вкладок. Вкладочные процеси запускаються з урізаними привілеями, це може бути Low Integrity Level для IE і AppContainer – нова технологія ізоляції додатків Microsoft — для Edge. Поговоримо детальніше про механізми управління доступом до об'єктів у Windows.
sandbox
Малюнок 15 — Архітектура Sandbox в IE і Edge (джерело [6])
Класична модель розмежування прав доступу до об'єктів Windows будується на основі ACL – access control lists – списків контролю доступу.
Для об'єкта доступу вказується або успадковується список dacl назад (discretionary access control list – дискреційний список контролю доступу), і він містить такі записи (ACE – access control entries): кому надати або заборонити доступ до якого роду (читання, запис, виконання тощо). При зверненні до об'єкту система бере маркер доступу (access token) потоку, що містить ряд SID-ов – ідентифікаторів суб'єктів: користувача, груп користувача і т. п., обходить список dacl назад об'єкта і перевіряє компоненти списку стосовно суб'єкта доступу.
Microsoft розширила цю модель захисту доступу, додавши до неї т. н. MIC – mandatory integrity control – мандатний контроль доступу; тут для суб'єктів і об'єктів доступу встановлюється т. н. integrity level – рівень цілісності» (не зовсім очевидно, «рівень доступу» було б точніше). Мандатний контроль доступу реалізується, як заборона звернень до об'єктів з більш високим рівнем привілеїв, ніж у суб'єкта. Мандатний контроль доступу виконується перед дискреційним.
Безіменний
Малюнок 16 — Зниження Integrity Level для вкладочных процесів IE
Це забезпечує ізоляцію потенційно скомпрометованих процесів. При наявності уразливості типу RCE в IE, це має скоротити імпакт можливої атаки, наприклад, перешкодити закріпитися в системі, записавши щось в критичні області реєстру або файлової системи.
AppContainer є останнім розширенням системи розмежування доступу Windows. Нова модель доступу створює власне ізольований простір об'єктів для кожного контейнера, йому вже, як годиться, недоступні глобальні об'єкти операційної системи. Області файлової системи і реєстру, виділені для збереження даних програми, мають створену для даного контейнера мітку, що дозволяє доступ спеціальному суб'єкту, асоційованого з даним контейнером. На відміну від мандатного підходу, тут процеси з скороченими привілеями не мають доступу до даних один одного. Крім індивідуального SID-а кожного контейнера, окремо для ізолюючих додатки специфицируются «capabilities», такі як internetClient, location або microphone, що обмежують його можливості.
Однак, для довільного об'єкта можуть бути задані правила ACE в списку dacl назад, дозволяють доступ усім контейнерів, посилаючись на SID «ALL APPLICATION PACKAGES». Це зберігає можливість використання системних компонентів контейнизированными процесами, наприклад, завантаження бібліотек з системній директорії.
Безымянный1
Малюнок 17 — права доступу до системної директорії для всіх контейнизированных процесів
Таким чином, для програми все ж залишається можливість читати область файлової системи і, аналогічним чином, реєстру, в якій зберігаються об'єкти операційної системи і встановленого для «Program Files» доданий той же SID). Це, як зазначає дослідник Mark Yanson [6], може застосовуватися для вилучення такої інформації, як ліцензійні ключі, конфігураційні файли встановленого ПЗ, також можливе використання витоку про встановлений ПО і версіях його компонентів для майбутніх атак.
Можна виділити основні способи втечі з ізольованого оточення і виконання власного коду поза контейнера:
  1. Експлуатація бінарних вразливостей процесу-брокера, сервісних процесів, драйверів і ядра операційної системи;
  2. Використання наявних прогалин у правах доступу, наприклад, можливість запуску з високими привілеями довіреної системою додатка, яке певним способом можна використовувати для виконання запланованих дій, наприклад, через запуск довільного скрипта.
В якості прикладу можна навести нещодавно потрапив у загальну увагу драйвер capcom.sys, який є запчастиною від однієї комп'ютерної гри. Цей драйвер надає одну «чудову» функцію:
CtD6L38XYAA2krC.jpg_large
Малюнок 18 — Вразлива функція драйвера capcom
Тут: відключається SMEP, передається до управління за адресою, переданим драйверу. Фактично, це виконання довільного коду на рівні ядра.
Інший приклад. У Windows 10 наявний процес dismhost.exe (Disk Cleanup), який може бути запущений непривілейованим користувачем, але при цьому буде наділений системою підвищеними привілеями. Після запуску він довантажував DLL бібліотеки з директорії в %TEMP%, доступною для запису поточного користувача. Це дозволяло, підклавши туди свою бібліотеку, отримати виконання коду в привілейованому процесі.
Chakra JIT Hardening
Реалізація JS в сучасних браузерах Microsoft — IE 11 і Edge – називається Chakra, і містить специфічні для JS движка механізми захисту від експлуатації бінарних вразливостей. Ключовим компонентом сучасних JS-інтерпретаторів є JIT (just-in-time) компілятор, який здійснює трансляцію JS-код в машинний код, що позитивно позначається на продуктивності скриптів на веб-сторінках.
З точки зору безпеки, додатки JIT відкривають нам деякі можливості. Ми можемо форсувати компіляцію скрипта в машинний код, і отримати виконувані сторінки в адресному просторі процесу з корисним для нас вмістом. В минулому були актуальні такі техніки:
  • Спочатку код і дані JS не розділялися і розміщувалися JIT компілятором в одному регіоні пам'яті з правами на виконання, що дозволяло, розмістивши наш шелл-код в масиві в скрипті, відразу отримати його виконуваним. Дану операцію можна повторити багато разів, реалізувавши т. н. «спрей» (JIT spray), заповнивши адресний простір процесу однорідними даними – нашим шелл-кодом. Це знімає проблему невизначеності місця розташування нашого шелл-коду.
Тепер JIT відокремлює код від даних, роблячи останні неисполняемыми.
  • З плином часу були використані решта можливості застосування JIT-компіляторів. Константные значення в скрипті (ініціалізації змінних, наприклад) заносилися в машинний код в незмінному вигляді на відомі місця від початку скомпілованої функції. Це дозволяло зручно закласти ROP гаджети – маленькі фрагменти коду, які виконуються послідовно, передаючи управління по ланцюжку один одному.
Тепер JIT накладає на константи маски, використовуючи xor, і раскодируя їх назад під час виконання скомпільованого коду – це "constant blinding". Друга техніка ускладнення рандомизирует розташування можливих ROP-гаджетів в згенерованої функції, вставляючи трохи сміттєвих команд в її пролозі – це "insert NOPs".
Constant Blinding
Що у нас тепер залишилося? Тільки можливість вставки маленьких, два байта або менше, констант – зберігаються у вихідній формі.
Спробуємо закласти цими скромними засобами набір ROP-гаджетів, які будуть потрібні для виклику функції VirtualProtect(addr, size, flags, oldflags). Завдання здійснення такого виклику типова для побудови експлойта в Windows, це необхідно для встановлення атрибутів доступу для сторінок пам'яті з шелл-кодом, тобто для того, щоб зробити цю пам'ять виконується. Прототип функції містить в собі чотири аргументи: адреса регіону пам'яті, його розмір, прапори необхідних атрибутів і покажчик на змінну, куди збережуться старі атрибути.
Ми будемо цілитися на x64, де для дзвінків WinAPI покладається використовувати fastcall, що означає заносити перші аргументи в регістри – rcx, rdx, r8 і r9, а решта – в стек. Двох байтів, які ми маємо, нам достатньо для ініціалізації значенням з стека одного з восьми регістрів загального призначення: pop, R + ret. З rcx і rdx ми впораємося, з r8 і r9 все не так просто. Щоб закодувати команду «pop R, де R – один з доданих до x64 архітектурі регістрів r8 — r15, доведеться поставити перед однобайтовым опкодом інструкції pop префікс зміни набору регістрів – REX префікс, відповідно, в наші два байти ret вже не поміститься. Крім того, звернемо увагу на те, що у нас вийде з команди з двухбайтной константою:
81 C0 41 58 00 00 add eax, 5841h
___________________________________________
41 58 pop r8
00 00 add byte [rax], al

Відразу виникає друга проблема: необхідно встановити в регістр rax адресу на що-небудь в пам'яті, куди можна писати, перед тим, як передати управління на такий гаджет. Не забуваємо про те, що потім нам треба передати управління на наступний гаджет...
По порядку:
  • Уникнути виникнення виключення при доступі за невалидному адресою в регістрі rax ми можемо, инициализировав його перед викликом цього гаджета, тут нам цілком вистачить маленької незамаскированной двухбайтной константи 0xc358 :)
    58 pop rax
    C3 ret

  • Передача управління. Якщо для JIT-а підготувати гранично просту JS функцію, то після нашого гаджета у функції залишиться відносно коротший «хвіст».
    function f(addr) {
    return addr + 0x5841;
    }

    Безымянный2
    Малюнок 19 — Хвіст скомпілованої функції: a. вирівняний по командам; b. вирівняний з ROP гаджета в константі. Картинка взята з дослідження [7]
    Цей «хвіст» зможе здійснитися до кінця, якщо підготувати для нього регістри і стек.
Insert NOPs
Цей нехитрий спосіб рандомізації розташування скомпільованого коду у вихідному буфері JIT-компілятора варто проілюструвати листингами, отриманими за декілька послідовних послідовності браузера з тестовим скриптом.
0000 mov rax, 2B5C990h 
000A cmp rsp, rax 
000D jg loc_2AE0034 
0013 mov rdx, 500790h 
001D mov rcx, 990h 
0027 mov rax, 7FEF3435450h 
0031 jmp rax 
0034 ; ---------------------------
0034 
0034 loc_2AE0034: 
0034 mov rax, 23D61A8h 
003E inc byte ptr [rax] 
0040 jnz loc_2AE0049 
0046 mov byte ptr [rax], 0FFh 
0049 
0049 loc_2AE0049: 
0049 mov [rsp+20h], r9 
004E mov [rsp+18h], r8 
0053 mov [rsp+10h], rdx 
0058 mov [rsp+8], rcx 
005D push rbp 
005F mov rbp, rsp 
0062 sub rsp, 10h 
0066 push rdi 
0068 push rsi 
006A push rbx 
006C sub rsp, 38h 
0070 xor eax, eax 
0072 mov [rbp-8], rax

Тут NOP-ів немає.
0000 mov rax, 33CC990h 
000A cmp rsp, rax 
000D jg loc_3160035 
0013 mov rdx, 326890h 
001D mov rcx, 990h 
0027 mov rax, 7FEF3435450h 
0031 jmp rax 
0034 ; ---------------------------
0034 nop 
0035 
0035 loc_3160035: 
0035 mov rax, 2FAC1A8h 
003F inc byte ptr [rax] 
0041 jnz loc_316004A 
0047 mov byte ptr [rax], 0FFh 
004A 
004A loc_316004A: 
004A mov [rsp+20h], r9 
004F mov [rsp+18h], r8 
0054 mov [rsp+10h], rdx 
0059 mov [rsp+8], rcx 
005E push rbp 
0060 mov rbp, rsp 
0063 sub rsp, 10h 
0067 push rdi 
0069 push rsi 
006B push rbx 
006D sub rsp, 38h 
0071 xor eax, eax 
0073 mov [rbp-8], rax

Зверніть увагу на зміщення 34h. Все зрушилося.
0034 nop dword ptr [rax] 
0037 nop dword ptr [rax+00h]
003B xchg ax, ax 

Бувають і інші NOP-и, різної довжини.
В цій ситуації встановити дійсне розташування даних можливо, хіба що здійснюючи читання з довільним адресами.
JIT & CFG (Control Flow Guard)
Попутно помічений цікавий момент — пробіл у реалізації CFG в Windows 8.1. Докладніше про цьому механізмі можна почитати в статті нашого колеги [8].
Області пам'яті, зайняті скомпільованим JIT-му кодом, занесені в бітову карту процесу, допускається передача управління в них, як, наприклад, вище — в опкоды, закладені в константах.
Однак, перевірка адреси CFG, хоча в даному випадку і пропускає виклик, має побічний ефект — псує значення в регістрі rax, що для наведеної техніки є необхідним. Для випадку підміни таблиці віртуальних методів в контрольованому об'єкті це викликає для нас незручності: якщо ми вирішимо зробити JIT ROP, спочатку треба буде перекинути покажчик стека rsp у наші дані для контролю потоку управління, а оскільки поки ми стек не контролюємо, то регістр rsp залишається ініціалізувати тільки значенням іншого регістра, і дуже зручно, якщо б це був rax — валідний вказівник на пам'ять, куди можна писати (він вказував в vtable!), щоб не обламатися на
add byte [rax], al
. Але тепер він безповоротно зіпсований.
mov rax, [rdi] ; this->vtable
mov rbx, [rax+208h] ; ptr = vtable[idx]
mov rcx, rbx ; _QWORD
call cs:__guard_check_icall_fptr
mov rcx, rdi ; this

Але JIT-компілятор існує не тільки для JS. Ми рекомендуємо вам ознайомитися з чудовим дослідженням слабкостей компілятора WARP браузера Edge, який у своєму розвитку повторив проблеми JIT-компілятора JS [9].
Висновок
На прикладі браузерів – IE і Edge — можна спостерігати процес еволюційного ускладнення технік атаки і механізмів захисту, безперервне протиборство щита і меча. Баги залишаються, але експлуатація вразливостей стає все складніше — збільшується вартість використання існуючих помилок. Однак, природа цих помилок не змінюється, оскільки застосовуються ті ж мови і середовище виконання. Не змінюється сам код уразливих частин браузера, але накладаються механізми, спрямовані на запобігання існуючих шляхів експлуатації багів.
Джерела
  1. https://www.blackhat.com/docs/us-15/materials/us-15-Gorenc-Abusing-Silent-Mitigations-Understanding-Weaknesses-Within-Internet-Explorers-Isolated-Heap-And-MemoryProtection-wp.pdf
  2. https://securityintelligence.com/understanding-ies-new-exploit-mitigations-the-memory-protector-and-the-isolated-heap/
  3. https://blogs.technet.microsoft.com/srd/2016/01/12/triaging-the-exploitability-of-ieedge-crashes/
  4. https://www.blackhat.com/docs/us-15/materials/us-15-Yason-Understanding-The-Attack-Surface-And-Attack-Resilience-Of-Project-Spartans-New-EdgeHTML-Rendering-Engine-wp.pdf
  5. https://github.com/zenhumany/hitcon2015
  6. https://www.blackhat.com/docs/asia-14/materials/Yason/WP-Asia-14-Yason-Diving-Into-IE10s-Enhanced-Protected-Mode-Sandbox.pdf
  7. http://users.ics.forth.gr/~elathan/papers/ndss15.pdf
  8. https://habrahabr.ru/company/dsec/blog/305960/
  9. https://472ac6bb-a-62cb3a1a-s-sites.googlegroups.com/site/bingsunsec/WARPJIT/JIT%20Spraying%20Never%20Dies%20-%20Bypass%20CFG%20By%20Leveraging%20WARP%20Shader%20JIT%20Spraying.pdf
Автори
  • Козоріз Максим
  • Турченков Дмитро
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.