首页 时政热点 科技头条 智能AI 安全攻防 数码硬件 开发者生态 归档 标签 关于

Chrome V8 UnwindAndFindHandler 物化对象漏洞利用分析

摘要

TL;DR 近期作者在学习 Chrome V8 漏洞利用相关技术,看了一下 Starlabs的师傅分享的一篇文章 ,从中学习了 V8 漏洞分析与利用的思路。顺着这套研究范式,作者选择了一个 V8CTF 里的漏洞开展实践 —— 该漏洞暂无 CVE 编号,暂无公开利用信息,正好适合练手 本文记录了对该漏洞的根因分析、POC 构造、调试踩坑以及利用思路的完整实践过程 简介 翻阅补丁后可以确定,漏洞位于 ...

the frame iter materialized objects for cleanup continue Remove removed
2026-11-10 1 阅读 约9分钟阅读 先知社区
分享:
字号:
TL;DR近期作者在学习Chrome V8漏洞利用相关技术,看了一下Starlabs的师傅分享的一篇文章,初步学习了V8漏洞分析与利用的思路。顺着这套研究范式,作者选择了一个V8CTF里的漏洞开展实践——漏洞暂无CVE编号,暂无公开利用信息,正好适合练手下文记录了漏洞的根因分析、POC构造、调试坑以及利用方法的完整实践过程简介翻阅文档补丁后可以确定,漏洞位于V8的UnwindAndFindHandler栈展开逻辑,和Fast API异常展开过程中MaterializedObjectStore物化对象的清理逻辑相关。撰写论文时,该漏洞还没有公开的完整分析和利用链,这也成为一个很合适的研究目标。作者仅依赖阿富汗逆向原因修复逻辑,理解根,并自行编写了PoC验证该漏洞的利用逆向补丁未打之前补丁会发生什么?我们逐条看看它到底改了什么 @@ -2918,6 +2918,12 @@ Tagged Isolate::UnwindAndFindHandler() { for (StackFrameIterator iter(this, thread_local_top());; iter.Advance()) { if (ignore_next_frame) {ignore_next_frame = false; } + // 由于我们要跳过下一帧,因此我们必须在这里手动对其执行 + // 展开清理,因为“继续”将绕过循环体末尾的 + // 清理。 + if (iter.frame()->is_optimized_js()) { + Materialized_object_store_->Remove(iter.frame()->fp()); + } 继续; int Visited_frames = iter.frame()->iteration_depth(); 六行代码,一个文件,而且只加不删——没有任何一行被改写。光是这一点就已经把方向指示来了:*不是某个条件判断写错了,而是少做了一件事。提交信息其实已经把话说得很白了:但是,使用简单的继续跳过帧的迭代绕过了循环体末尾的每帧展开清理,从而从 MaterializedObjectStore 中删除了物化对象。当堆栈帧随后被另一个函数在相同的调用深度重用时,这会泄漏物化对象并导致崩溃(由于陈旧的物化对象)。 最后一句值得两遍:当堆栈帧随后被另一个函数在相同的调用深度重用时,栈帧被另一个函数同调用深度上复用——这不是一句崩溃描述,这是一个原语把UnwindAndFindHandler函数代码拉到最底部可以,看到在正常的逻辑中,当函数栈帧发生unwind时,会执行删除操作清除物化对象的指针 if (frame->is_optimized_js()) { // 删除每帧读取存储的物化对象。 bool 已删除 = Materialized_object_store_->Remove(frame->fp());使用(已删除); // 如果有任何物化对象,则代码应该 // 标记为 deopt。 DCHECK_IMPLIES(删除, 框架->LookupCode()->marked_for_deoptimization());而在该分支的代码中,将ignore_next_frame设置为true case StackFrame::INTERNAL: { auto code = frame->GcSafeLookupCode(); if (code->builtin_id() ==builtin::kDeoptimizationEntry_LazyAfterFastCall) { // 如果快速 API 调用者被标记为去优化,内置的去优化条目会对快速 API 进行异常处理。 // 因此,即使 `frame->fp()` 表示我们来自 // 内置的去优化入口,我们也必须 // 进行堆栈展开,就像我们来自快速调用一样。 // 快速调用的返回地址仍然存储在isolate 中,因此我们可以从 // 那里加载它。地址 fast_call_pc =isolate_data()->fast_c_call_caller_pc(); std::可选> 结果 = inside_pointer_to_code_cache()->GetCacheEntry(fast_call_pc)->code; CHECK(!结果->is_null()); Tagged caller_code = result.value(); int caller_offset = caller_code->GetOffsetFromInstructionStart(this, fast_call_pc); // 检查是否确实在快速调用的 // 返回地址处注册了异常处理程序。如果是这样,那么我们 // 告诉反优化器该异常,并返回 // 反优化条目。否则,我们进一步迭代堆栈以找到处理程序。 HandlerTable 表(TrustedCast(caller_code)); int handler_offset = table.LookupReturn(caller_offset); if (handler_offset < 0) { // 没有为快速调用注册处理程序。 // 因此,堆栈迭代将继续下一帧, // 这是快速 API 调用的调用者的帧。 // 我们已经在此处检查了该框架,因此无需再次检查。另外, // 下一帧的 `pc` 被调用指令关闭,因为 // 对于正常的 deopt,堆栈上的返回地址将指向 // 调用的 deopt 出口,但是要到达 // 当前代码位置,deopt 出口已经被调用, // 到 deopt 出口在堆栈上之后的地址作为返回地址。忽略下一个帧=真;休息; }最终回到patch修复的部分代码时,会直接执行继续跳过循环,循环执行卸载清除的操作就成功被跳过了 for (StackFrameIterator iter(this, thread_local_top());; iter.Advance()) { if (ignore_next_frame) {ignore_next_frame = false; }继续;这本身就是一处完全合理的优化。异常从快速 API 调用里喷射去、而方已经被标记为待优化去时,调用展开器会四分之一帧 DeoptimizationEntry_LazyAfterFastCall 改道;此时调用方那一帧其实已经被检查过一遍了,于是设个标志,下一轮不要再扫一遍让问题只出在实现方式上:「跳过这一帧」被写成了 continue —— 它跳过了这一帧,同时也跳过了对这一帧的记账MaterializedObjectStore 是每个isolate一张的表,键是指针指针,值为一个装「彼此已物化对象」的FixedArray。注意它的键既不是函数、也不是code对象、也不是调用点,就是栈地址 fp : class MaterializedObjectStore { public: DirectHandle
这篇文章对您有帮助吗?

订阅66必读

每日精选科技资讯,直达你的邮箱