Мы продолжаем цикл статей о механизмах защиты браузеров от эксплойтов:
Давайте заглянем под капот браузера Chrome и выясним, что есть в его анти-эксплоит арсенале.
Задействуются общие механизмы, предоставляемые современными версиями Windows и компиляторами:
- Рандомизация баз модулей (Image ASLR);
- HighEntropy ASLR (использует 64-битные адреса, Windows 8+);
- Force ASLR (принудительная рандомизация баз модулей);
- Рандомизация аллокаций (HeapAlloc, VirtualAlloc; Windows 8+):
Функции выделения памяти выбирают базу возвращаемой памяти тремя способами:
- выбирая самый нижний адрес;
- выбирая самый верхний адрес;
- выбирая адрес относительно заданной базы.
На системах до Windows 8 первые два способа никак не рандомизировались и потому
позволяли выделить память по предсказуемым адресам.
- Data Execution Prevention:
неисполняемая память (стеки, кучи, секции данных модулей и прочее). - Stack cookies (канарейки):
в прологе функции в стек помещается рандомное значение перед адресом возврата, которое проверяется перед возвратом управления. Усложняет эксплуатацию переполнений буфера в стеке. - Safe Structured Exception Handling (SAFESEH):
предотвращает перехват управления через перезапись содержимого управляющих структур SEH_EXCEPTION_REGISTRATION_RECORD. Перед тем, как передать управление на обработчик, указанный в этой структуре, происходит его проверка. Если исполняемый модуль, которому принадлежит обработчик, собран с /SAFESEH, то происходит проверка адреса по таблице SEH обработчиков этого модуля (во время линковки они вшиваются в бинарник). - Structured Exception Handler Overwrite Protection (SEHOP):
дополняет предыдущую технику, поскольку сама по себе она была далеко не идеальна. Во-первых, она требовала рекомпиляции приложений, во-вторых — если обработчик располагался в модуле без /SAFESEH или вообще вне какого-либо модуля — она попросту не работала. Потому компания Microsoft добавила технику уровня OS — SEHOP. Её суть заключается в проверке целостности SEH-цепочки: при старте потока в цепь SEH-обработчиков добавлялась "канарейка", наличие которой проверялось перед вызовом каких-либо обработчиков. Как правило, при перезаписи адреса обработчика исключения, перезаписывается ещё и указатель ну следующую структуру в цепочке, что разрывает цепь и делает "канарейку" недосягаемой. - Control Flow Guard (CFG):
предотвращает перехват потока управления через перезапись адресов в данных программы, включая таблицы виртуальных функций, проверяя адреса на командах косвенных переходов. Валидные адреса функций модуля хранятся в битовой карте, которая строится компилятором. Подробнее. - Windows mitigation policies:
ряд ограничений, применяемых Windows 8+ к процессам, из которых в Chrome использует для своей песочницы следующие:
- Relocate Images
- Heap Terminate
- Bottom-up ASLR
- High-entropy ASLR
- Strict Handle Checks
- Disable Font Loading
- Disable Image Load from Remote Devices
- Disable Image Load of "mandatory low" (low integrity level)
- Extra Disable Child Process Creation
Мы рассмотрим внутренние механизмы браузера:
- Аллокатор PartitionAlloc:
позволяет разнести объекты разных размеров и типов по разным хипам, тем самым урезав возможности атакующего в выборе объектов-кандидатов на освобождённую память. Помогает ловить heap over/underflow-ы как внутри самого хипа, так и на его границах. Подробнее — далее. - Трассирующий сборщик мусора для C++ — Oilpan;
- JIT-компилятор JavaScript:
включает в себя ряд механизмов, затрудняющих его использование для генерации элементов эксплойта. - Sandbox:
система безопасности браузера, предназначенная для снижения ущерба от компрометации его компонентов.
Введение про UaF и повреждения памяти
Браузер предоставляет некоторый API для управления объектами-компонентами документа — веб-страницы. Содержимое этого документа представляется в виде дерева узлов, каждый узел которого — элемент, атрибут, текстовый, графический или любой другой объект — это представление DOM. Узлы этого дерева — "ноды" — могут создаваться, уничтожаться и изменяться средствами JavaScript. Существование множества взаимозависимых сложных объектов является предпосылкой для наличия багов, а удобство управления этими объектами с помощью JavaScript — способом использования этих багов.
Баги, приводящие к повреждению объектов в куче, очень часто являются отправной точкой для всей последовательности действий, которую выполняет браузерный эксплойт. Уязвимости, вызванные ими, можно поделить на две большие категории:
- temporary (временные) — возникающие в результате попытки использования объекта вне периода его существования (вызов метода объекта после его освобождения, например);
- spatial (пространственные) — порождённые ошибочным доступом за пределы расположения объекта в памяти (обращение к элементу массива по неверному индексу, например).
Для предотвращения нацеленного воздействия на другие объекты в памяти, используя уязвимый объект, предлагается рассмотреть ряд механизмов, реализуемых в аллокаторах — менеджерах кучи.
Blink — рендерер Chrome — задействует два собственных аллокатора: PartitionAlloc и Oilpan (он же BlinkGC). Есть два отдельных редких случая:
- discardable memory — выгружаемая память, которая используется для кеширования больших графических объектов; освобождается, когда свободная память на устройстве исчерпывается;
- malloc, от которого хотят целиком избавиться.
PartitionAlloc
PartitionAlloc используется для тех объектов, для которых не предполагается автоматическая сборка мусора. Это его ключевое отличие от Oilpan, о котором позже.
Дизайн PartitionAlloc включает в себя такие элементы, имеющие отношение к безопасности объектов в памяти:
- разбиение на разделы аллокаций объектов разных типов. Такая изоляция позволяет сократить возможности, которые будут у атакующего в случае эксплуатации багов, приводящих к чтению или записи данных за пределы уязвимого объекта в объект другого типа, например, линейного переполнения буфера в этой куче.
- LayoutObjects — тесно связанные с DOM нодами объекты, используемые рендерером для отображения элементов страницы.
- Buffers — буферы для массивов, строк, битвекторов.
- FastMalloc — тут достаточно обширный список разнообразных объектов.
- хранение метаданных кучи в отдельном регионе — т.е. они также изолируются для предотвращения их повреждения;
- указатель на freelist (список свободных блоков памяти) защищается от частичной перезаписи, либо разыменования;
- большие аллокации размещаются отдельно и обрамляются guard pages — страницами памяти перед началом и после конца выделенного блока, при доступе к которым процесс срочно прервётся.
Проиллюстрировать это развитие можно, взглянув на эксплойт, продемонстрированный Pinkie Pie на Mobile Pwn2Own 2013. Здесь автор использует integer overflow, возникающую в конструкторе типизированного массива. Условия таковы: выделяется буфер массива Float64, при этом последовательная инициализация всех его элементов выходит за его конец и запись произвольных значений типа Float64 может быть продолжена сколь угодно дальше подряд каждые 8 байт памяти после выделенного буфера. Для массива подходящей длины потребуется буфер большого размера, PartitionAlloc делегирует его выделение системному аллокатору — dlmalloc на Android. Pinkie Pie перезаписывал заголовок следующей аллокации в куче, изменяя её размер, освобождал размещённый там объект (добавляя блок нужного размера в freelist) и таким образом добился выделения следующего объекта заданного размера на этом месте — куда можно продолжить писать произвольные значения.
Отсюда мы видим, от чего разработчики защищают метаданные аллокатора, зачем большие аллокации обрамляются guard pages, которые не дадут выйти за пределы буфера таким манером, почему буферы типизированных массивов изолируются от остальных объектов.
Oilpan
Oilpan предлагает автоматическую сборку мусора. Эта система снимает с разработчиков необходимость ручного управления памятью, являющуюся причиной ошибок класса use-after-free. Кратко напомним суть уязвимостей, вызванных такими ошибками: происходит преждевременное освобождение объекта, то есть освобождение, после которого он может быть использован.
В качестве примера посмотрим на то, что нам удалось найти на багтрекере проекта: https://bugs.chromium.org/p/chromium/issues/detail?id=69965 Здесь рассматривается UaF баг, связанный с классом Geolocation. Происходит следующее: объекты класса Geolocation уничтожаются при обновлении страницы, однако, связанные с ними запросы на разрешение геолокации не были предварительно отменены, из-за чего остаются висящие указатели в менеджере запросов, и попытка их отмены при закрытии вкладки в будущем заканчивается ошибочным доступом к освобождённому объекту геолокации. Патч для этого бага добавляет метод pageDestroyed в класс Geolocation, который, видимо, должен был устроить правильный порядок освобождения объектов страницы. С тех пор класс Geolocation претерпел изменения, связанные с внедрением Oilpan, сейчас он управляется этой системой автоматически.
Эксплуатация подобных багов состоит из этапов: выполнение условий, в которых уязвимый объект будет удалён из кучи, размещение на освободившейся от этого объекта памяти контролируемых данных — создание "фейкового объекта" таким образом и выполнение условий, приводящих к использованию элементов этого фейкового объекта как собственных членов. Для того, чтобы предотвратить вторую часть этого действия — изготовление фейкового объекта посредством размещения на освободившейся памяти — разработчики Chrome изолируют регионы памяти, в которых живут объекты разных типов. Посмотрим, как это делается:
// Override operator new to allocate Node subtype objects onto
// a dedicated heap.
GC_PLUGIN_IGNORE("crbug.com/443854")
void* operator new(size_t size) { return allocateObject(size, false); }
static void* allocateObject(size_t size, bool isEager) {
ThreadState* state =
ThreadStateFor<ThreadingTrait<Node>::Affinity>::state();
const char typeName[] = "blink::Node";
return ThreadHeap::allocateOnArenaIndex(
state, size,
isEager ? BlinkGC::EagerSweepArenaIndex : BlinkGC::NodeArenaIndex,
GCInfoTrait<EventTarget>::index(), typeName);
}
chromium//src/third_party/WebKit/Source/core/dom/Node.h
В переопределённом new вызывается allocateObject, аргумент isEager == false, поэтому — далее, ThreadHeap::allocateOnArenaIndex примет третьим аргументом arenaIndex значение BlinkGC::NodeArenaIndex — индекс "арены" (региона памяти), в котором будем выделять объект:
inline Address ThreadHeap::allocateOnArenaIndex(ThreadState* state,
size_t size,
int arenaIndex,
size_t gcInfoIndex,
const char* typeName) {
ASSERT(state->isAllocationAllowed());
ASSERT(arenaIndex != BlinkGC::LargeObjectArenaIndex);
NormalPageArena* arena =
static_cast<NormalPageArena*>(state->arena(arenaIndex));
Address address =
arena->allocateObject(allocationSizeFromSize(size), gcInfoIndex);
HeapAllocHooks::allocationHookIfEnabled(address, size, typeName);
return address;
}
chromium//src/third_party/WebKit/Source/platform/heap/Heap.h
Какие ещё регионы определены?
enum HeapIndices {
EagerSweepArenaIndex = 0,
NormalPage1ArenaIndex,
NormalPage2ArenaIndex,
NormalPage3ArenaIndex,
NormalPage4ArenaIndex,
Vector1ArenaIndex,
Vector2ArenaIndex,
Vector3ArenaIndex,
Vector4ArenaIndex,
InlineVectorArenaIndex,
HashTableArenaIndex,
FOR_EACH_TYPED_ARENA(TypedArenaEnumName) LargeObjectArenaIndex,
// Values used for iteration of heap segments.
NumberOfArenas,
};
* * *
// List of typed arenas. The list is used to generate the implementation
// of typed arena related methods.
//
// To create a new typed arena add a H(<ClassName>) to the
// FOR_EACH_TYPED_ARENA macro below.
#define FOR_EACH_TYPED_ARENA(H) H(Node) H(CSSValue)
#define TypedArenaEnumName(Type) Type##ArenaIndex,
chromium//src/third_party/WebKit/Source/platform/heap/BlinkGC.h
Здесь мы видим: в памяти будут разделены объекты классов Node, CSSValue, HashTables, Vectors; остальные объекты этим аллокатором распределяются по регионам по размеру.
Перейдём к рассмотрению ключевого свойства Oilpan/BlinkGC — автоматической сборке мусора. Объекты, которые должны управляться этой системой, наследуются от шаблонного класса GarbageCollected, GarbageCollectedFinalized или GarbageCollectedMixin. Члены-объекты этих классов, располагаемые в куче, представляются шаблонными классами Member или WeakMember в зависимости от требуемой семантики.
Алгоритм, выполняющий сборку мусора, является mark-and-sweep алгоритмом, и состоит из двух основных этапов:
- mark — производится обход графа объектов, для этого вызывается метод trace() каждого, отмечающий достижимые объекты из данного; отправные точки для такого обхода могут выбираться в зависимости от текущего состояния программы в двух вариациях:
- precise — выбирается тогда, когда потоки программы остановлены в конце циклов обработки сообщений. Это гарантирует отсутствие "сырых" (raw) указателей в стеках потоков — значит, можно исходить из специальных глобальных указателей "persistent handles";
- conservative — осуществляется в тех случаях, когда есть необходимость пройти стеки потоков и взять оттуда возможные указатели.
- sweep — недостижимые объекты, выявленные на предыдущем этапе, отмечаются на освобождение и будут уничтожены, когда понадобится память. Вследствие недетерминированного порядка отложенного удаления объектов из кучи, в момент вызова деструктора какого-либо объекта уже нельзя полагаться на наличие в куче с ним связанных объектов. Поэтому разработчики добавляют специальный метод — pre-finalizer, вызываемый между этими этапами для недостижимых объектов, когда они все ещё живы.
JIT Hardening
Если бы инструкции, получаемые при сборке динамически генерируемого кода, не претерпевали изменений, атакующий получил бы мощный примитив, позволяющий создавать шеллкод в исполняемой памяти. Чтобы избежать этого, был введён ряд противомер:
NOPs
В тело программы случайным образом вставляются NOP'ы (инструкции, не меняющие состояние окружения, единственная цель которых — занять место) различных размеров — от одного до восьми байт. Они нужны, чтобы исключить возможность появления константных последовательностей байт в собранном коде.
void Assembler::Nop(int n) { // The recommended muti-byte sequences of NOP instructions from the Intel 64 // and IA-32 Architectures Software Developer's Manual. // // Length Assembly Byte Sequence // 2 bytes 66 NOP 66 90H // 3 bytes NOP DWORD ptr [EAX] 0F 1F 00H // 4 bytes NOP DWORD ptr [EAX + 00H] 0F 1F 40 00H // 5 bytes NOP DWORD ptr [EAX + EAX*1 + 00H] 0F 1F 44 00 00H // 6 bytes 66 NOP DWORD ptr [EAX + EAX*1 + 00H] 66 0F 1F 44 00 00H // 7 bytes NOP DWORD ptr [EAX + 00000000H] 0F 1F 80 00 00 00 00H // 8 bytes NOP DWORD ptr [EAX + EAX*1 + 00000000H] 0F 1F 84 00 00 00 00 00H // 9 bytes 66 NOP DWORD ptr [EAX + EAX*1 + 66 0F 1F 84 00 00 00 00 // 00000000H] 00H ... }
Constant Folding
Арифметические выражения подсчитываются (сворачиваются) во время сборки кода:
<script> x = 0x123 + 0x567; // == 0x68A </script>
mov rax,68A00000000h
Constant Blinding
Только значения до двух байт хранятся в коде без изменений. Например:
<script> a = 0x1234; </script>
Будет собран в:
... mov rax,123400000000h ...
Константы бОльшего размера ксорятся со случайным значением (jit_cookie):
void MacroAssembler::SafeMove(Register dst, Smi* src) { ... if (IsUnsafeInt(src->value()) && jit_cookie() != 0) { if (SmiValuesAre32Bits()) { // JIT cookie can be converted to Smi. Move(dst, Smi::FromInt(src->value() ^ jit_cookie())); Move(kScratchRegister, Smi::FromInt(jit_cookie())); xorp(dst, kScratchRegister); } else { DCHECK(SmiValuesAre31Bits()); int32_t value = static_cast<int32_t>(reinterpret_cast<intptr_t>(src)); movp(dst, Immediate(value ^ jit_cookie())); xorp(dst, Immediate(jit_cookie())); } } else { Move(dst, src); } }
Guard Pages
Буффер, содержащий собранный JIT код, обрамляется PAGE_NOACCESS страницами, чтобы предотвратить его перезаписывание, если случится переполнение хипа в близлежащих аллокациях.
JIT Page Randomization
Расположение памяти, где будет размещаться собранный JIT код, зачастую (но не всегда) рандомизируется. Если свободный адрес не будет угадан с трёх попыток, Chrome дает системному аллокатору самому выбрать адрес для создающегося буффера.
static void* RandomizedVirtualAlloc(size_t size, int action, int protection) { ... if (use_aslr && (protection == PAGE_EXECUTE_READWRITE || protection == PAGE_NOACCESS)) { // For executable pages try and randomize the allocation address for (size_t attempts = 0; base == NULL && attempts < 3; ++attempts) { base = VirtualAlloc(OS::GetRandomMmapAddr(), size, action, protection); ... } void* OS::GetRandomMmapAddr() { ... static const uintptr_t kAllocationRandomAddressMin = 0x0000000080000000; static const uintptr_t kAllocationRandomAddressMax = 0x000003FFFFFF0000; ... uintptr_t address; platform_random_number_generator.Pointer()->NextBytes(&address, sizeof(address)); address <<= kPageSizeBits; address += kAllocationRandomAddressMin; address &= kAllocationRandomAddressMax; return reinterpret_cast<void *>(address); }
Sandboxing
Chrome реализует многопроцессную архитектуру, которая позволяет назначить различные привилегии и ограничения для разных частей браузера. Единица, которой оперирует песочница — это процесс. Минимальная конфигурация песочницы Chrome включает в себя два процесса: один привилегированный, называемый брокером, и один (или более) изолированный. Например, как изолированные процессы выделяются рендереры — инстансы движка Blink, отрисовывающие HTML документы. Рендереры запускаются для вкладок веб-страниц и для браузерных расширений. Риск компрометации рендерера велик, потому что внутри него происходит интерпретация разнородного кода, загружаемого из любых источников, которые пользователь будет серфить. Кроме рендереров, отдельные процессы — это контейнеры плагинов (flash), вспомогательные — crash репортер, gpu акселератор для графики. Рендереры и прочие используют IPC (inter-process communication) для запросов доступа к ресурсам из брокера. Они делегируют такие вызовы API брокеру посредством IPC, брокер сверяет делегированный вызов с политикой, специфицируемой для каждого изолированного процесса, разрешенные вызовы исполняются, а результат возвращается через тот же IPC-механизм обратно.
Модель песочницы Chrome, источник
Средства Windows, на которых основана изоляция процессов Chrome:
- Access token (маркер доступа)
- Job object
- Desktop object
- Integrity Levels (уровни доверенности)
- AppContainer
- Windows Mitigation Policies
Стоит ещё раз заметить, что есть взаимодействие между изолированными процессами и привилегированным брокером, значит выход из песочницы может быть осуществлён не только через слабости перечисленных выше системных механизмов, но и через эксплуатацию уязвимости брокера, достижимую через IPC. Такой подход был продемонстрирован Pinkie Pie на Mobile Pwn2Own 2013, в связке с RCE, которую мы уже рассмотрели ранее в этой статье: см. Part II, линк.
Access token
Маркер доступа содержит SID-ы — идентификаторы субъектов доступа: пользователей и групп. Изолированным процессам устанавливается маркер, содержащий NULL SID (S-1-0-0), для которого в системе едва ли обнаружится объект, обладающий ACL, который может быть получен.
Каким образом такой процесс получает хендл какого-либо файла? На API-функции (тут — ZwCreateFile) устанавливаются обычные хуки, вызов перенаправляется через модули песочницы брокеру, брокер открывает файл и дублирует хендл обратно.
Job object
Включает в себя некоторые специальные ограничения, связанные с ресурсами, не управляемыми ACL. Этой сущностью запрещается создание дочерних процессов, чтение/запись буфера обмена и прочее. Подробнее.
Desktop object
Для изолированных процессов Chrome создаётся отдельный объект рабочего стола для того, чтобы предотвратить взаимодействие с другими процессами, передавая сообщения их окнам.
Чем опасно такое взаимодействие? Это старая слабость архитектуры Windows, которая использовалась для исполнения т.н. Shatter Attack. Оконные сообщения вплоть до Vista были анонимны и могли посылаться к любому процессу. Особенно пикантную возможность давало сообщение WM_TIMER с адресом функции, по которому передаст управление целевой процесс без какого-либо участия со своей стороны.
В Vista и последующих версиях была ограничена передача сообщений между процессами исходя из их Integrity level (уровня доверенности): User Interface Privilege Escalation. Менее привилегированные процессы больше не могут отправлять сообщения более привилегированным.
Integrity levels, AppContainer
Механизмы разграничения доступа Windows, про них мы писали в предыдущей статье.
Windows Mitigation Policies
Набор новых security-фич ОС Windows, которые могут быть включены для процессов, частично перекрывают возможности EMET (Enhanced Mitigation Experience Toolkit). Здесь такие возможности, как отключение загрузки шрифтов (парсятся в ядре Windows), модулей в свой процесс, также — создание процессов.
Запрет на создание процессов пересекается с тем, что уже сделано в Job object для изолированных процессов Chrome, но в Job object был выявлен один забавный пробел. Обход заключается в вызове API AllocConsole, которая создаёт консольное окно для программы, а для консольного окна системой будет запущен хост-процесс conhost.exe. Подробнее об этих политиках и их слабостях можно почитать у исследователя James Forshaw в презентации.
ProcessSystemCallDisablePolicy / Win32k.sys Lockdown
Эту политику мы рассмотрим отдельно.
Графическая подсистема Windows исправно поставляет LPE-уязвимости уже много лет. В случае с браузерными атаками, их используют после RCE. Получив исполнение кода в процессе-рендере, эксплоит повышает привилегии через уязвимость компонента Windows, тем самым получая полный доступ к системе. Проиллюстрировать это можно хорошо документированным эксплойтом для уязвимости kernel pool corruption в win32k, который продемонстрировали исследователи MWR Labs на Pwn2Own 2013 в связке с RCE в Chrome: статья, презентация.
Уязвимость была обнаружена в обработчике вызова, который используется для передачи сообщений между окнами: W32KAPI LRESULT NtUserMessageCall( IN HWND hwnd, IN UINT msg, IN WPARAM wParam, IN LPARAM lParam, IN ULONG_PTR xParam, IN DWORD xpfnProc, IN BOOL bAnsi);
. Последний параметр bAnsi определяет кодировку текста сообщения, которое копируется из вызвавшего сервис процесса в память ядра: WCHAR или ASCII — 2 или 1 байт на символ. И этот параметр интерпретировался по-разному при аллокации буфера в пуле ядра и при копировании сообщения в буфер — сначала как bool, потом как битовая маска. Это дало возможность переполнить буфер, скопировав в него в два раза больше байт. Таким образом манипулируя данными в ядре, добились исполнения шеллкода в ring0, шеллкод обнулил ACL привилегированного процесса winlogon.exe, то есть оставил его беззащитным перед тривиальной инъекцией кода. Profit!
Проблема win32k
Разработка этой простой, на первый взгляд, противомеры заняла много времени и сил, так как потребовала модификации не только кода самого Chrome, но и координации с командами разработки Adobe Flash Player и Pdfium (lockdown нужен не только для процессов-рендеров, но и для PPAPI процессов, где исполняются плагины). Инженеры Google добавили в стек общения Flash с win32k свой брокер. На текущий момент полноценная реализация lockdown существует только для Windows 10, поскольку сама операционная система предоставляет возможности фильтрации системных вызовов. Очень рекомендуем ознакомиться с документом, описывающим проблемы и решения этого средства защиты.
Заключение
Сильная сторона Chrome — это, конечно, песочница. Здесь мы видим широкий набор способов ограничения полномочий для смягчения последствий эксплуатации уязвимостей в кодовой базе рендерера. Набор этих способов зависит от того, что предлагает нам операционная система, в свежих версиях Windows было добавлено много нового и интересного. Кроме того, большое внимание уделено управлению динамической памятью, которая остаётся на заднем плане при создании новых фич браузера для современного веба, но имеет первостепенное значение с точки зрения безопасности. Разработчики внедрили прогрессивную систему сборки мусора и получили новые свойства среды, в которой исполняются компоненты браузера, не характерные для обычных C++ приложений.
HonoraryBoT
Good job, парни