Chrome 渲染器通过异步堆栈跟踪中的类型混淆进行 1day RCE(CVE-2023-6702)
概括
该漏洞允许远程攻击者在 Chrome 渲染器进程内执行任意代码。
异步堆栈跟踪处理代码中的类型检查不足。这会导致FunctionContext和之间的类型混淆NativeContext,从而导致非法访问值JSGlobalProxy->hash。通过堆喷射,攻击者能够注入伪造的异步堆栈框架,并构造fakeobj原语。使用fakeobj原语,攻击者能够在 Chrome 渲染器进程中实现任意代码执行。
您可以查看我们的 TyphoonCon 2024 幻灯片。
供应商/产品/版本
谷歌浏览器
-
受影响的版本:120.0.6099.109 之前版本
-
修复版本:120.0.6099.109
时间线
-
2020-05-13:引入 Bug – [Promise.any] 为 Promise.any 实现异步堆栈跟踪
-
2023-11-10:错误报告 -安全性:V8 调试检查失败:LAST_TYPE >= 值
-
2023-11-15:补丁 – [承诺,异步堆栈跟踪] 修复闭包已运行的情况
-
2023-12-12:公告 – https://chromereleases.googleblog.com/2023/12/stable-channel-update-for-desktop_12.html
-
2024-01-12:v8CTF 提交<–我们处理此漏洞的时间
-
2024-02-23:漏洞报告披露
背景
异步堆栈跟踪
异步是 JavaScript 中最重要的特性之一。过去,使用错误堆栈调试异步代码非常困难,因为错误堆栈中不会捕获异步函数。挂起的异步函数存储在事件循环的回调队列中,而不是调用堆栈中,因此错误堆栈不包含异步函数。为了解决这个问题,V8 提供了“异步堆栈跟踪”功能(自 V8 v7.3 起默认)来捕获错误堆栈中的异步函数。(v8 博客、v8 文档)
Promise.all 解析元素闭包
“Promise.all Resolve Element Closure” 是一个辅助函数,用于解析函数中的输入承诺Promise.all。Promise.all函数接受承诺数组,并返回一个承诺,该承诺在所有输入承诺都得到解析时解析。“Promise.all Resolve Element Closure” 是函数中每个输入承诺的解析处理程序Promise.all。该函数的作用是解析输入承诺并将实现值存储在结果数组中。
该函数有2点需要注意:
-
它是一个内在的内置函数,不能直接从 JavaScript 代码访问。
-
函数上下文用作标记,用于检查函数是否已执行。在函数FunctionContext被调用之前,它已执行;在函数NativeContext被调用之后,它已执行。(v8 代码)
漏洞
Bug 类别:FunctionContext和之间的类型混淆NativeContext
漏洞详细信息:
可以通过捕获已执行的“Promise.all Resolve Element Closure”函数或类似的内置函数的异步堆栈跟踪来触发此漏洞。在此漏洞利用中,我以“Promise.all Resolve Element Closure”函数为例。
当 JavaScript 代码中抛出错误时,V8 会从堆栈中捕获错误堆栈,并从当前微任务中附加异步堆栈帧 [ 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);
}
CaptureAsyncStackTrace 函数 [ 2 ] 查找承诺链,并根据异步调用类型(例如,,,)附加异步堆栈await框架。Promise.all
Promise.any
CaptureAsyncStackTrace下面是处理该案例的函数片段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 (
在查找承诺链时,如果reaction->fulfill_handler是“Promise.all Resolve Element Closure”内置函数,它会将异步承诺组合器框架附加到错误堆栈。然后,它通过访问移动到下一个承诺function->context->capability->promise。
问题在于,该函数假设“Promise.all Resolve Element Closure”函数尚未执行。如果“Promise.all Resolve Element Closure”函数已执行,则上下文将从 更改为FunctionContext。NativeContext这会导致函数中的FunctionContext和之间出现类型混淆。NativeContext
CaptureAsyncStackTrace
制作 PoC:
触发漏洞的策略如下:
-
获取“Promise.all Resolve Element Closure”函数,它是一个内在的内置函数。
-
明确调用“Promise.all Resolve Element Closure”函数将上下文从 更改FunctionContext为NativeContext。
-
将“Promise.all Resolve Element Closure”函数设置为具有新承诺链的承诺的履行处理程序。
-
在承诺链中抛出错误并捕获异步堆栈跟踪。
我使用了同步承诺解析模式来Promise.all在 JS 脚本级别获取“Promise.all 解析元素闭包”函数。我从 test262 测试用例中借用了该模式。
在明确调用该函数后,为了触发漏洞,我使用零成本异步堆栈跟踪文档中的示例代码来准备一个新的承诺链,并将内在的内置函数设置为其中一个承诺的履行处理程序。
最后,当抛出错误时,异步堆栈跟踪将被捕获,并使用已经执行的“Promise.all Resolve Element Closure”函数作为实现处理程序,从而导致FunctionContext和之间的类型混淆NativeContext。
以下是 PoC 代码:poc.js
https://github.com/kaist-hacking/CVE-2023-6702/blob/master/poc.js
漏洞利用
(这里定义了术语“漏洞利用原语”、“漏洞利用策略”、“漏洞利用技术和漏洞利用流程”。)
利用原语: fakeobj原语
利用策略: 为了利用类型混淆错误构建fakeobj原语,我使用了以下策略:
-
使用 JSPromise 对象进行堆喷射,将随机哈希数与有效的 JSPromise 对象指针相匹配。
-
使用哈希值作为有效的 JSPromise 对象指针并注入伪异步堆栈框架。
-
使用Error.prepareStackTrace方法getThis来检索虚假对象。
该漏洞导致函数中和的类型混淆。FunctionContext它访问以构建下一个异步堆栈框架。当漏洞被触发时,它会访问。为了利用该漏洞,我使用哈希值作为 JSPromise 对象指针。NativeContext
CaptureAsyncStackTrace
Context->PromiseCapability->JSPromise
NativeContext->JSGlobalProxy->hash
我们可以从以下哈希生成函数中检查哈希值的范围是(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 对象指针匹配,我们有 2 个约束:
-
解释的指针地址应为奇数。
-
我们必须在 (0, 0xfffff << 1) 范围内喷射堆。
按照这些限制,我在堆中喷射了 JSPromise 对象,并将地址左移 8 位以使地址变为奇数,并使用小的 for 循环来适应范围 (0, 0xfffff << 1)。
在这里,将随机哈希数与有效对象指针匹配的几率看起来相当低。为了提高可靠性,我使用了iframe 技术。由于 Chrome 中的站点隔离,来自不同网站的页面在不同的进程中运行。因此,我创建了一个具有不同域的 iframe,并在 iframe 中运行漏洞,以避免主进程崩溃。
在移动到承诺链中的下一个承诺之后,程序将检查承诺的有效性,并尝试根据异步调用类型附加异步堆栈框架。
while (!builder->Full()) {
// Check that the {promise} is not settled.
if (promise->status() != Promise::kPending) return;
// Check that we have exactly one PromiseReaction on the {promise}.
if (!IsPromiseReaction(promise->reactions())) return;
Handle<PromiseReaction> reaction(
PromiseReaction::cast(promise->reactions()), isolate);
if (!IsSmi(reaction->next())) return;
// Check if the {reaction} has one of the known async function or
// async generator continuations as its fulfill handler.
if (IsBuiltinFunction(isolate, reaction->fulfill_handler(),
Builtin::kAsyncFunctionAwaitResolveClosure) ||
IsBuiltinFunction(isolate, reaction->fulfill_handler(),
Builtin::kAsyncGeneratorAwaitResolveClosure) ||
IsBuiltinFunction(
isolate, reaction->fulfill_handler(),
Builtin::kAsyncGeneratorYieldWithAwaitResolveClosure)) {
// Now peek into the handlers' AwaitContext to get to
// the JSGeneratorObject for the async function.
Handle<Context> context(
JSFunction::cast(reaction->fulfill_handler())->context(), isolate);
Handle<JSGeneratorObject> generator_object(
JSGeneratorObject::cast(context->extension()), isolate);
CHECK(generator_object->is_suspended());
// Append async frame corresponding to the {generator_object}.
builder->AppendAsyncFrame(generator_object);
我们选择case 是因为函数kAsyncFunctionAwaitResolveClosure的参数是完全可控的。AppendAsyncFramegenerator_object
通过设置适当的伪造对象(如PromiseReaction,Function,Context,JSGeneratorObject)来传递条件,我们可以通过调用来注入我们的伪造异步框架builder->AppendAsyncFrame(generator_object)。我们可以从终端检查注入的伪造异步框架。
Error: Let's have a look...
at bar (../../../../fake_frame.js:168:15)
at async foo (../../../../fake_frame.js:163:9)
at async Promise.all (index 0)
at async Array.sloppy_func (../../../../fake_frame.js:1:1)
这是fake_frame.js代码。https://github.com/kaist-hacking/CVE-2023-6702/blob/master/fake_frame.js
注入伪异步框架后,我使用Error.prepareStackTrace
withgetThis
方法获取receiver错误对象(在本例中为JSGeneratorObject)。使用receiver,我们可以从堆中检索伪对象(fakeobj原语)。
漏洞利用流程:我使用了 V8 漏洞利用的典型漏洞利用流程。
-
使用fakeobj原语,我植入并检索了伪 OOB 数组。
-
使用伪 OOB 数组,我构建了 caged_read/caged_write 原语。
-
对于 RCE,我参考了Google CTF 2023 分享的技术。为了逃离 V8 沙箱,我破坏了 BytecodeArray 对象以执行任意字节码。使用具有越界访问的 Ldar/Star 指令,我们可以读写堆栈。为了泄漏 chrome 二进制基址,我从堆栈中读取返回地址以泄漏基址的低 32 位,并读取 libc 堆指针以获取地址的高 16 位。然后,我破坏了用于堆栈旋转的帧指针并执行 ROP 链以实现 RCE。
这是完整的漏洞代码:index.html和exploit.html 它在 Chrome 118.0.5993.70 上进行了测试,这是 v8CTF M118 的目标版本。
致谢:韩国科学技术研究院黑客实验室的 Haein Lee
https://github.com/kaist-hacking/CVE-2023-6702
感谢您抽出
.
.
来阅读本文
点它,分享点赞在看都在这里
原文始发于微信公众号(Ots安全):Chrome Renderer 通过异步堆栈跟踪中的类型混淆进行