Chrome V8 在野 0day CVE-2025-6554 深度分析
说点题外话:这是一个非常清爽的类型混淆漏洞,代码简洁优雅,看得人赏心悦目。
📋 漏洞概述
CVE-2025-6554 是 Chrome V8 JavaScript 引擎中的一个类型混淆漏洞(Type Confusion) 。攻击者可以通过构造特定的 JavaScript 表达式触发该漏洞,最终可能导致远程代码执行(RCE) 。
漏洞时间线
- 2025年6月30日:Google 修复漏洞并确认存在在野利用
- 2025年7月2日:研究员 @DarkNavyOrg 在 X 平台公开 PoC
- 修复版本:Commit
22e9d9621de58ec6fe6581b56215059a48451b9f
修复 Commit 信息
22e9d96 [interpreter] don't elide hole checks across optional chain
Bug: 427663123
Author: Stephen Roettger <sroettger@google.com>
Date: Thu Jun 26 08:33:16 2025
🔍 一、漏洞原理深度剖析
1.1 PoC 分析
function f() {
let x;
delete x?.[y]?.a; // 核心:可选链 + delete 操作
return y;
let y; // TDZ: y 声明在 return 之后
}
let hole = f();
let map = new Map();
map.delete(hole); // 触发类型混淆
1.2 关键技术点解析
1) 可选链操作符(Optional Chaining)?.
- 可选链的语义是:如果左侧表达式为
null或undefined,则短路返回undefined,不会访问后续属性 - 在 PoC 中:
x为undefined→x?.[y]应该返回undefined - V8 在字节码生成阶段,遇到
x为undefined时会通过JumpIfUndefinedOrNull指令跳转,绕过对y的访问
2) "Hole" 类型的本质
- Hole 是 V8 引擎内部的特殊值,用于表示数组/对象中的"空位"(如
[1,,3]中的中间元素) - Hole 本不应该暴露给 JavaScript 用户代码,但通过特定构造可以让其"泄露"出来
- 历史上通过 Hole 实现代码执行的案例包括 CVE-2023-3079
3) 暂时性死区(Temporal Dead Zone, TDZ)
- 使用
let/const声明的变量在初始化前无法访问,访问会抛出ReferenceError - 在 PoC 中:
y在delete表达式中被使用,但其let y声明在后面 - V8 内部通过将 TDZ 变量的值设为 Hole 来实现这一机制,当检测到 Hole 时会调用
ThrowReferenceErrorIfHole抛出错误
1.3 漏洞触发流程
初始状态:
├─ x = undefined (未初始化)
└─ y = hole (TDZ 状态)
执行 delete x?.[y]?.a:
├─ 判断 x == undefined ✓
├─ 跳转到短路逻辑
├─ 【漏洞点】跳过了 y 的 Hole 检查
└─ 直接返回 y (此时 y 仍是 hole)
结果:函数返回了本不应暴露的 Hole 值
🛠️ 二、字节码层面的漏洞根因
通过 d8 --print-bytecode 输出的字节码可以清晰看到问题:
2.1 漏洞版本字节码(简化)
0: LdaTheHole ; r1 = hole (y 的初始值)
1: Star1 ; 存储到 r1
2: LdaUndefined ; r2 = undefined
3: Star2 ; 存储到 r2 (x 的值)
7: JumpIfUndefinedOrNull [25] ; 如果 x 是 undefined/null,跳到 25
9: Ldar r1 ; 加载 y
11: ThrowReferenceErrorIfHole ; 【检查点1】检测 y 是否为 hole
; ... 访问 x[y].a 的逻辑 ...
25: Ldar r1 ; 【漏洞!】直接加载 y (hole)
26: Return ; 返回 hole 值 ❌
问题所在:
- 当
x为undefined时,代码跳转到第 25 行,直接返回y - 返回路径上没有
ThrowReferenceErrorIfHole检查,导致 Hole 值被返回
2.2 修复后字节码(简化)
0: LdaTheHole
1: Star1
2: LdaUndefined
3: Star2
7: JumpIfUndefinedOrNull [27] ; 跳转目标调整
9: Ldar r1
11: ThrowReferenceErrorIfHole ; 【检查点1】
; ... x[y].a 逻辑 ...
27: Ldar r1 ; 加载 y
28: ThrowReferenceErrorIfHole ; 【检查点2】新增检查 ✓
29: Return ; 安全返回
🔧 三、修复原理详解
3.1 核心修改:引入 HoleCheckElisionScope
修复的核心是在 OptionalChainNullLabelScope 函数中增加了 HoleCheckElisionScope。
修改前的代码逻辑
// src/interpreter/bytecode-generator.cc (漏洞版本)
void BytecodeGenerator::VisitDelete(...) {
// ... 可选链处理 ...
if (expr->IsOptionalChain()) {
OptionalChainNullLabelScope scope(this);
// ❌ 缺少 HoleCheckElisionScope
VisitForAccumulatorValue(property); // 触发 BuildThrowIfHole
// hole check 状态"污染"了父级作用域
}
// ...
}
修改后的代码逻辑
void BytecodeGenerator::VisitDelete(...) {
if (expr->IsOptionalChain()) {
OptionalChainNullLabelScope scope(this);
HoleCheckElisionScope hole_check_scope_(this); // ✅ 新增
VisitForAccumulatorValue(property);
// hole check 状态被限制在当前作用域
}
}
3.2 hole_check_bitmap_ 机制详解
V8 使用 hole_check_bitmap_ 位图记录每个基本块中哪些变量已完成 Hole 检查:
工作原理
class HoleCheckElisionScope {
public:
HoleCheckElisionScope(BytecodeGenerator* gen)
: generator_(gen),
prev_bitmap_(gen->hole_check_bitmap_) {
// 保存旧 bitmap,创建新的空白 bitmap
generator_->hole_check_bitmap_ = {};
}
~HoleCheckElisionScope() {
// 恢复旧 bitmap
generator_->hole_check_bitmap_ = prev_bitmap_;
}
private:
BytecodeGenerator* generator_;
HoleCheckBitmap prev_bitmap_;
};
Bitmap 的作用
- 进入新的基本块时,创建独立的 bitmap
- 变量通过 Hole 检查后,调用
RememberHoleCheckInCurrentBlock标记该变量为"已检查" - 后续访问同一变量时,
VariableNeedsHoleCheckInCurrentBlock检测 bitmap,若已检查则跳过 - 退出基本块时,恢复上一级的 bitmap
3.3 漏洞的根本原因
没有使用 HoleCheckElisionScope 时,VisitDelete 中对 y 的 Hole 检查状态会"污染"父级的 bitmap:
基本流程:
1. 进入函数 f(),创建空白 basicBitmap
2. 进入 VisitDelete (处理 delete x?.[y]?.a)
- ❌ 没有创建独立的 currentBitmap
- 调用 BuildThrowIfHole 检查 y
- 通过 RememberHoleCheckInCurrentBlock 标记 y 已检查
- 【问题】标记写入了 basicBitmap ❌
3. 由于 x == undefined,字节码跳转到 return y
4. return y 执行时:
- 调用 VariableNeedsHoleCheckInCurrentBlock 检查 basicBitmap
- 发现 y 已标记为"已检查" ✓
- 跳过 BuildThrowIfHole ❌
- 直接返回 Hole 值 ❌
修复后,HoleCheckElisionScope 将 VisitDelete 中的检查状态隔离,不会影响外部的 return y。
📊 四、修复效果对比
4.1 函数调用栈对比
漏洞版本(仅1次 Hole 检查)
BuildThrowIfHole (y)
├─ 调用来源: VisitDelete
│ └─ 处理 x?.[y]?.a
├─ 标记写入: basicBitmap ❌
└─ return y 时跳过检查 ❌
修复版本(2次 Hole 检查)
第1次 BuildThrowIfHole (y)
├─ 调用来源: VisitDelete
│ └─ HoleCheckElisionScope 隔离 ✓
└─ 标记写入: currentBitmap (不污染 basicBitmap)
第2次 BuildThrowIfHole (y)
├─ 调用来源: VisitReturnStatement
├─ 检查 basicBitmap: y 未标记为已检查 ✓
└─ 生成 ThrowReferenceErrorIfHole 指令 ✓
4.2 测试用例验证
PoC 来源:DarkNavySecurity/PoC(写得非常简洁优雅 👍)
function f() {
let x;
delete x?.[y]?.a;
return y;
let y;
}
let hole = f();
print(%StrictEqual(hole, %TheHole())); // 修复前: true, 修复后: 抛出 ReferenceError
let map = new Map();
map.delete(hole);
修复前行为:
- 返回
%TheHole()内部值 map.delete(hole)触发类型混淆
修复后行为:
return y时抛出ReferenceError- 无法获取 Hole 值,漏洞消除
🔬 五、复现环境搭建
5.1 获取漏洞版本
# 漏洞版本(修复前)
git clone https://chromium.googlesource.com/v8/v8.git
cd v8
git checkout 47c9ee633a84c0f9b990dee0b9f288e03cbcf495
# 编译
gclient sync
tools/dev/gm.py x64.debug
5.2 复现结果
- 时间:2025年10月9日
- 状态:✅ 复现成功,触发稳定
- 备注:之前复现失败是环境配置问题(已解决)
📚 六、关键术语表
| 术语 | 解释 |
|---|---|
| Hole | V8 内部表示"空位"的特殊值,不应暴露给 JS 代码 |
| TDZ | 暂时性死区,let/const 变量在声明前的不可访问状态 |
| Hole Check Elision | 跳过 Hole 检查的优化机制(需谨慎使用) |
| HoleCheckElisionScope | 限制 Hole 检查状态作用域的 RAII 类 |
| hole_check_bitmap_ | 记录变量 Hole 检查状态的位图 |
🎯 七、安全启示
7.1 对开发者的建议
- 谨慎使用可选链与 delete 组合:这种边缘场景可能触发引擎优化漏洞
- 避免依赖 TDZ 的边界行为:变量应在声明后再使用
7.2 对引擎开发者的教训
- 优化隔离原则:任何改变检查状态的操作必须使用
HoleCheckElisionScope隔离 - 对称性要求:
BuildThrowIfHole和HoleCheckElisionScope必须成对出现 - 复杂控制流的完整性验证:可选链的多个分支都需要完整的安全检查