Spectre-v2класс атак через спекулятивное исполнение, при котором процессор предсказывает цель косвенного перехода и выполняет код по предсказанному адресу до проверки. Классический вариант такой атаки требует подобрать адрес-алиас, чтобы заставить предсказатель прыгнуть туда, куда нужно атакующему. BTR (Branch Target Reuse) устроен иначе: он опирается на то, что процессор после самомодификации кода восстанавливает архитектурную когерентность, но не обязательно инвалидирует устаревшие записи предсказания косвенных переходов. В JIT-движках такие устаревшие цели переживают исходный код и позже применяются к новому коду, занявшему ту же память, — это даёт примитив спекулятивного execute-after-free. Разберём, как именно это работает и почему защиты закрывают проблему лишь частично.
Что такое BTR и чем он отличается от классического Spectre-v2
BTR — атака класса Spectre-v2, нацеленная на JIT-компиляторыкомпиляторы, которые порождают машинный код прямо во время выполнения программы. Её ключевая идея: современные процессоры восстанавливают архитектурную когерентность кода после самомодификации, но не обязательно инвалидируют устаревшие записи BTBBranch Target Buffer — буфер предсказания целей косвенных переходов, где процессор хранит адреса, по которым ранее прыгали ветвления. В JIT-движках эти устаревшие цели могут пережить исходный код и быть повторно использованы при повторном заполнении кэша кода, давая примитив спекулятивного execute-after-free.
Отличие от классического Spectre-v2 с BTB-инъекцией через alias-адреса принципиальное: здесь не нужно подбирать адрес-алиас. Устаревшая запись BTB сама переживает удаление кода и применяется к новому коду, занявшему ту же память. Это позволяет перехватить спекулятивное управление на новый код по устаревшим смещениям или достичь невыровненных гаджетов.
Почему мишень — именно JIT-компиляторы: они создают машинный код во время выполнения, а BTR использует рассинхронизацию между таким кодом и предсказателем переходов. BTR затрагивает JIT-движки в веб-браузерах, языковых средах и ядре операционной системы, у нескольких производителей процессоров.
Модель угроз
Для SpiderMonkey модель угроз простая и реалистичная: вредоносная веб-страница управляет JavaScript, исполняемым в браузере самой жертвы, и через BTR пытается извлечь её чувствительные данные. Поскольку изоляция сайтов ещё не полностью развёрнута, другие вкладки могут оказаться в том же адресном пространстве, и их данные становятся целью.
Для GraalVM модель угроз — это код, который вы намеренно запускаете в песочнице, например плагин или пользовательский скрипт. В самом строгом режиме песочница явно усилена против атак через транзиентное исполнение: каждое обращение гостя к памяти маскируется, чтобы оно не могло выйти за пределы арены песочницы.
В эксплойте на Linux cBPF атакующий создаёт две cBPF-программы — тренировочную и целевую — и устанавливает их как seccomp-фильтрыфильтры системных вызовов, которые ядро исполняет для ограничения доступных процессу операций, то есть исполняет код в ядре.
Механика BTR пошагово
Атакующий заставляет JIT-движок выделить новый участок кода — тренировочный чанкучасток исполняемой памяти, который JIT-движок выделяет под сгенерированный код и позже может освободить и выдать заново — и обучает косвенный переход, прыгая на его точку входа. Затем тренировочный чанк освобождается, а на его место выделяется целевой чанк, частично переиспользующий тот же адрес. Когда косвенный переход срабатывает снова, процессор использует устаревшую запись BTB и спекулятивно прыгает на старую точку входа тренировочного чанка — это даёт перехват спекулятивного потока управления и раскрытие секретных данных.
В эксплойте на Linux cBPF тренировочная программа сначала устанавливается и исполняется, чтобы обучить косвенный переход на её точку входа. Затем тренировочная программа удаляется, а целевая устанавливается в той же области памяти. Устаревшая запись BTB продолжает указывать на старое расположение кода, хотя JIT-кэш теперь содержит другую cBPF-программу.
Гаджет закодирован в 4-байтовых непосредственных значениях целевой программы. Те же байты можно переинтерпретировать по невыровненному смещению и спекулятивно выполнить гаджет раскрытия до того, как процессор восстановит архитектурную согласованность. Эксплойт утекает 8 байт в секунду — при аккуратном отслеживании цепочек указателей этого достаточно, чтобы добраться до секрета.
Условия срабатывания устаревшей записи
Чтобы запись BTB, созданная тренировочным чанком, была использована целевым, целевой чанк должен частично переиспользовать тот же адрес, что и тренировочный. Устаревшая запись BTB продолжает указывать на старый адрес, хотя кэш JIT-кода теперь содержит другую программу. При повторном срабатывании косвенного перехода процессор использует эту устаревшую запись и спекулятивно прыгает на старую точку входа тренировочного чанка.
Эксплойт на Linux cBPF: обход constant-blinding
Ядро Linux может включать constant blindingзащиту, при которой непосредственные значения в JIT-коде маскируются, чтобы атакующий не мог закодировать в них полезные байты в рантайме опцией bpf_jit_harden. При её включении применяется blinding к immediate-значениям, чтобы предотвратить прямой JIT spraying.
BTR обходит это, кодируя контролируемые атакующим байты не в immediate-значениях, а в байтах смещения прямых переходов — по jump-offsetтехнике, при которой управляемые атакующим байты размещаются в байтах смещения прямых переходов, а не в непосредственных значениях. Одна и та же последовательность байт декодируется как валидная цепочка cBPF-переходов при одном выравнивании и как «dispatch»-гаджет, когда исполнение повторно входит на два байта позже.
максимальное смещение прямого перехода: 0x014fc5Поскольку максимум равен 0x014fc5, атакующий контролирует только младшие два байта смещения, а старшие остаются нулевыми. Чтобы остаться в невыровненном потоке исполнения, инструкции подбираются так, чтобы следующая инструкция потребляла хотя бы один из оставшихся байтов. В невыровненном представлении те же байты декодируются как гаджет, который загружает контролируемые атакующим данные, передаёт их в rdi и rdx, а затем переходит к disclosure-гаджету через контролируемый косвенный переход.
Цепочка end-to-end эксплойта на cBPF
Атакующий создаёт тренировочный и целевой чанки и устанавливает их как seccomp-фильтры. Сначала устанавливается и выполняется тренировочная программа, чтобы обучить предсказатель косвенных переходов на её точку входа. Затем тренировочная программа удаляется, а на её место в той же области памяти устанавливается целевая; устаревшая запись BTB всё ещё указывает на старый адрес кода. Байты гаджета, закодированные в 4-байтовых immediate-значениях целевой программы, переинтерпретируются по невыровненному смещению и спекулятивно исполняются до восстановления архитектурной согласованности.
Утечка составляет 8 байт в секунду. С помощью аккуратного отслеживания цепочек указателей атакующему достаточно малого объёма данных, чтобы добраться до секрета. В демонстрации он утекает хеш пароля root из процесса «su»: сначала запускается «su root», загружающий хеш в память; затем обходится список задач ядра Linux в обратном порядке по указателям «prev», для каждой task struct проверяется совпадение PID с жертвой; после нахождения нужной task struct утекается структура «mm» и начинается обход таблиц страниц, где для каждой отображённой страницы проверяется, содержит ли она хеш пароля root.
Эксплойт на SpiderMonkey
Для SpiderMonkey BTR опирается на три особенности дизайна, из которых строятся суб-примитивы.
Первый — надёжное повторное использование исполняемой памяти на границе JS/Wasm. Аллокатор при освобождении непрерывного диапазона возвращает курсор выделения к его началу, поэтому только что освобождённые блоки снова выдаются первыми. Это лишь минимально рандомизировано — в резком контрасте с практиками усиления, которые намеренно рандомизируют размещение, чтобы затруднить атаки через повторное использование памяти.
Второй — запись произвольных байтов в исполняемую память через литеральный пул. Инструкции с многобайтовыми константами, такие как f64.const и v128.const, компилируются в PC-relative загрузки, чьи константы лежат в пуле внутри буфера исполняемого кода. Это даёт удобное «сопло для распыления»: можно записывать произвольные выбранные атакующим байты в исполняемую память, включая валидные 4-байтово выровненные последовательности инструкций даже на Arm64. Поскольку SpiderMonkey редко применяет ослепление констант — снова ради производительности, — планка для построения целевых гаджетов резко снижается.
Третий — жертвенная ветвьветвь, которую атакующий выбирает мишенью для обучения предсказателя: она годится потому, что её ядро load-jump не имеет защиты от Spectre-v2, а значит, спекулятивное исполнение по устаревшей записи предсказателя не будет остановлено. Каждый JS-вызов, который сопротивляется inline caching и инлайнингу, направляется в общий call stub IonGenericCallStub, чьё ядро load-jump не имеет Spectre-v2 hardening.
Суб-примитивы связываются в proof of concept: на процессорах Intel записи BTB переживают полный цикл освобождения и повторного выделения, что даёт оценочную скорость утечки порядка десятков байт в секунду. Для превращения в end-to-end браузерный эксплойт требуется дополнительная работа — например, удлинение окна транзиентного выполнения и построение скрытого канала, терпимого к сниженной точности таймеров.
Эксплойт на GraalVM
GraalVM JIT-компилирует исполняемый код, а в строгом режиме маскирует каждое обращение гостя к памяти, чтобы оно не могло выйти за пределы арены песочницы. Атакующие использовали GraalPy — Python-реализацию GraalVM — чтобы генерировать функции, чья скомпилированная форма содержит доступ к массиву, предварённый такой маской. С помощью BTR они спекулятивно перепрыгивают мимо маски прямо в загрузку: доступ становится неограниченным и читает за пределами арены.
Собственный гаджет строить не нужно — компилятор уже выдаёт подходящий, а небольшой объём фиктивного кода направляет контролируемые атакующим значения в нужные регистры.
Повторное использование памяти в GraalVM почти бесплатно. В отличие от SpiderMonkey, GraalVM не резервирует и не управляет собственным регионом для JIT-кода, а делегирует каждое выделение непосредственно ядру. Ядро, применяя мало собственной рандомизации, склонно возвращать недавно освобождённый регион по следующему запросу, поэтому освобождение одной скомпилированной функции и запрос другой размещают новый код поверх старого — именно то перекрытие, которое нужно BTR. Стабильное повторное использование адресов достигается, и движок оказывается явно уязвим сам по себе.
Ограничение: собственная активность компиляции и сборки мусора в GraalVM стирает записи BTB до того, как их можно использовать; сборка мусора создаёт много шума. Исследователи не считают это фундаментальным — лучшее планирование или более умная работа с памятью могут закрыть разрыв.
Поведение BTB на разных архитектурах
Поведение, на котором строится BTR, подтверждено на всех протестированных процессорах Intel, AMD и Arm. Практический end-to-end сценарий с кражей данных из ядра продемонстрировали на Intel, где записи BTB переживают полный цикл освобождения и повторного выделения памяти, давая оценочную скорость утечки порядка десятков байт в секунду.
На Intel устойчивость записей BTB даёт атакующему возможность спекулятивного execute-after-free: устаревшая запись продолжает указывать на старый адрес кода, и при повторном срабатывании косвенного перехода процессор спекулятивно прыгает по устаревшей цели. В сценарии на Linux cBPF это позволяет перехватить спекулятивное управление на новый код по устаревшим смещениям и раскрыть данные со скоростью 8 байт в секунду — при аккуратном pointer chasingпоследовательном разыменовании указателей для перехода от одной структуры данных к другой этого достаточно, чтобы добраться до секрета.
Пограничные случаи и ограничения
IBT на x86 и BTI на Arm требуют, чтобы цели косвенных переходов начинались с инструкции endbr64 или BTI, но не устраняют угрозу полностью. На старых процессорах Intel одна или несколько инструкций могут выполняться спекулятивно до завершения проверки IBT; это поведение исчезло только с архитектурой Lion Cove — первой свободной от этой гонки генерацией Intel, которую нашли исследователи. Даже на реализациях без гонки атакующий может внедрить байты, кодирующие endbr64, в JIT-скомпилированный код и достичь их через невыровненное исполнение, превратив в допустимую площадку для приземления; это удалось только при отключённой маскировке констант.
Для обхода маскировки констант cBPF байты кодируются в байтах смещения прямых переходов, при этом максимальное смещение равно 0x014fc5, поэтому контролируются только младшие два байта, а старшие остаются нулевыми.
Утечка через BTB на Intel сохраняется после полного цикла освобождения и повторного выделения памяти. Для сквозного браузерного эксплойта нужно расширить окно временного исполнения и построить скрытый канал, устойчивый к сниженной точности таймера.
В GraalVM стабильное повторное использование адресов достигается потому, что ядро при следующем запросе склонно возвращать только что освобождённую область, но собственная компиляция и сборка мусора движка стирают записи BTB до их использования, причём сборка мусора создаёт много шума.
Защита и реакция вендоров
Рекомендация общая: обновлять ОС и ПО сразу после появления патчей вендоров. Патчи выпустили ядро Linux и Oracle.
В ядре Linux для x86 добавили смягчение: оно выдаёт IBPBбарьер, предписывающий процессору сбросить накопленные записи предсказателя косвенных переходов на всех ядрах, когда cBPF-программа повторно использует ранее исполненный регион cBPF/eBPF, и такое повторное использование не рекомендуется как оптимизация. Смягчение применяется независимо от того, включён ли IBT.
Oracle в GraalVM вместо этого затрудняет повторное использование регионов путём рандомизации расположения JIT-кэша.
Mozilla рассматривала смягчения на основе IBPB, но в настоящее время отдаёт приоритет завершению и развёртыванию изоляции сайтов.
Аппаратные средства контроля потока управления (IBT на x86 и BTI на Arm) лишь повышают планку, но не устраняют риск. Причина частичности: на старых процессорах Intel до проверки IBT могут спекулятивно выполниться инструкции, а даже в свободных от гонки реализациях атакующий может внедрить байты, кодирующие endbr64, в JIT-код и достичь их при невыровненном исполнении.
Как проходило раскрытие
Исследователи раскрыли находки затронутым производителям оборудования и ПО, и те подтвердили находки. Производители оборудования заявили, что механизмы смягчения (например, IBPB) уже существуют, а смягчения BTR должны развёртываться в ПО. Разработчики ядра Linux и Oracle развернули смягчения.
Что из этого следует на практике
- JIT-движок — это поверхность атаки, а не только оптимизация. BTR работает именно потому, что JIT постоянно выделяет и освобождает исполняемую память, а аллокаторы склонны возвращать недавно освобождённые блоки первыми. Чем предсказуемее повторное использование адресов, тем надёжнее атака.
- Архитектурная когерентность кода не равна когерентности предсказателя. Процессор корректно исполняет новый код после самомодификации, но устаревшие цели переходов могут пережить его. Любая защита, полагающаяся только на корректность архитектурного состояния, оставляет этот зазор.
- Constant blinding обходится через смещения переходов. Маскировка непосредственных значений не мешает закодировать управляемые байты в байтах смещения прямых переходов — при условии, что атакующий укладывается в младшие два байта (максимум 0x014fc5).
- IBT и BTI повышают планку, но не закрывают вопрос. На части процессоров проверка не успевает за спекулятивным исполнением, а на свободных от гонки реализациях байты
endbr64можно внедрить в JIT-код и достичь их через невыровненное исполнение. - Смягчения вендоров адресны. Linux выдаёт IBPB при повторном использовании региона cBPF/eBPF, GraalVM рандомизирует размещение JIT-кэша, Mozilla делает ставку на изоляцию сайтов. Каждое из них сужает конкретный сценарий, а не устраняет механизм целиком.
- Скорость утечки не приговор. 8 байт в секунду на cBPF-эксплойте и десятки байт в секунду на Intel в SpiderMonkey достаточны, потому что при аккуратном отслеживании указателей нужно утечь немного данных, чтобы добраться до секрета.