前段时间一直没有仔细研究原理, 基本就是到处抄exp加ai一把梭, 但感觉还是需要自己花时间研究学习才行, 不能太急于求成. 所以这次就花点时间研究下cve-2025-12429的原理.
这个洞属于TDZ HoleAttack的攻击面. 和之前的cve-2025-6554类似, 都是绕过v8的静态作用域分析检查

概览

  1. 作者的exp是怎么构造出越界读写的
    因为Turbofan在优化的时候把js数组的越界检查去掉了

  2. 为什么Turbofan会去掉数组检查
    因为作者构造了特殊未被检查的变量y, 被初始化为Hole, 导致Turbofan的推断出错

  3. 如何构造出未被检查的变量y?
    这是这个cve的重点, 下面慢慢分析

构造原理

还没有初始化, 但可能会在初始化前被访问的变量会被v8标记为HoleCheckMode::kRequired, 并且在初始化前会被赋值为Hole这个特殊的值. 而Hole是不属于js语义的, 所以在访问的时候会有一个TDZ check.
TDZ(temporal dead zone)用来形容这个初始化之前, 但属于作用域的区域.

问题出在TDZ check的优化. v8会静态分析程序运行流, 对于确保已经被TDZ check过的变量, 就会消除重复检查.
所以就有了各种各样奇形怪状的控制流构造, 绕过v8的静态分析, 而一旦能访问到遗漏检查的变量, 就可以获取Hole的值.

v8里TDZ check重复的检查是以作用域为单位. 在调用HoleCheckElisionScope会创建一个新的HoleCheckElision作用域. 其类似于一个栈的结构, 用于在parser遍历AST语法树的时候隔离各个作用域. 而很多控制流的构造就是找到是否HoleCheckElision的作用域与实际作用域并不匹配.

cve-2025-12429这个漏洞是commit [7ce3a5517944fdac428313d80f8cd49474dce667]引入的. 这个commit修复了另一个bug, 却引入了一个新bug, 真是哭笑不得.

diff中看到, VisitInHoleCheckElisionScope被从next的作用域移到了bodynext之前. 也就意味着body和next共享一个HoleCheckElision作用域.

1
2
3
4
5
6
7
8
9
10
11
12
13
 void BytecodeGenerator::VisitForStatement(ForStatement* stmt) {
@@ -2486,10 +2491,11 @@ void BytecodeGenerator::VisitForStatement(ForStatement* stmt) {
// flow like breaks or continues, has its own HoleCheckElisionScope. NEXT is
// therefore conditionally evaluated and also so has its own
// HoleCheckElisionScope.
+ HoleCheckElisionScope elider(this);
VisitIterationBody(stmt, &loop_builder);
if (stmt->next() != nullptr) {
builder()->SetStatementPosition(stmt->next());
- VisitInHoleCheckElisionScope(stmt->next());
+ Visit(stmt->next());
}
}

这时候再看poc, 由于body和next共享HoleCheckElision, 按顺序执行(没有continue时), body里面的y会先执行, 只要这个y有HoleCheck就行了, 所以use(y)处的HoleCheckElision就被消除了.

1
2
3
4
5
6
7
8
9
10
11
12
13
function use(x) {
% DebugPrint(x);
}
function pwn() {
for (var i = 0; i < 1; use(y)) {
if (i == 0)
continue;
y;
}
let y;
}

pwn();

思考

前段时间看到一个总结很有启发, ai类似一辆开的很快的车, 但并检查从使用者期待的路线上跑偏, 需要使用者来掌舵. 而且现阶段ai对于很多问题依然不能独立解决(至少独立挖v8是不太现实), 人依然需要保持学习, 不能幻想ai一把梭解决所有问题, 让ai去研究自己也不懂的复杂问题不是很靠谱.