
Chrome Renderer 1day RCE tramite Type Confusion nello Stack Trace Asincrono (v8ctf submission)
Questa vulnerabilità permetteva a un attaccante remoto di eseguire codice arbitrario all'interno del processo renderer di Chrome.
C'era un controllo di tipo insufficiente nel codice di gestione dello stack trace asincrono.
Porta a una confusione di tipo tra FunctionContext e NativeContext, causando un accesso illegale al valore JSGlobalProxy->hash.
Con l'heap spraying, l'attaccante è stato in grado di iniettare un finto frame dello stack asincrono e costruire la primitiva fakeobj.
Usando la primitiva fakeobj, l'attaccante è riuscito a ottenere l'esecuzione di codice arbitrario nel processo renderer di Chrome.
Puoi consultare le nostre slide di TyphoonCon 2024.
L'asincronia è una delle caratteristiche più importanti in JavaScript. In passato, era difficile eseguire il debug del codice asincrono con lo stack di errori perché le funzioni asincrone non venivano catturate nello stack di errori. Le funzioni asincrone sospese sono memorizzate nella coda di callback del ciclo di eventi, non nello stack di chiamate, quindi lo stack di errori non contiene la funzione asincrona. Per risolvere questo problema, V8 fornisce la funzionalità "async stack trace" (per impostazione predefinita da V8 v7.3) per catturare la funzione asincrona nello stack di errori. ([v8 blog], [v8 docs])
"Promise.all Resolve Element Closure" è una funzione helper per risolvere le promise di input nella funzione Promise.all.
La funzione Promise.all prende un array di promise e restituisce una promise che si risolve quando tutte le promise di input sono risolte.
"Promise.all Resolve Element Closure" è un gestore di risoluzione per ogni promise di input nella funzione Promise.all.
Il ruolo della funzione è risolvere la promise di input e memorizzare il valore di completamento nell'array dei risultati.
Ci sono 2 punti da notare riguardo alla funzione:
FunctionContext fino a quando non viene chiamata, e poi ha NativeContext dopo essere stata chiamata. (v8 code)Classe di bug: Confusione di tipo tra FunctionContext e NativeContext
Dettagli della vulnerabilità:
La vulnerabilità può essere attivata catturando uno stack trace asincrono con la funzione "Promise.all Resolve Element Closure" già eseguita o funzioni builtin intrinseche simili. In questo exploit, ho usato la funzione "Promise.all Resolve Element Closure" come esempio.
Quando un errore viene lanciato nel codice JavaScript, V8 cattura lo stack di errori dallo stack e aggiunge i frame dello stack asincrono dal microtask corrente [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);
}
La funzione CaptureAsyncStackTrace [2] cerca nella catena di promise e aggiunge il frame dello stack asincrono in base al tipo di chiamata asincrona (es., await, Promise.all, Promise.any).
Di seguito è riportato lo snippet della funzione CaptureAsyncStackTrace che gestisce il 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 (
Durante la ricerca nella catena di promise, se reaction->fulfill_handler è la funzione builtin "Promise.all Resolve Element Closure", aggiunge il frame combinatorio della promise asincrona allo stack di errori.
Poi, passa alla promise successiva accedendo a function->context->capability->promise.
Il problema è che la funzione presuppone che la funzione "Promise.all Resolve Element Closure" non sia stata ancora eseguita.
Se la funzione "Promise.all Resolve Element Closure" è già stata eseguita, il contesto viene cambiato da FunctionContext a NativeContext.
Porta a una confusione di tipo tra FunctionContext e NativeContext nella funzione CaptureAsyncStackTrace.
Realizzazione del PoC:
La strategia per attivare la vulnerabilità è la seguente:
FunctionContext a NativeContext.Ho usato il pattern di risoluzione sincrona delle promise per Promise.all per ottenere la funzione "Promise.all Resolve Element Closure" a livello di script JS.
Ho preso in prestito il pattern dai casi di test test262.
Dopo aver chiamato esplicitamente la funzione, per attivare la vulnerabilità, ho usato il codice di esempio nel [documento sullo stack trace asincrono a costo zero][v8 docs] per preparare una nuova catena di promise e impostare la funzione builtin intrinseca come gestore di completamento di una delle promise.