
Chrome Renderer 1day RCE durch Type Confusion im Async Stack Trace (v8ctf-Einreichung)
Diese Schwachstelle erlaubte es einem entfernten Angreifer, beliebigen Code im Chrome-Renderer-Prozess auszuführen.
In der Verarbeitung der asynchronen Stack-Traces fehlte eine ausreichende Typprüfung.
Dies führt zu einer Typverwechslung zwischen FunctionContext und NativeContext, die einen unerlaubten Zugriff auf den JSGlobalProxy->hash-Wert verursacht.
Durch Heap-Spraying konnte der Angreifer einen gefälschten asynchronen Stack-Frame einschleusen und die fakeobj-Primitive konstruieren.
Mit der fakeobj-Primitive konnte der Angreifer beliebigen Code im Chrome-Renderer-Prozess ausführen.
Du kannst dir unsere TyphoonCon-2024-Folien ansehen.
Asynchrone Programmierung ist eines der wichtigsten Features in JavaScript. Früher war es schwierig, asynchronen Code mit dem Fehler-Stack zu debuggen, da asynchrone Funktionen nicht im Fehler-Stack erfasst wurden. Suspendierte asynchrone Funktionen werden in der Callback-Queue der Ereignisschleife gespeichert, nicht im Aufrufstapel, daher enthält der Fehler-Stack die asynchrone Funktion nicht. Um dieses Problem zu lösen, bietet V8 die Funktion „async stack trace“ (standardmäßig seit V8 v7.3), um asynchrone Funktionen im Fehler-Stack zu erfassen. ([v8-Blog], [v8-Dokumentation])
„Promise.all Resolve Element Closure“ ist eine Hilfsfunktion zum Auflösen der eingehenden Promises in der Promise.all-Funktion.
Die Promise.all-Funktion nimmt ein Array von Promises entgegen und gibt ein Promise zurück, das aufgelöst wird, wenn alle eingegebenen Promises aufgelöst sind.
„Promise.all Resolve Element Closure“ ist ein Resolve-Handler für jedes eingehende Promise in der Promise.all-Funktion.
Die Funktion hat die Aufgabe, das eingehende Promise aufzulösen und den Erfüllungswert im Ergebnis-Array zu speichern.
Es gibt 2 Punkte, die man bei dieser Funktion beachten sollte:
FunctionContext, bis sie aufgerufen wurde, und nach dem Aufruf hat sie NativeContext. (v8-Code)Bug-Klasse: Typverwechslung zwischen FunctionContext und NativeContext
Details zur Schwachstelle:
Die Schwachstelle kann ausgelöst werden, indem ein asynchroner Stack-Trace mit der bereits ausgeführten Funktion „Promise.all Resolve Element Closure“ oder ähnlichen intrinsischen Built-in-Funktionen erfasst wird. In diesem Exploit habe ich die Funktion „Promise.all Resolve Element Closure“ als Beispiel verwendet.
Wenn im JavaScript-Code ein Fehler ausgelöst wird, erfasst V8 den Fehler-Stack aus dem Aufrufstapel und hängt die asynchronen Stack-Frames aus der aktuellen Mikrotask an [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);
}
Die Funktion CaptureAsyncStackTrace [[2]] durchsucht die Promise-Kette und hängt den asynchronen Stack-Frame gemäß dem asynchronen Aufruftyp an (z. B. await, Promise.all, Promise.any).
Unten ist der Ausschnitt der Funktion CaptureAsyncStackTrace, der den Fall Promise.all behandelt:
} 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 (
Wenn beim Durchsuchen der Promise-Kette reaction->fulfill_handler die Built-in-Funktion „Promise.all Resolve Element Closure“ ist, wird der asynchrone Promise-Combinator-Frame an den Fehler-Stack angehängt.
Danach wechselt es zum nächsten Promise, indem es auf function->context->capability->promise zugreift.
Das Problem ist, dass die Funktion davon ausgeht, dass die „Promise.all Resolve Element Closure“-Funktion noch nicht ausgeführt wurde.
Wenn die „Promise.all Resolve Element Closure“-Funktion bereits ausgeführt wurde, ändert sich der Kontext von FunctionContext zu NativeContext.
Das führt zu einer Typverwechslung zwischen FunctionContext und NativeContext in der Funktion CaptureAsyncStackTrace.
Erstellung des PoC:
Die Strategie zum Auslösen der Schwachstelle ist wie folgt:
FunctionContext zu NativeContext zu ändern.Ich habe das synchrone Promise-Auflösungsmuster für Promise.all verwendet, um die Funktion „Promise.all Resolve Element Closure“ auf JS-Skriptebene zu erhalten.
Das Muster habe ich aus den test262-Testfällen übernommen.
Nachdem ich die Funktion explizit aufgerufen hatte, habe ich zum Auslösen der Schwachstelle den Beispielcode aus der [Dokumentation zu Zero-Cost-Async-Stack-Traces][v8-Dokumentation] verwendet, um eine neue Promise-Kette vorzubereiten, und die intrinsische Built-in-Funktion als Fulfill-Handler eines der Promises festgelegt.