
Chrome Renderer 1day RCE mediante Type Confusion en Async Stack Trace (v8ctf submission)
Esta vulnerabilidad permitía a un atacante remoto ejecutar código arbitrario dentro del proceso renderizador de Chrome.
Había una comprobación de tipo insuficiente en el código de manejo de trazas de pila asíncronas. Conduce a una confusión de tipos entre FunctionContext y NativeContext, causando un acceso ilegal al valor JSGlobalProxy->hash. Mediante heap spraying, el atacante pudo inyectar un marco de pila asíncrono falso y construir la primitiva fakeobj. Usando la primitiva fakeobj, el atacante pudo lograr la ejecución de código arbitrario en el proceso renderizador de Chrome.
Puedes consultar nuestras diapositivas de TyphoonCon 2024.
La asincronía es una de las características más importantes de JavaScript. En el pasado, era difícil depurar código asíncrono con la pila de errores porque las funciones asíncronas no se capturaban en la pila de errores. Las funciones asíncronas suspendidas se almacenan en la cola de callbacks del bucle de eventos, no en la pila de llamadas, por lo que la pila de errores no contenía la función asíncrona. Para resolver este problema, V8 ofrece la funcionalidad de "traza de pila asíncrona" (por defecto desde V8 v7.3) para capturar funciones asíncronas en la pila de errores. ([v8 blog], [v8 docs])
El "Closure de resolución de elementos de Promise.all" es una función auxiliar para resolver las promesas de entrada en la función Promise.all. La función Promise.all toma un array de promesas y devuelve una promesa que se resuelve cuando todas las promesas de entrada se han resuelto. El "Closure de resolución de elementos de Promise.all" es un manejador de resolución de cada promesa de entrada en la función Promise.all. Su función es resolver la promesa de entrada y almacenar el valor de cumplimiento en el array de resultados.
Hay 2 puntos a tener en cuenta sobre la función:
FunctionContext hasta que se llama, y luego tiene NativeContext después de ser llamada. (código v8)Clase de bug: Confusión de tipos entre FunctionContext y NativeContext
Detalles de la vulnerabilidad:
La vulnerabilidad se puede desencadenar capturando una traza de pila asíncrona con la función "Closure de resolución de elementos de Promise.all" ya ejecutada o funciones integradas intrínsecas similares. En este exploit, utilicé la función "Closure de resolución de elementos de Promise.all" como ejemplo.
Cuando se lanza un error en el código JavaScript, V8 captura la pila de errores de la pila y añade los marcos de pila asíncronos de la microtarea actual [1].
CallSiteBuilder builder(isolate, mode, limit, caller);
VisitStack(isolate, &builder);
// Si --async-stack-traces está habilitado y la "microtarea actual" es un
// PromiseReactionJobTask, intentamos enriquecer la traza de pila con marcos
// asíncronos.
if (v8_flags.async_stack_traces) {
CaptureAsyncStackTrace(isolate, &builder);
}
La función CaptureAsyncStackTrace [[2]] recorre la cadena de promesas y añade el marco de pila asíncrono según el tipo de llamada asíncrona (p. ej., await, Promise.all, Promise.any).
A continuación se muestra el fragmento de la función CaptureAsyncStackTrace que maneja el caso de 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);
// Ahora inspeccionamos el contexto del elemento de resolución de Promise.all()
// para encontrar la capacidad de promesa que se está resolviendo cuando todas
// las promesas concurrentes se resuelven.
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 (
Mientras recorre la cadena de promesas, si reaction->fulfill_handler es la función integrada "Closure de resolución de elementos de Promise.all", añade el marco de combinador de promesas asíncrono a la pila de errores. Luego, pasa a la siguiente promesa accediendo a function->context->capability->promise.
El problema es que la función asume que la función "Closure de resolución de elementos de Promise.all" no se ha ejecutado todavía. Si la función "Closure de resolución de elementos de Promise.all" ya se ha ejecutado, el contexto cambia de FunctionContext a NativeContext. Esto lleva a una confusión de tipos entre FunctionContext y NativeContext en la función CaptureAsyncStackTrace.
Creación del PoC:
La estrategia para desencadenar la vulnerabilidad es la siguiente:
FunctionContext a NativeContext.Usé el patrón de resolución de promesas síncrono para Promise.all con el fin de obtener la función "Closure de resolución de elementos de Promise.all" a nivel de script JS. Tomé prestado el patrón de los casos de prueba de test262.
Después de llamar explícitamente a la función, para desencadenar la vulnerabilidad, usé el código de ejemplo del [documento de traza de pila asíncrona de costo cero][v8 docs] para preparar una nueva cadena de promesas y establecer la función intrínseca integrada como manejador de cumplimiento de una de las promesas.