
Chrome Renderer RCE 1-day via confusion de type dans la trace de pile asynchrone (soumission v8ctf)
Cette vulnérabilité permettait à un attaquant distant d'exécuter du code arbitraire dans le processus de rendu Chrome.
Il y avait une vérification de type insuffisante dans le code de gestion des traces de pile asynchrones.
Cela conduit à une confusion de type entre FunctionContext et NativeContext, provoquant un accès illégal à la valeur JSGlobalProxy->hash.
Avec du heap spraying, l'attaquant a pu injecter une fausse trame de pile asynchrone et construire le primitif fakeobj.
En utilisant le primitif fakeobj, l'attaquant a pu réaliser une exécution de code arbitraire dans le processus de rendu Chrome.
Vous pouvez consulter nos slides TyphoonCon 2024.
L'asynchrone est l'une des fonctionnalités les plus importantes de JavaScript. Dans le passé, il était difficile de déboguer du code asynchrone avec la pile d'erreur car les fonctions asynchrones ne sont pas capturées dans la pile d'erreur. Les fonctions asynchrones suspendues sont stockées dans la file d'attente de rappels de la boucle d'événements, pas dans la pile d'appels, donc la pile d'erreur ne contient pas la fonction asynchrone. Pour résoudre ce problème, V8 fournit la fonctionnalité "trace de pile asynchrone" (par défaut depuis V8 v7.3) pour capturer les fonctions asynchrones dans la pile d'erreur. ([v8 blog], [v8 docs])
"Fermeture de résolution d'élément Promise.all" est une fonction auxiliaire pour résoudre les promesses d'entrée dans la fonction Promise.all.
La fonction Promise.all prend un tableau de promesses et retourne une promesse qui se résout lorsque toutes les promesses d'entrée sont résolues.
"Fermeture de résolution d'élément Promise.all" est un gestionnaire de résolution de chaque promesse d'entrée dans la fonction Promise.all.
Son rôle est de résoudre la promesse d'entrée et de stocker la valeur d'accomplissement dans le tableau de résultats.
Il y a 2 points à noter à propos de cette fonction :
FunctionContext jusqu'à ce qu'elle soit appelée, puis elle a NativeContext après avoir été appelée. (code v8)Classe de bogue : Confusion de type entre FunctionContext et NativeContext
Détails de la vulnérabilité :
La vulnérabilité peut être déclenchée en capturant une trace de pile asynchrone avec la fonction "Fermeture de résolution d'élément Promise.all" déjà exécutée ou une fonction native intrinsèque similaire. Dans cet exploit, j'ai utilisé la fonction "Fermeture de résolution d'élément Promise.all" comme exemple.
Lorsqu'une erreur est levée dans le code JavaScript, V8 capture la pile d'erreur depuis la pile et ajoute les trames de pile asynchrones depuis la microtâche courante [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 fonction CaptureAsyncStackTrace [[2]] parcourt la chaîne de promesses et ajoute la trame de pile asynchrone en fonction du type d'appel asynchrone (par exemple, await, Promise.all, Promise.any).
Voici l'extrait de la fonction CaptureAsyncStackTrace qui gère le cas 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 (
En parcourant la chaîne de promesses, si reaction->fulfill_handler est la fonction native "Fermeture de résolution d'élément Promise.all", elle ajoute la trame de combinaison de promesses asynchrones à la pile d'erreur.
Ensuite, elle passe à la promesse suivante en accédant à function->context->capability->promise.
Le problème est que la fonction suppose que la fonction "Fermeture de résolution d'élément Promise.all" n'a pas encore été exécutée.
Si la fonction "Fermeture de résolution d'élément Promise.all" a déjà été exécutée, le contexte passe de FunctionContext à NativeContext.
Cela conduit à une confusion de type entre FunctionContext et NativeContext dans la fonction CaptureAsyncStackTrace.
Réalisation du PoC :
La stratégie pour déclencher la vulnérabilité est la suivante :
FunctionContext à NativeContext.J'ai utilisé le motif de résolution synchrone de promesses pour Promise.all afin d'obtenir la fonction "Fermeture de résolution d'élément Promise.all" au niveau du script JS.
J'ai emprunté le motif des cas de test test262.
Après avoir appelé explicitement la fonction, pour déclencher la vulnérabilité, j'ai utilisé l'exemple de code dans le [document sur la trace de pile asynchrone à coût zéro][v8 docs] pour préparer une nouvelle chaîne de promesses et définir la fonction native intrinsèque comme gestionnaire d'accomplissement de l'une des promesses.