
Уязвимость 1-day в Chrome Renderer: RCE через путаницу типов в асинхронном стеке вызовов (участие v8ctf)
Эта уязвимость позволяла удаленному злоумышленнику выполнить произвольный код в процессе рендеринга Chrome.
В коде обработки асинхронных стековых трасс была недостаточная проверка типа. Это приводило к путанице типов (type confusion) между FunctionContext и NativeContext, что вызывало незаконный доступ к значению JSGlobalProxy->hash. С помощью heap spraying злоумышленник мог внедрить поддельный асинхронный кадр стека и построить примитив fakeobj. Используя примитив fakeobj, злоумышленник мог добиться произвольного выполнения кода в процессе рендеринга Chrome.
Вы можете ознакомиться с нашими слайдами с TyphoonCon 2024.
Асинхронность — одна из важнейших функций JavaScript. В прошлом было трудно отлаживать асинхронный код с помощью стеков ошибок, потому что асинхронные функции не захватывались в стеке ошибок. Приостановленные асинхронные функции хранятся в очереди обратных вызовов цикла событий, а не в стеке вызовов, поэтому стек ошибок не содержит асинхронной функции. Чтобы решить эту проблему, V8 предоставляет функцию "async stack trace" (по умолчанию начиная с V8 v7.3) для захвата асинхронной функции в стеке ошибок. ([v8 blog], [v8 docs])
"Promise.all Resolve Element Closure" — это вспомогательная функция для разрешения входных промисов в функции Promise.all. Функция Promise.all принимает массив промисов и возвращает промис, который разрешается, когда все входные промисы будут разрешены. "Promise.all Resolve Element Closure" является обработчиком разрешения каждого входного промиса в функции Promise.all. Роль этой функции — разрешить входной промис и сохранить значение выполнения в результирующем массиве.
Есть два важных момента об этой функции:
FunctionContext, а после вызова — NativeContext. (код V8)Класс ошибки: Путаница типов (type confusion) между FunctionContext и NativeContext
Детали уязвимости:
Уязвимость можно вызвать, захватив асинхронный стек вызовов с уже выполненной функцией "Promise.all Resolve Element Closure" или аналогичной встроенной функцией. В этом эксплойте в качестве примера используется функция "Promise.all Resolve Element Closure".
Когда в JavaScript-коде возникает ошибка, V8 захватывает стек ошибки из стека и добавляет асинхронные кадры стека из текущей микрозадачи. [1]
CallSiteBuilder builder(isolate, mode, limit, caller);
VisitStack(isolate, &builder);
// Если --async-stack-traces включены и "текущая микрозадача" является
// PromiseReactionJobTask, мы пытаемся обогатить стек ошибок асинхронными кадрами.
if (v8_flags.async_stack_traces) {
CaptureAsyncStackTrace(isolate, &builder);
}
Функция CaptureAsyncStackTrace [[2]] просматривает цепочку промисов и добавляет асинхронный кадр стека в соответствии с типом асинхронного вызова (например, await, Promise.all, Promise.any).
Ниже приведён фрагмент функции CaptureAsyncStackTrace, который обрабатывает случай Promise.all:
} else if (IsBuiltinFunction(isolate, reaction->fulfill_handler(),
Builtin::kPromiseAllResolveElementClosure)) {
Handle<JSFunction> function(JSFunction::cast(reaction->fulfill_handler()),
isolate);
Handle<Context> context(function->context(), isolate);
Handle<JSFunction> combinator(context->native_context()->promise_all(),
isolate);
builder->AppendPromiseCombinatorFrame(function, combinator);
// Теперь заглядываем в контекст resolve element функции Promise.all(),
// чтобы найти promise capability, который разрешается, когда все
// одновременные промисы будут разрешены.
int const index =
PromiseBuiltins::kPromiseAllResolveElementCapabilitySlot;
Handle<PromiseCapability> capability(
PromiseCapability::cast(context->get(index)), isolate);
if (!IsJSPromise(capability->promise())) return;
promise = handle(JSPromise::cast(capability->promise()), isolate);
} else if (
При просмотре цепочки промисов, если reaction->fulfill_handler является встроенной функцией "Promise.all Resolve Element Closure", она добавляет кадр асинхронного комбинатора промисов в стек ошибок. Затем она переходит к следующему промису, обращаясь к function->context->capability->promise.
Проблема в том, что функция предполагает, что "Promise.all Resolve Element Closure" ещё не была выполнена. Если эта функция уже выполнена, контекст меняется с FunctionContext на NativeContext. Это приводит к путанице типов между FunctionContext и NativeContext в функции CaptureAsyncStackTrace.
Создание PoC:
Стратегия для вызова уязвимости следующая:
FunctionContext на NativeContext.Для получения функции "Promise.all Resolve Element Closure" на уровне JS-скрипта я использовал синхронный паттерн разрешения промисов для Promise.all. Я позаимствовал этот паттерн из тестовых случаев test262.
После явного вызова функции, чтобы вызвать уязвимость, я использовал пример кода из [документации по zero-cost async stack trace][v8 docs] для подготовки новой цепочки промисов и установил встроенную функцию как обработчик выполнения одного из промисов.
Наконец, когда возникает ошибка, асинхронный стек захватывается с уже выполненной функцией "Promise.all Resolve Element Closure" в качестве обработчика выполнения, что приводит к путанице типов между FunctionContext и NativeContext.
Вот код PoC: poc.js
(Термины «примитив эксплойта», «стратегия эксплойта», «техника эксплойта» и «поток эксплойта» определены здесь.)
Примитив эксплойта: примитив fakeobj
Стратегия эксплойта: Для построения примитива fakeobj из ошибки путаницы типов я использовал следующую стратегию: