CVE-2025-13224 分析¶
基本信息¶
| 项目 | 内容 |
|---|---|
| 漏洞编号 | CVE-2025-13224 |
| 官方描述 | Type Confusion in V8 in Google Chrome prior to 142.0.7444.175 allowed a remote attacker to potentially exploit heap corruption via a crafted HTML page |
| Chromium 安全等级 | High |
| 修复版本 | Chrome 142.0.7444.175 之前受影响 |
| 公开 issue | 450328966 |
| 漏洞位置 | V8 LoadSuperIC / accessor getter 调用路径 |
| 核心问题 | super 属性访问命中 accessor 后,primitive receiver 在 CallGetterIfAccessor 路径中仍按 JSReceiver 处理 |
漏洞概览¶
这个漏洞不是固定偏移字段读取,也不是典型的对象布局错位。问题发生在 super 属性访问命中 accessor 之后的 getter 调用阶段。
super.prop 本身就包含两层语义。属性查找围绕 lookup_start_object 展开,getter 调用围绕实际 receiver 展开。只要这两个值被人为拆开,就可能出现这样的状态:查找命中的是一个完全合法的 accessor,但真正传给 getter 的 receiver 却不是对象。
CVE-2025-13224 的核心就在这里。漏洞版本中,LoadSuperIC 可以从合法对象上查到 accessor,但在后续 CallGetterIfAccessor 路径里,实际 receiver 仍然按默认的 kExpectingJSReceiver 约束处理。当 receiver 是 primitive,尤其是 Smi 时,这条路径就把一个非对象值当成了 JSReceiver。
PoC 与 Root Cause¶
PoC¶
const err = new Error();
class B {
m() {
return super.stack;
}
}
Object.setPrototypeOf(B.prototype, err);
const b = new B();
b.m.call(0x4141414 >> 1);
这段代码只做了三件事。第一,用 err.stack 提供一个稳定可命中的 accessor。第二,通过 Object.setPrototypeOf(B.prototype, err) 把 super.stack 的查找起点重定向到 err。第三,用 .call(0x4141414 >> 1) 把实际 receiver 换成 primitive。
因此,这个 PoC 的真正构造状态不是“错误地访问了某个属性”,而是把两个本来不该拆开的对象强行拆开了:
- 查找对象是
err - 调用对象是 primitive / Smi
执行链条可以压成下面这样:
super.stack
-> 从 err 查到 accessor
-> 命中 LoadSuperIC 的 accessor 路径
-> 调用 CallGetterIfAccessor
-> 传入的 receiver 实际是 Smi
查找阶段本身没有问题。super.stack 本来就应该从 B.prototype 的原型开始查找,手动把这条原型链改到 err 之后,命中 err.stack 这个 accessor 也完全符合语义。真正的失配发生在命中 accessor 之后。此时执行路径已经从“属性查找”切换到“getter 调用”,后半段不再只是问“属性在哪里”,而是要回答“getter 以谁为 this 调用”。
问题就在这里。CallGetterIfAccessor 默认要求传入的 receiver 满足 JSReceiver 约束,但 LoadSuperIC 在这个场景下没有继续维护前后两段语义的一致性。前半段只保证了查找对象是合法的,后半段却直接把实际 receiver 继续传下去。当实际 receiver 已经被 .call() 换成 primitive,这条路径仍然按 kExpectingJSReceiver 模式执行,于是最终把一个 Smi 当成对象使用。
把 root cause 压成一句话,可以写成:
LoadSuperIC在super访问中把“从lookup_start_object查找属性”和“以实际 receiver 调用 getter”分开处理,但在命中 accessor 后,后续CallGetterIfAccessor仍按kExpectingJSReceiver模式使用 receiver,没有对 primitive receiver 做必要的转换或约束检查,最终导致 Smi 被错误地按JSReceiver解释。
从对象类别上看,这里的 confusion 不是两个堆对象布局之间的互相解释,而是:
这也是它和 30517 的根本区别。30517 最后会落到固定偏移读取,13224 则停在调用边界。
为什么难以利用¶
这个洞难利用,首先是因为它停在了非法调用这一层,而不是稳定的错误读取。30517 一类漏洞之所以容易往下推进,是因为错误路径最终会在某个固定偏移上读到稳定数据,进而继续做 fakeobj、addrof 或任意读写。13224 并不具备这种条件。它的后果优先表现为“一个不合法的 receiver 被送进 getter 调用边界”,随后通常直接崩溃或异常退出,而不是返回某个还能继续加工的中间值。
其次,PoC 命中的是 Error.stack 这类 API accessor。accessor 本身就比普通对象字段更难利用,因为它不是一段可以靠对象布局细调的固定偏移读取,也不是那种“错一个对象就稳定读到另一块内存”的专用 handler。这里真正被控制的是“查找能不能命中 accessor”,而不是“命中后能稳定读出什么字段”。
最后,primitive receiver 本身也不能承载后续原语。PoC 里传下去的是 Smi。Smi 的优点只是触发稳定,但它不是堆对象,没有 map、properties、elements,也没有可布置的对象布局。换句话说,这里能控制的是“把什么值送进调用路径”,却不能像 30517 那样再去安排“这个值在堆上长成什么样”。查找对象 err 和调用对象 primitive 之间因此也没有形成可持续放大的错位关系。这条链更像一次语义级错误调用,而不是内存原语入口。
分析路径¶
先确认问题一定经过 accessor¶
分析时先做的是把触发点收窄。这里最关键的不是 super,也不是 Error,而是 stack 必须是 accessor。如果 err.stack 只是普通 data property,那么整个访问最多只是一次普通属性返回,不会进入 getter 调用边界,也就不会走到 CallGetterIfAccessor。所以第一步先确认 err.stack 的属性类型,目的是把问题范围从“super property access”收窄到“super property access 命中 accessor”。
这一步确认之后,后面的分析重点就不再是属性查找是否正确,而是 getter 调用时 receiver 有没有被正确处理。
再确认 super 查找起点确实被改到了 err¶
下一步要确认的不是 this,而是 lookup_start_object。这类 PoC 容易误判的地方在于:表面上执行的是 b.m.call(...),直觉上会一直盯着调用 receiver 看,但真正决定属性从哪里开始查找的是 B.prototype 的原型。
这里的 Object.setPrototypeOf(B.prototype, err) 需要单独拎出来验证,因为它决定了 super.stack 最终是不是从 err 开始查。如果这一步不成立,那么后面即便 .call() 传入 primitive,也只是一次普通 receiver 变化,不会命中 err.stack 这个 accessor。
因此第二步的目标是把两件事拆清楚:属性是从 err 查到的,receiver 却不是 err。漏洞后面所有问题都建立在这个分离状态上。
确认真正传下去的 receiver 已经变成了 Smi¶
再往后看的是 .call(0x4141414 >> 1) 这一层。这里的重点不是数值本身,而是它会在 V8 内部变成一个 Smi。也就是说,PoC 并不是简单地“把 this 改掉”,而是把调用 receiver 明确换成了一个非对象值。
这一步一旦确认,整个问题就可以被精确描述成:查找阶段依赖的是合法对象 err,调用阶段依赖的却是 Smi。于是后面只要某条路径仍然默认“传下去的 receiver 一定是 JSReceiver”,漏洞就成立。
沿 err 的对象布局确认 stack 背后确实是 AccessorPair¶
在把语义路径对齐之后,接下来做的是 DebugPrint(err) 和内存查看。这个步骤的目的不是做 exploit,而是确认 err.stack 在对象层面到底对应什么。调试结果可以看到 stack 对应的是 AccessorPair,getter / setter 也都能在相应位置被观察到。
这一层分析很重要,因为它把抽象语义里的“这是一个 accessor”落到了具体对象表示上。到这里可以确认,PoC 后半段不是在误打误撞地触发某条普通属性路径,而是在稳定进入一条 accessor getter 调用路径。
评估 err 本身能否像 30517 那样继续承载原语¶
做到这里之后,最自然的下一步就是问:既然访问已经稳定命中了 err.stack,能不能像 30517 一样,继续围绕 err 的对象布局做文章。
这里我做过的第一轮尝试就是沿这个方向展开的。但很快会发现这条路不顺。30517 的关键是专用 handler 在错误对象布局上按固定偏移读取,于是可以把“目标对象的字段”替换成“另一种对象在同偏移上的可控数据”。13224 这里没有这样的固定偏移错读。err 只负责把访问送进 accessor 路径,本身并没有变成一个被错误解释的承载体。
换句话说,err 在这条链里的角色更接近入口,而不是工作区。
评估 primitive receiver 能不能继续长成 fakeobj / addrof¶
既然 err 这条路走不通,下一步自然会回头看 receiver。本地尝试过的第二个方向,是把 primitive receiver 当成真正的可控输入,去评估能不能把这条 confusion 继续推成更强原语。
这个方向最后也没有继续走通。原因并不复杂:receiver 最终是 Smi,而 Smi 不是堆对象。它没有 map、properties、elements,没有对象头,也没有可布置的 backing store。30517 那套 fakeobj / addrof 能做起来,是因为错误路径最后仍然落在堆对象或其可控数据区上;13224 这里传下去的是一个根本不是对象的值,后果优先表现为非法调用,而不是稳定返回一段错误数据。
因此这条链虽然也是 type confusion,但并不能自然翻译成“把某段内存解释成对象”这一类常见利用模式。
3224 虽然被归类为 high severity 的 V8 type confusion,但它的 root cause 形态决定了它比 30517 更难直接发展成稳定利用链。
小结¶
CVE-2025-13224 的核心不在对象布局错位,而在 receiver 语义脱节。LoadSuperIC 前半段允许从合法 lookup_start_object 上命中 accessor,后半段却没有继续保证实际 receiver 仍满足 JSReceiver 约束,于是 primitive / Smi 被直接送进 CallGetterIfAccessor。
从 root cause 角度看,这个洞体现的是 super 访问里“查找对象”和“调用对象”分离之后,receiver 约束没有在后续 fast path 中被一致维护。从利用角度看,它虽然仍然属于 type confusion,但更接近一次调用边界上的类型假设失效,而不是一条能自然扩展成固定偏移读写的内存原语。这也是难利用的根本原因。