此漏洞允许远程攻击者在 Chrome 渲染器进程中执行任意代码。
在异步堆栈跟踪处理代码中存在一个不充分的类型检查。
它导致了 FunctionContext 和 NativeContext 之间的类型混淆,从而非法访问 JSGlobalProxy->hash 值。
通过堆喷射,攻击者能够注入伪造的异步堆栈帧,并构建 fakeobj 原语。
利用 fakeobj 原语,攻击者能够在 Chrome 渲染器进程中实现任意代码执行。
你可以查看 我们在 TyphoonCon 2024 上的幻灯片。
异步是 JavaScript 中最重要的特性之一。 过去,由于异步函数不会被捕获在错误堆栈中,调试异步代码的堆栈信息非常困难。 被挂起的异步函数存储在事件循环的回调队列中,而非调用栈中,因此错误堆栈不包含该异步函数。 为了解决这个问题,V8 提供了“异步堆栈跟踪”功能(自 V8 v7.3 起默认启用),以在错误堆栈中捕获异步函数。([v8 博客], [v8 文档])
“Promise.all Resolve Element Closure”是一个辅助函数,用于解析 Promise.all 函数中的输入 promise。
Promise.all 函数接收一个 promise 数组,并返回一个在所有输入 promise 都 resolve 后 resolve 的 promise。
“Promise.all Resolve Element Closure”是 Promise.all 函数中每个输入 promise 的 resolve 处理器。
该函数的作用是解析输入 promise 并将 fulfillment 值存储到结果数组中。
关于此函数有两点需要注意:
FunctionContext,在调用之后,它拥有 NativeContext。(v8 代码)漏洞类型: FunctionContext 与 NativeContext 之间的类型混淆
漏洞细节:
该漏洞可以通过在已执行的“Promise.all Resolve Element Closure”函数或类似的内置函数上捕获异步堆栈跟踪来触发。 在此漏洞利用中,我以“Promise.all Resolve Element Closure”函数为例。
当 JavaScript 代码中抛出错误时,V8 从堆栈中捕获错误堆栈,并将当前微任务中的异步堆栈帧追加到其中 [1]。
CallSiteBuilder builder(isolate, mode, limit, caller);
VisitStack(isolate, &builder);
// 如果启用了 --async-stack-traces 并且“当前微任务”是一个
// PromiseReactionJobTask,我们尝试用异步帧丰富堆栈跟踪。
if (v8_flags.async_stack_traces) {
CaptureAsyncStackTrace(isolate, &builder);
}
CaptureAsyncStackTrace 函数 [[2]] 会查找 promise 链,并根据异步调用类型(例如 await、Promise.all、Promise.any)追加异步堆栈帧。
下面是处理 Promise.all 情况的 CaptureAsyncStackTrace 函数片段:
} 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);
// 现在窥视 Promise.all() 的 resolve 元素上下文,以
// 找到当所有并发 promise 都 resolve 时正在被 resolve 的 promise capability。
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 (
在查找 promise 链时,如果 reaction->fulfill_handler 是“Promise.all Resolve Element Closure”内置函数,它会将异步 promise 组合器帧追加到错误堆栈中。
然后,通过访问 function->context->capability->promise 移动到下一个 promise。
问题在于该函数假设“Promise.all Resolve Element Closure”函数尚未被执行。
如果“Promise.all Resolve Element Closure”函数已被执行,其上下文会从 FunctionContext 变为 NativeContext。
这会导致 CaptureAsyncStackTrace 函数中出现 FunctionContext 与 NativeContext 之间的类型混淆。
构造 PoC:
触发漏洞的策略如下:
FunctionContext 变为 NativeContext。我使用了 Promise.all 的同步 promise 解析模式,以便在 JS 脚本层面获取“Promise.all Resolve Element Closure”函数。
我从 test262 测试用例中借鉴了该模式。
显式调用该函数后,为了触发漏洞,我使用了 [零成本异步堆栈跟踪文档][v8 docs] 中的示例代码来准备一个新的 promise 链,并将该内置函数设置为其中一个 promise 的 fulfill 处理器。
最后,当错误被抛出时,异步堆栈跟踪会以已执行的“Promise.all Resolve Element Closure”函数作为 fulfill 处理器被捕获,从而导致 FunctionContext 与 NativeContext 之间的类型混淆。
PoC 代码如下:poc.js
(关于漏洞利用原语、利用策略、利用技术和利用流程,此处有定义。)
漏洞利用原语: fakeobj 原语
漏洞利用策略: 为了从类型混淆漏洞构建 fakeobj 原语,我使用了以下策略:
Error.prepareStackTrace 配合 getThis 方法来检索伪造的对象。该漏洞导致 CaptureAsyncStackTrace 函数中出现 FunctionContext 与 NativeContext 之间的类型混淆。
它访问 Context->PromiseCapability->JSPromise 来构建下一个异步堆栈帧。
当漏洞被触发时,它访问 NativeContext->JSGlobalProxy->hash。
为了利用该漏洞,我将哈希值用作 JSPromise 对象指针。
从以下哈希生成函数可以看出,哈希值的范围是 (0, 0xfffff):
int Isolate::GenerateIdentityHash(uint32_t mask) {
int hash;
int attempts = 0;
do {
hash = random_number_generator()->NextInt() & mask;
} while (hash == 0 && attempts++ < 30);
return hash != 0 ? hash : 1;
}
pwndbg> p/x mask
$1 = 0xfffff
哈希值经过 SMI 标记,因此在内存中,它将存储为 hash << 1。
因此,内存中的值范围是 (0, 0xfffff << 1),且为偶数。
为了使随机哈希数匹配到一个有效的 JSPromise 对象指针,我们需要满足两个约束条件:
根据这些约束,我使用 JSPromise 对象进行堆喷射,并左移 8 位使地址变为奇数,同时使用小的 for 循环以适应 (0, 0xfffff << 1) 的范围。
将随机哈希数匹配到一个有效的对象指针看起来成功率较低。 为了提高可靠性,我使用了 iframe 技术。 由于站点隔离,来自不同网站的页面运行在不同的进程中。 因此,我创建了一个不同域的 iframe,并在该 iframe 中运行漏洞利用,以避免主进程崩溃。
移动到 promise 链中的下一个 promise 后,程序会检查该 promise 的有效性,并尝试根据异步调用类型追加异步堆栈帧。
while (!builder->Full()) {
// 检查 {promise} 是否未 settle。
if (promise->status() != Promise::kPending) return;
// 检查 {promise} 上是否恰好有一个 PromiseReaction。
if (!IsPromiseReaction(promise->reactions())) return;
Handle<PromiseReaction> reaction(
PromiseReaction::cast(promise->reactions()), isolate);
if (!IsSmi(reaction->next())) return;
// 检查 {reaction} 的 fulfill 处理器是否为已知的异步函数或
// 异步生成器延续之一。
if (IsBuiltinFunction(isolate, reaction->fulfill_handler(),
Builtin::kAsyncFunctionAwaitResolveClosure) ||
IsBuiltinFunction(isolate, reaction->fulfill_handler(),
Builtin::kAsyncGeneratorAwaitResolveClosure) ||
IsBuiltinFunction(
isolate, reaction->fulfill_handler(),
Builtin::kAsyncGeneratorYieldWithAwaitResolveClosure)) {
// 现在窥视处理器的 AwaitContext 以获取
// 异步函数的 JSGeneratorObject。
Handle<Context> context(
JSFunction::cast(reaction->fulfill_handler())->context(), isolate);
Handle<JSGeneratorObject> generator_object(
JSGeneratorObject::cast(context->extension()), isolate);
CHECK(generator_object->is_suspended());
// 追加与 {generator_object} 对应的异步帧。
builder->AppendAsyncFrame(generator_object);
我们选择了 kAsyncFunctionAwaitResolveClosure 情况,因为 AppendAsyncFrame 函数的参数 generator_object 是完全可控的。
通过设置适当的伪造对象(如 PromiseReaction、Function、Context、JSGeneratorObject)来满足条件,我们可以通过调用 builder->AppendAsyncFrame(generator_object) 注入我们伪造的异步帧。
我们可以从终端查看注入的伪造异步帧。