Когда «+10» приводит к -2³¹ и полному контролю: уязвимость в математике Firefox

Всё было по правилам, но правила были неправильными.


n9wj7s7oqozfk9ybc6putjrhd4hc3jy3.jpg


В ходе соревнования Pwn2Own Berlin 2025 исследователь Манфред Пауль продемонстрировал успешную атаку на процесс рендеринга браузера Mozilla Firefox, воспользовавшись уязвимостью в JIT-компиляторе IonMonkey. Несмотря на то, что ему не удалось выйти за пределы песочницы ( out-of-bounds ) JavaScript-движка, сама находка оказалась крайне значимой — ошибка получила идентификатор CVE-2025-4919. Уже на следующий день Mozilla устранила проблему в версии Firefox 138.0.4, о чём сообщила в бюллетене безопасности 2025-36. Организация Zero Day Initiative присвоила эксплойту номер ZDI-25-291.

IonMonkey — это JIT-компилятор, используемый в SpiderMonkey, движке JavaScript и WebAssembly, лежащем в основе Firefox. В процессе оптимизации кода IonMonkey применяет функцию ExtractLinearSum для анализа линейных выражений, позволяя упростить операции вида «x + n» до более понятной формы. Такой анализ используется, в частности, для оптимизаций, связанных с проверками границ массивов.

Функция ExtractLinearSum возвращает структуру SimpleLinearSum, представляющую линейную сумму из переменной и константы. Она может работать в разных математических пространствах: с модульной арифметикой (Modulo), в бесконечном пространстве без переполнений (Infinite) и в неизвестном (Unknown), который определяется динамически в зависимости от контекста.

Ошибка кроется в функции TryEliminateBoundsCheck — именно она занимается удалением избыточных проверок границ массивов. Если компилятор обнаруживает две последовательные проверки индексов с выражениями, которые можно привести к одной переменной с разными константами (например, i+5 и i+10), он объединяет их в одну. Однако если операции выполняются в модульной арифметике, это приводит к некорректным вычислениям.

Например, при использовании битовой операции «|0» в выражениях происходит принудительное приведение к 32-битному типу с обрезкой, что превращает сложение в модульное. Это сбивает компилятор с толку: он считает, что индексы в выражениях вроде (i+5)|0 и (i+10)|0 всегда отличаются на фиксированную величину, игнорируя возможность переполнения.

Как показал Пауль, это приводит к тому, что компилятор ошибочно объединяет проверки и разрешает доступ по отрицательным индексам — то есть за пределами массива. В случае с массивами типа Uint8Array длиной 2³² байт это открывает возможность как чтения, так и записи вне границ выделенной памяти, предоставляя примитивы для дальнейшей эксплуатации.

Для успешной атаки достаточно подготовить массив и переменную, используемую в качестве индекса, значение которой близко к границе 32-битного пространства. Благодаря особенностям компиляции, при определённых условиях в ходе оптимизации происходит удаление критически важной проверки, позволяя злоумышленнику обратиться по отрицательному смещению, что открывает доступ к чужой памяти.

Особенности реализации в Firefox делают возможным получение так называемых «addrOf» и «fakeObj» примитивов — инструментов, с помощью которых можно определить адрес объекта в памяти и создать поддельный объект. Это, в свою очередь, позволяет перезаписывать указатели и управлять структурой памяти вручную.

Финальный этап атаки включает размещение шеллкода в исполняемой области памяти — например, внутри WASM-функции — и подмену адреса её входной точки. Тем самым достигается выполнение произвольного кода. Демонстрация эксплуатации доступна на YouTube.

Стоит отметить, что подобные уязвимости остаются крайне редкими и трудноуловимыми для автоматических инструментов вроде фуззеров. Проблема описанная в CVE-2025-4919 связана с тонкостями логики компилятора и требует масштабных аллокаций памяти, что снижает вероятность её обнаружения в ходе обычного тестирования. Это в очередной раз подчёркивает важность анализа исходного кода при поиске критических багов.