Internet Explorer & Edge
Целью данной статьи является обзор специфичных, интегрированных в браузеры Internet Explorer и Edge, механизмов защиты от эксплойтов.
Мы решили объединить обзор механизмов безопасности IE и Edge в одну статью, поскольку, во-первых, оба они являются продуктами небезызвестной компании Microsoft, а, во-вторых, такой подход позволяет отследить, как менялся подход к защите и, соответственно, само развитие механизмов защиты у данных браузеров. Ну и также по той причине, что у IE и Edge общая кодовая база.
Рисунок 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.
В рамках данной статьи мы рассмотрим следующие механизмы:
- Isolated Heap & Memory Protection;
- MemGC;
- Реализация Sandbox в IE и Edge;
- JIT Hardening.
Isolated Heap & Memory Protection & MemGC
Одним из самых распространенных классов уязвимостей в рассматриваемых браузерах, да и в других браузерах тоже, являются UAF (use-after-free).
Любую эксплуатацию UAF-уязвимостей можно описать в виде следующей схемы:
Рисунок 2 – Схема эксплуатации UAF-уязвимостей
Т.е. эксплуатация UAF-уязвимостей производится в 2 шага:
Free operation – триггерится условие, при котором производится освобождение памяти объекта, при том, что остаётся указатель на этот объект – «висящий указатель» или dangling pointer. Он будет использован в дальнейшем для обращения к уже несуществующему объекту, например, для вызова его метода или записи какого-нибудь значения.
- Realloc operation – производится реаллоцирование памяти и запись в нее нужных значений. На месте освобождённого старого объекта создаётся новый с подготовленными данными, которые будут ошибочно интерпретированы как компоненты старого объекта – уже после его освобождения.
Например, если вызывается метод объекта после его преждевременного освобождения, которое делает возможным занять освобождённую память и перезаписать указатель на таблицу виртуальных функций, таким образом, подменив адрес метода, который будет вызван. В результате, это может быть использовано для передачи управления по заданному адресу.
Ошибочное чтение данных из уже освобождённого объекта оставляет возможность взять из памяти уже своевременно подложенные туда значения, например, указатель на какую-либо функцию в кодовой секции релоцируемого ASLR модуля, т.е. осуществить утечку адреса и – далее – обход ASLR
Рассмотрим механизмы, с помощью которых осуществляется защита от подобного рода уязвимостей в браузерах IE и Edge.
Isolated Heap
Защита Isolated Heap появилась в IE вместе с июньским обновлением 2014 года (MS14-035). Основная ее цель – это защита от UAF-уязвимостей. Теперь, при выделении памяти для объекта, она выделяется не из кучи процесса, а из специальной «изолированной кучи» (что понятно из названия):
Рисунок 3 — Использование изолированной «кучи» при создании элемента CImgElement
Практически все HTML и SVG DOM-объекты (CXXXElement) используют изолированную «кучу». Они с высокой долей вероятности могут иметь UAF-уязвимости, поэтому крайне важно изолировать данные объекты.
Благодаря внедрению отдельной «кучи», у атакующего появляются проблемы со вторым шагом эксплуатации UAF-уязвимостей, а именно с реаллоцированием памяти и записью в неё своих контролируемых значений или кода, потому что такие «удобные» объекты, как строки, выделяются в другой «куче» и не могут быть использованы для подмены значений в уязвимом объекте.
Как видно на рисунке ниже, указатель на изолированную «кучу» хранится в глобальной переменной, инициализация этой «кучи» происходит в функции DllProcessAttach():
Рисунок 4 – Инициализация изолированной кучи
Если отследить XREF-ы к _g_hIsolatedHeap, можно увидеть 2 функции-аллокатора, которые её используют:
_MemIsolatedAlloc()
;_MemIsolatedAllocClear()
.
Второй вариант вызывает HeapAlloc с флагом HEAP_ZERO_MEMORY, что должно предотвращать от эксплуатации уязвимостей, использующих неинициализированную память.
Несмотря на то, что применение изолированной «кучи» помогает добиться уменьшения вероятности успешной эксплуатации UAF-уязвимостей, не всё так радужно, как кажется.
Во-первых, до сих пор присутствуют объекты, использующие «кучу» процесса – например, CStr. Во-вторых, есть теоретический путь обхода, связанный с тем, что имплементация изолированной «кучи» точно такая же, как и «кучи» процесса (нет проверки типов объектов при выделении им памяти), а также с тем, что выделенные объекты хранятся в отдельном месте.
Т.е. для обхода Isolated Heap атакующему необходимо выполнение следующих условий:
- Найти объект, который аллоцируется в изолированной «куче»;
- Данный объект должен иметь примерно тот же размер, что освобождаемый UAF-объект;
- Атакующий должен легко контролировать содержимое данного объекта.
Ребята из ZDI в 2015 году представили доклад на Black Hat и пэйпер [1], в котором предложили несколько техник обхода Isolated Heap. Они имеют под собой одно общее основание, которое как раз удовлетворяет описанным выше условиям.
Разница между техниками лишь в деталях, а именно в предварительной работе с «кучей».
Рассмотрим основную технику.
Техника обхода, основанная на использовании объекта другого типа (Type Confusion Bypass Techique)
Как было сказано ранее, изолированная «куча» точно такая же, как и обычная «куча» процесса, то есть при выделении памяти в ней не учитывается тип создаваемого объекта. Из-за этого у атакующего есть вариант заполнить освобождаемый объект объектом другого типа. Перезапись освобожденного объекта объектом другого типа приведет к условию type confusion.
Такой объект, по отношению к которому возможно создание условия type confusion, может иметь размер больший или даже меньший, чем размер объекта, которым мы собираемся перезаписывать память. Это важный факт, который позволяет атакующему контролировать определенные смещения внутри повторно используемого объекта. Например, если мы знаем, что UAF-уязвимость возникает при разыменовании указателя, по смещению 0x30, то всё, что нам нужно, это заменить освобождаемый объект таким, который содержит значение по смещению 0x30, которое мы можем контролировать.
Таким образом, последовательность действий такова:
- Триггерим условие освобождения памяти «атакуемого» объекта;
- Заменяем объект другим объектом, применяя heap spray;
- Триггерим повторное использование «атакуемого» объекта.
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).
Рисунок 5 — Функция ProtectCurrentThread()
Объект CMemoryProtector имеет следующую структуру (рисунок 6) [2]
Рисунок 6 — Структура объекта CMemoryProtector
Объект CMemoryProtector состоит из следующих полей:
- BlocksArray – структура типа SBlockDescriptorArray, которая содержит информацию об освобождаемых участках памяти (wait-list);
- IsForceMarkAndReclaim – флаг, применяемый для определения того, нужно ли вызвать операцию Reclamation Sweep (об этом ниже);
- StackHighAddress – наибольший адрес стека в потоке. Используется для операции пометки (mark operation) участка при выборке значений указателей из стека;
- StackMarkerAddress – наибольший адрес стека, когда вызывается функция
MemoryProtection::CMemoryProtector::ProtectCurrentThread()
. Применяется для определения того, полностью ли стек пройден, и, следовательно, будет ли выполняется операция Reclamation Sweep (об этом ниже).
Структура SBlockDescriptorArray содержит следующие поля:
- Blocks – это список освобождаемых участков (wait-list). Каждый элемент wail-list – это дескриптор (SBlockDescriptor), содержащий информацию о блоке памяти: его базовом адресе, размере, содержится ли он в изолированной куче или в обычной.
- TotalSize – это общий размер всех освобождаемых участков, находящихся в wait-list. Используется для определения, нужно ли выполнить операцию Reclamation Sweep;
- Count – количество освобождаемых участков, находящихся в списке Blocks;
- Max Count – максимальное количество элементов в wait-list;
- 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:
- Проверка суммарного количества участков памяти, находящихся в Wait-List, их суммарного объема;
- Если суммарное количество участков больше, чем 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 и заполняется нулями.
Рисунок 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 байт.
Рисунок 8 — Состояние wait-list после добавления в него буфера A (источник [1])
Далее, мы снова выделяем и освобождаем буфер B уже необходимого нам размера s. Во время вызова ProtectedFree()
Memory Protection увидит, что размер wait-list таков, что пора бы начать освобождать (реально освобождать) его элементы. После данной процедуры наш буфер размером s будет в wait-list.
Рисунок 9 — Состояние wait-list после добавления в него буфера B (источник [1])
Вполне возможно, что не все участки из wait-list будут освобождены и покинут его – на некоторые из них могут быть указатели в стеке (назовем такие Wi). Но, во-первых, мы можем с уверенностью сказать, что их суммарный размер гораздо меньше 100 000 байт. Во-вторых, неважно, каково их общее количество и суммарный размер для последующих шагов. Мы точно знаем, что пока на них есть указатели в стеке, эти участки будут в wait-list.
Итак, на данном этапе, мы знаем примерное состояние wait-list и готовы выполнить желаемые действия с «кучей» для эксплуатации уязвимостей.
Предположим, что мы хотим освободить блок памяти по адресу C. Вполне возможно, что даже с целью триггернуть UAF по этому адресу. Для того, чтобы блок памяти по этому адресу точно освободился, и сделал это предсказуемым образом:
- Вызываем
ProtectedFree()
для блока C. Блок C попадает в wait-list и находится там вместе с блоками Wi и блоком B размера s; - Выделяем память под большой блок памяти D размером 100 000 байт и сразу же его освобождаем. Теперь этот блок памяти попадает в wait-list, и размер wait-list становится >= 100 000 байт;
- Выделяем память под блок памяти E размером s и освобождаем его. После вызова
ProtectedFree()
для блока E, наш нужный блок C и блоки B и D попадут под процедуру Reclamation Sweep и будут освобождены.
Рисунок 10 – Итоговое состояние wait-list (источник [1])
Теперь привнесем практики в данную теоретическую стратегию. Необходимо определить объект, который выделяет буферы произвольного размера и освобождает их через ProtectedFree()
. Парни из ZDI выбрали объект CStr
– он имеет динамический размер и его можно аллоцировать из вне. Но всегда можно попытаться выбрать другой объект, благо на ProtectedFree 1100 xref-ов.
Также, анализуя MSHTML, они нашли, что CStr
использует метод CElement::var_getElementsByClassName
. Добраться до него можно через DOM-метод getElementsByClassName
на любом HTML-элементе (что прямо то, что нужно).
Во время исполнения, данный метод создает CStr
, содержащий строковые данные, которые передаются в качестве параметра getElementsByClassName
, а затем удаляет этот CStr
.
Рисунок 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), а те, в свою очередь, используются для выделения памяти объектам соответствующего размера:
Рисунок 12 – Схема MemGC Heap (источник [4])
EdgeHTML/MSHTML DOM объекты и многие другие внутренние элементы, которые рендерятся движком, управляются MemGC. И поскольку MemGC использует отдельную управляемую «кучу», Isolated Heap, описанная выше, становится не нужна и не используется.
Т.е. фактически, MemGC заменяет собой и Isolated Heap, и Memory Protector, и усложняет эксплуатацию UAF-уязвимостей на обоих шагах (смотри рисунок 1, если забыл о чем речь).
MemGC производит следующие операции:
- Выделение памяти;
- Освобождение памяти;
- «Сбор мусора» (Garbage Collector).
Рассмотрим каждую операцию поподробнее.
1. Выделение памяти.
В имплементации MemGC в EdgeHTML, когда объекту, управляемому MemGC, необходимо выделить память, производится вызов функции edgehtml!MemoryProtection::HeapAlloc<1>()
или edgehtml!MemoryProtection::HeapAllocClear<1>()
, которая, в свою очередь, производит вызов chakra!MemProtectedHeapRootAlloc()
.
Рисунок 13 – Код функции MemoryProtection::HeapAlloc<1>()
chakra!MemProtectedHeapRootAlloc()
выделит кусок (chunk) из блока (Block) в подходящем «ведре» (Bucket), и затем на такой кусок памяти ставится флаг “root”. “Root”-флаг, в терминологии MemGC, такой объект/кусок памяти, который адресуется напрямую (directly referenced) программой и поэтому не должен попасть под «сборку мусора».
2. Освобождение памяти.
Когда необходимо освободить память объекта, вызывается функция edgehtml!MemoryProtection::HeapFree()
, которая, в свою очередь, вызывает chakra!MemProtectHeapUnrootAndZero()
.
Рисунок 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:
MemGC имеет свою, отдельную от «кучи» процесса, со сложной структурой, и управляет операциями и выделения, и освобождения памяти – т.е. имеет свой менеджер «кучи»;
MemGC сканирует не только стек и регистры, но и саму «кучу» на предмет ссылок на освобождаемый блок памяти (скорее всего, в ответ на технику обхода ребят из ZDI :) );
Пороговое значение, когда стартует процедура Reclamation Sweep, определяется динамически, в отличие от Memory Protection, где пороговое значение имеет константное значение. Также сама процедура очистки памяти частично производится в другом потоке.
- 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.
Рисунок 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». Это сохраняет возможность использования системных компонентов контейнизированными процессами, например, загрузки библиотек из системной директории.
Рисунок 17 — права доступа к системной директории для всех контейнизированных процессов
Таким образом, для приложения всё же остаётся возможность читать область файловой системы и, аналогичным образом, реестра, в которой хранятся объекты операционной системы и установленного ПО (для “Program Files” добавлен тот же SID). Это, как отмечает исследователь Mark Yanson [6], может применяться для извлечения такой информации, как лицензионные ключи, конфигурационные файлы установленного ПО, также возможно использование утечки об установленном ПО и версиях его компонентов для будущих атак.
Можно выделить основные способы побега из изолированного окружения и выполнения собственного кода вне контейнера:
Эксплуатация бинарных уязвимостей процесса-брокера, сервисных процессов, драйверов и ядра операционной системы;
- Использование пробелов в имеющихся правах доступа, например, возможности запуска с высокими привилегиями доверенного системой приложения, которое определённым способом можно использовать для выполнения запланированных действий, например, через запуск произвольного скрипта.
В качестве примера можно привести недавно попавший во всеобщее внимание драйвер capcom.sys, являющийся запчастью от одной компьютерной игры. Этот драйвер предоставляет одну «замечательную» функцию:
Рисунок 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-кода в машинный код, что положительно сказывается на производительности скриптов на web-страницах.
С точки зрения безопасности, приложения 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; }
Рисунок 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 — можно наблюдать процесс эволюционного усложнения техник атаки и механизмов защиты, непрерывное противоборство щита и меча. Баги остаются, но эксплуатация уязвимостей становится всё сложнее — увеличивается стоимость использования существующих ошибок. Однако, природа этих ошибок не меняется, поскольку применяются те же языки и среда исполнения. Не изменяется сам код уязвимых частей браузера, но накладываются механизмы, направленные на предотвращение существующих путей эксплуатации багов.
Источники
- 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
- https://securityintelligence.com/understanding-ies-new-exploit-mitigations-the-memory-protector-and-the-isolated-heap/
- https://blogs.technet.microsoft.com/srd/2016/01/12/triaging-the-exploitability-of-ieedge-crashes/
- 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
- https://github.com/zenhumany/hitcon2015
- https://www.blackhat.com/docs/asia-14/materials/Yason/WP-Asia-14-Yason-Diving-Into-IE10s-Enhanced-Protected-Mode-Sandbox.pdf
- http://users.ics.forth.gr/~elathan/papers/ndss15.pdf
- https://habrahabr.ru/company/dsec/blog/305960/
- 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
Авторы
- Козорез Максим
- Турченков Дмитрий / @d-t
Комментарии (3)
Geork
05.10.2016 16:21Интересная статья, только очень объемная вышла.
Мне кажется, что разделы про «3. Реализация Sandbox в IE и Edge» и «4. JIT Hardening» можно было бы вынести в отдельную статью. Они логически мало связаны с первыми двумя пунктами.
Xalium
Эм. А что, сборщики мусора кроме Memory бывают другие?
d-t
Это же терминология MS, переименовывать их технологии нам ни к чему :)