Анализ уязвимости CVE‑2024‑7965
21 августа браузер Chrome получил обновление, которое исправило 37 ошибок, связанных с безопасностью. Внимание исследователей по всему миру привлекла уязвимость CVE‑2024‑7965, описанная как некорректная имплементация в V8. На практике это означает возможность RCE (remote code execution) в рендерере браузера, что открывает простор для последующей эксплуатации. Заинтересованность исследователей повысилась еще сильнее, когда 26 августа Google сообщила об использовании уязвимости «в дикой природе».
Мы проанализировали CVE‑2024‑7965 и представляем в этой статье результаты исследования.
В отличие от предыдущего нашего исследования, где было необходимо заниматься сравнением исполняемых файлов, сейчас делать ничего подобного не нужно: весь исходный код V8 публичен. Однако провести некоторый анализ все равно надо, чтобы найти необходимый коммит. Спустя некоторое время поисков видим следующее:
Здесь мы сразу обращаем внимание на важную деталь: патч внесен в компонент V8 TurboFan — оптимизирующий компилятор кода JS. TurboFan работает по принципу sea of nodes: сначала он строит граф, проводит на нем оптимизации, а затем выбирает инструкции под конкретную архитектуру и генерирует машинный код.
Исправлена функция ZeroExtendsWord32ToWord64
, с помощью которой компилятор проверяет, всегда ли значение, приходящее по разным путям (так называемая цепочка phi‑узлов, phi nodes), имеет верхние 32 бита равными 0. Если компилятор не может доказать, что верхние 32 бита числа равны 0, то добавляет дополнительную проверку — сравнивает число с максимальным значением unsigned int (0xffffffff)
.
Потенциально уязвимая функция работает следующим образом: она рекурсивно обходит граф из phi‑узлов в глубину, помечая пройденные ноды как kUpperBitsGuaranteedZero
. Отметка узлов происходит через запись в кастомный вектор phi_states_
. Если среди потомков находится хоть один узел, верхние биты которого не равны нулю гарантированно, то происходит рекурсивный возврат с пометкой всех узлов на пути к данному как kNoGuarantee
. Если узел уже был помечен, то компилятор его не проходит.
Если мы присмотримся к патчу, то заметим, что при первом вхождении в узел он обнуляет все ранее сохраненные значения для предыдущих узлов. Это наталкивает нас на мысль, что обход графа делает некорректные пометки. Но каким образом это происходит? Ведь если мы попробуем обойти обычный граф, то заметим, что все работает корректно:
В процессорной архитектуре x86‑64 компилятор считает, что положительная константа имеет верхние 32 бита равными 0, а отрицательная константа — нет. При обходе такого графа phi_states_
будет находиться в полностью корректном состоянии.
phi_states_
зеленым помечаются все узлы, по которым уже прошел обход
На картинках с обходом графа зеленым цветом отмечены те вершины, которые определены как имеющие верхние 32 бита равными нулю, красным цветом — как те, верхние 32 бита которых гарантированно нулю не равны. Как мы помним, если хоть один потомок вершины не имеет верхние 32 бита равными нулю, то сама вершина помечается красным.
Однако если граф будет циклическим, то можно получить некорректное состояние phi_states_
: например, при условии, что обход пойдет по циклу раньше, чем по другим соседним узлам, в phi_states_
будет храниться некорректное состояние.
Можно получить граф из phi‑нод, в котором определенный узел помечен в phi_states_
как зеленый, однако верхние 32 бита его потомка могут быть не равны нулю. Таким образом, существует путь, при котором компилятор «поверит» в наше значение, а мы его обманем. Именно это исправляет патч. При каждом повторном вызове функции ZeroExtendsWord32ToWord64
он обнуляет состояние phi_states_
.
Что критичного может произойти в такой ситуации? Если мы посмотрим, как компилируется операция обращения к массиву по индексу, то заметим, что в 64‑битных архитектурах компилятор сначала пытается убедиться, что индекс, по которому мы обращаемся, имеет верхние 32 бита равными нулю, затем кладет его в 64‑битный регистр и производит операцию над памятью. Если компилятор «поверил», что верхние 32 бита равны нулю, то он убирает проверку на то, что число меньше, чем 0xffffffff
. Соответственно, мы получаем ситуацию, при которой если в 64‑битном регистре верхние биты окажутся не равными нулю в момент обращения по индексу, то произойдет обращение out‑of‑bounds.
К сожалению, на архитектуре x86‑64 получить memory corruption PoC не удалось в силу того, что при попытке привести 64‑битное число к 32‑битному компиляция происходит так, чтобы гарантированно обнулить верхние 32 бита, поэтому мы не можем получить такой регистр, в котором будут находиться неопределенные значения. Комментарий в начале функции это достаточно хорошо описывает:
Однако в архитектуре ARM64 аналогичная функция оставляет неопределенные значения в верхних битах, что позволяет нам получить их не равными нулю в момент обращения к массиву по индексу:
Так же как серьезный шахматист, не досчитывая вариант до конца, может понять, что надо играть именно так, на этом этапе мы были уверены, что это и есть тот самый правильный способ эксплуатации и приступили к разработке PoC.
Итак, наш план эксплуатации уязвимости:
- Получаем необходимый циклический граф с помощью циклов и условий.
- Инициируем некорректное состояние в
phi_states_
. Наш PoC использует для этогоBigInt
. - Добавляем в граф вершину
TruncateInt64ToWord32
с помощью комбинацииMath.min
с оператором>>> 0
. - Инициируем последующий вызов таким образом, чтобы в качестве индекса было число, в которое мы поместили верхние биты.
Эти шаги позволяют получить memory corruption и словить segmentation fault в V8.
Что же дает эксплуатация этой уязвимости? Так как она позволяет атаковать только устройства с процессорной архитектурой ARM64, то распространяется по большей части на смартфоны Android и ноутбуки Apple, выпущенные после ноября 2020 года. В случае наличия у хакеров эксплоита, позволяющего совершить побег из песочницы браузера, можно получить полный контроль над приложением браузера: читать пароли, красть сессии пользователей.
В то же время отдельная эксплуатация рассмотренной уязвимости также опасна: при наличии XSS на любом из поддоменов сайта можно получить пароли и cookie с основного домена и всех поддоменов. Получить конфиденциальные данные с других сайтов в этом случае нельзя, ведь от этого защищает технология Site Isolation, разделяющая процессы рендереров. Однако даже в таком виде уязвимость критична, и если на ваших устройствах версия Chrome все еще меньше чем 128.0.6613.84, то необходимо обновить браузер в кратчайшие сроки.
Это и подобные исследования мы проводим как для повышения осведомленности технических специалистов об уязвимостях в различном ПО, так и для улучшения предоставления качества наших услуг пентеста и red team.