
Chrome Renderer 1day RCE via Type Confusion em Async Stack Trace (submissão v8ctf)
Esta vulnerabilidade permitiu que um atacante remoto executasse código arbitrário dentro do processo renderizador do Chrome.
Havia uma verificação de tipo insuficiente no código de manipulação de rastreamento de pilha assíncrono.
Isso leva a uma confusão de tipo entre FunctionContext e NativeContext, causando acesso ilegal ao valor JSGlobalProxy->hash.
Com heap spraying, o atacante conseguiu injetar um frame de pilha assíncrono falso e construir a primitiva fakeobj.
Usando a primitiva fakeobj, o atacante conseguiu alcançar execução de código arbitrário no processo renderizador do Chrome.
Você pode verificar nossos slides do TyphoonCon 2024.
Assíncrono é uma das características mais importantes em JavaScript. No passado, era difícil depurar código assíncrono com pilha de erros porque funções assíncronas não eram capturadas na pilha de erros. Funções assíncronas suspensas são armazenadas na fila de callbacks do loop de eventos, não na pilha de chamadas, portanto a pilha de erros não contém a função assíncrona. Para resolver esse problema, o V8 fornece o recurso "async stack trace" (por padrão desde V8 v7.3) para capturar funções assíncronas na pilha de erros. ([v8 blog], [v8 docs])
"Promise.all Resolve Element Closure" é uma função auxiliar para resolver as promessas de entrada na função Promise.all.
A função Promise.all recebe um array de promessas e retorna uma promessa que resolve quando todas as promessas de entrada são resolvidas.
O papel da função é resolver a promessa de entrada e armazenar o valor de cumprimento no array de resultados.
Há 2 pontos a serem observados sobre a função:
FunctionContext até ser chamada, e então tem NativeContext depois de ser chamada. (v8 code)Classe do bug: Confusão de tipo entre FunctionContext e NativeContext
Detalhes da vulnerabilidade:
A vulnerabilidade pode ser desencadeada ao capturar um rastreamento de pilha assíncrono com a função "Promise.all Resolve Element Closure" já executada ou funções intrínsecas semelhantes. Neste exploit, usei a função "Promise.all Resolve Element Closure" como exemplo.
Quando um erro é lançado no código JavaScript, o V8 captura a pilha de erros da pilha e anexa os frames de pilha assíncronos da microtarefa atual [1].
CallSiteBuilder builder(isolate, mode, limit, caller);
VisitStack(isolate, &builder);
// If --async-stack-traces are enabled and the "current microtask" is a
// PromiseReactionJobTask, we try to enrich the stack trace with async
// frames.
if (v8_flags.async_stack_traces) {
CaptureAsyncStackTrace(isolate, &builder);
}
A função CaptureAsyncStackTrace [2] percorre a cadeia de promessas e anexa o frame de pilha assíncrono de acordo com o tipo de chamada assíncrona (ex.: await, Promise.all, Promise.any).
Abaixo está o trecho da função CaptureAsyncStackTrace que lida com o caso 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);
// Now peak into the Promise.all() resolve element context to
// find the promise capability that's being resolved when all
// the concurrent promises resolve.
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 (
Ao percorrer a cadeia de promessas, se reaction->fulfill_handler for a função interna "Promise.all Resolve Element Closure", ele anexa o frame de combinador de promessa assíncrona à pilha de erros.
Em seguida, move-se para a próxima promessa acessando function->context->capability->promise.
O problema é que a função assume que a função "Promise.all Resolve Element Closure" ainda não foi executada.
Se a função "Promise.all Resolve Element Closure" já foi executada, o contexto é alterado de FunctionContext para NativeContext.
Isso leva a uma confusão de tipo entre FunctionContext e NativeContext na função CaptureAsyncStackTrace.
Criando o PoC:
A estratégia para acionar a vulnerabilidade é a seguinte:
FunctionContext para NativeContext.Usei o padrão de resolução síncrona de promessas para Promise.all para obter a função "Promise.all Resolve Element Closure" no nível do script JS.
Peguei o padrão dos casos de teste test262.
Após chamar explicitamente a função, para acionar a vulnerabilidade, usei o código de exemplo no [documento de rastreamento de pilha assíncrono de custo zero][v8 docs] para preparar uma nova cadeia de promessas e definir a função intrínseca interna como um manipulador de cumprimento de uma das promessas.
Finalmente, quando o erro é lançado, o rastreamento de pilha assíncrono é capturado com a função "Promise.all Resolve Element Closure" já executada como manipulador de cumprimento, levando a uma confusão de tipo entre FunctionContext e NativeContext.
Aqui está o código PoC: poc.js