CVE-2024-0517(还未复现)
看了一段时间了,就是利用不了,一是很不稳定,二是可能理解不够到位,后续还会看几天,要是一个月都打不通,就找现成的exp调试学习了。
TryBuildFindNonDefaultConstructorOrConstruct函数分析
函数通过遍历原型链寻找合适的构造函数,生成高效的对象创建代码,同时处理优化依赖和约束条件,遇到不支持或动态特性时回退。
函数运行流程分析:
1. 常量检查与初始化
-
输入参数:
this_function(当前函数对象)、new_target(new操作的目标构造函数)、result(结果寄存器对)。 -
步骤:
- 尝试将
this_function常量折叠(TryGetConstant),若失败则返回false(无法优化)。 - 获取
this_function的 Map(对象结构描述),并从其原型开始遍历。
- 尝试将
2. 原型链遍历
-
循环条件:
while (true),持续向上遍历原型链。 -
关键检查:
- 非 JSFunction:若当前原型不是
JSFunction,返回false(无法优化)。 - 类字段检查:若构造函数需要实例成员初始化(如类字段),或存在私有方法(通过
ClassScopeHasPrivateBrand检测),则放弃优化(因未实现相关处理)。
- 非 JSFunction:若当前原型不是
3. 构造函数类型判断
-
函数类型(
FunctionKind)决定行为:-
kDefaultDerivedConstructor(默认派生构造函数):
- 继续向上遍历原型链(
current = current_function.map().prototype())。
- 继续向上遍历原型链(
-
kDefaultBaseConstructor(默认基类构造函数)或 其他类型:
-
依赖保护:确保
ArrayIteratorProtector未被破坏(避免因运行时数组迭代行为变化导致优化失效)。 -
生成对象构造代码:
-
存储标志位(
result.first):标记是否找到基类构造函数(true/false)。 -
对象创建:
- 条件优化:若
new_target是有效JSFunction且初始 Map 合法(HasValidInitialMap),直接调用BuildAllocateFastObject生成快速对象。 - 回退路径:否则调用内置函数
Builtin::kFastNewObject创建对象。
- 条件优化:若
-
-
-
4. 结果存储与依赖管理
-
存储结果:
- 若为基类构造函数,将
true和生成的对象分别存入result.first和result.second。 - 若为其他类型,将
false和当前构造函数存入结果寄存器。
- 若为基类构造函数,将
-
稳定原型链依赖:
- 调用
DependOnStablePrototypeChain,确保原型链在后续执行中不会变化,否则触发 反优化(Deoptimization)。
- 调用
patch增加的ClearCurrentRawAllocation() 的作用
当调用 BuildAllocateFastObject 完成快速对象分配后,ClearCurrentRawAllocation() 被调用以 清除当前上下文的临时内存分配状态。其核心目的是:
-
防止状态残留:
- 确保后续操作不会意外引用已完成的分配状态(例如错误复用已分配的临时内存)。
-
内存管理:
- 释放或标记当前分配为已完成,避免内存泄漏(尤其在存在多个嵌套分配时)。
-
优化安全性:
- 确保编译器生成的 IR 节点与实际内存状态严格一致,避免因残留状态导致错误的优化假设。
bool MaglevGraphBuilder::TryBuildFindNonDefaultConstructorOrConstruct(
ValueNode* this_function, ValueNode* new_target,
std::pair<interpreter::Register, interpreter::Register> result) {
// See also:
// JSNativeContextSpecialization::ReduceJSFindNonDefaultConstructorOrConstruct
compiler::OptionalHeapObjectRef maybe_constant =
TryGetConstant(this_function);
if (!maybe_constant) return false;
compiler::MapRef function_map = maybe_constant->map(broker());
compiler::HeapObjectRef current = function_map.prototype(broker());
// TODO(v8:13091): Don't produce incomplete stack traces when debug is active.
// We already deopt when a breakpoint is set. But it would be even nicer to
// avoid producting incomplete stack traces when when debug is active, even if
// there are no breakpoints - then a user inspecting stack traces via Dev
// Tools would always see the full stack trace.
while (true) {
if (!current.IsJSFunction()) return false;
compiler::JSFunctionRef current_function = current.AsJSFunction();
// If there are class fields, bail out. TODO(v8:13091): Handle them here.
if (current_function.shared(broker())
.requires_instance_members_initializer()) {
return false;
}
// If there are private methods, bail out. TODO(v8:13091): Handle them here.
if (current_function.context(broker())
.scope_info(broker())
.ClassScopeHasPrivateBrand()) {
return false;
}
FunctionKind kind = current_function.shared(broker()).kind();
if (kind != FunctionKind::kDefaultDerivedConstructor) {
// The hierarchy walk will end here; this is the last change to bail out
// before creating new nodes.
if (!broker()->dependencies()->DependOnArrayIteratorProtector()) {
return false;
}
compiler::OptionalHeapObjectRef new_target_function =
TryGetConstant(new_target);
if (kind == FunctionKind::kDefaultBaseConstructor) {
// Store the result register first, so that a lazy deopt in
// `FastNewObject` writes `true` to this register.
StoreRegister(result.first, GetBooleanConstant(true));
ValueNode* object;
if (new_target_function && new_target_function->IsJSFunction() &&
HasValidInitialMap(new_target_function->AsJSFunction(),
current_function)) {
object = BuildAllocateFastObject(
FastObject(new_target_function->AsJSFunction(), zone(), broker()),
AllocationType::kYoung);
// patch
ClearCurrentRawAllocation();
// patch
} else {
object = BuildCallBuiltin<Builtin::kFastNewObject>(
{GetConstant(current_function), new_target});
// We've already stored "true" into result.first, so a deopt here just
// has to store result.second. Also mark result.first as being used,
// since the lazy deopt frame won't have marked it since it used to be
// a result register.
current_interpreter_frame_.get(result.first)->add_use();
object->lazy_deopt_info()->UpdateResultLocation(result.second, 1);
}
StoreRegister(result.second, object);
} else {
StoreRegister(result.first, GetBooleanConstant(false));
StoreRegister(result.second, GetConstant(current));
}
broker()->dependencies()->DependOnStablePrototypeChain(
function_map, WhereToStart::kStartAtReceiver, current_function);
return true;
}
// Keep walking up the class tree.
current = current_function.map(broker()).prototype(broker());
}
}
漏洞分析
1. 漏洞根源
Maglev 优化编译器在处理类构造函数时,未能正确验证 new.target 的值,导致 Type Confusion 和内存越界写入。
2. 漏洞原理
(1) Maglev 的 new.target 优化漏洞
-
Maglev 的常量假设: Maglev 在优化代码时,假设
new.target在构造函数中是 运行时常量(即始终指向当前类)。-
示例代码:
class ClassBug extends ClassParent { constructor() { const v24 = new new.target(); // Maglev 认为 new.target 恒为 ClassBug super(); // 实际可能被覆盖为 ClassParent } }
-
-
漏洞触发条件: 通过
Reflect.construct显式指定new.target为父类(ClassParent),导致 Maglev 的优化假设失效:Reflect.construct(ClassBug, [], ClassParent); // new.target = ClassParent
(2) 对象分配与内存折叠
-
对象分配流程:
- 子类
ClassBug的构造函数调用super()时,V8 需要分配父类实例。 - Maglev 优化时错误地将父类实例的大小与子类实例的大小合并(内存折叠),导致后续操作越界。
- 子类
-
内存折叠示意图:
// 优化前: Allocate(ClassBug) → Allocate(ClassParent) → 初始化属性 // 优化后(漏洞): Allocate(ClassBug + ClassParent) → 越界写入父类属性
(3) 越界写入的根源
-
Maglev 生成错误的代码: 在优化后的代码中,Maglev 会生成类似以下操作:
69 f8 f7 01 02 Construct r2, r3-r3, [2] // 分配内存时错误计算大小- 错误计算: 由于
new.target被错误假设为ClassBug,Maglev 在分配内存时未预留足够的空间,导致后续写入父类属性时越界。
- 错误计算: 由于
3. 利用原理
(1) 内存布局控制
-
触发优化: 通过多次调用
Reflect.construct(ClassBug, [], ClassParent),迫使 Maglev 生成优化后的代码。for (let i = 0; i < 300; i++) { Reflect.construct(ClassBug, [], ClassParent); // 累积优化触发阈值 } -
内存污染: 在构造函数中,通过
new new.target()和super()的组合,触发越界写入:class ClassBug extends ClassParent { constructor() { const v24 = new new.target(); // 实际分配 ClassParent super(); // 尝试写入 ClassBug 的属性到父类内存区域 } }
(2) 劫持 ArrayBuffer 的 backing store
-
分配大内存: 创建一个超大的
ArrayBuffer(如 4GB),迫使 V8 分配连续内存区域:let buffer = new ArrayBuffer(0x100000000); // 4GB 分配 -
越界写入指针: 利用漏洞覆盖
ArrayBuffer的backing store指针,指向可写可执行内存区域(如堆或代码页):let view = new BigUint64Array(buffer); view[0] = 0x7ffff7dd0000n; // 覆盖为可执行内存地址
(3) Shellcode 注入与执行
-
写入 Shellcode: 通过
BigUint64Array将机器码写入篡改后的内存区域:const shellcode = [ 0x48, 0x31, 0xc0, // xor rax, rax 0x48, 0x31, 0xff, // xor rdi, rdi 0x48, 0xc7, 0xc7, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, 0x68, 0x00, // mov rdi, '/bin/sh\x00' 0x48, 0x89, 0xe7, // mov rdi, rsp 0x48, 0x31, 0xf6, // xor rsi, rsi 0x48, 0x31, 0xd2, // xor rdx, rdx 0xb0, 0x3b, // mov al, 0x3b (execve syscall) 0x0f, 0x05, // syscall ]; view.set(shellcode); // 写入 Shellcode -
触发代码执行: 通过调用
ArrayBufferDetach或其他函数触发对篡改内存的访问:const shell = new Function('return %ArrayBufferDetach(buffer)'); shell(); // 跳转到 Shellcode 地址
4. 关键漏洞利用步骤
| 步骤 | 操作 | V8 内部行为 |
|---|---|---|
| 1 | 多次调用 Reflect.construct | 触发 Maglev 优化,错误假设 new.target 为常量 |
| 2 | 越界写入父类属性 | 破坏 ArrayBuffer 的 backing store 指针 |
| 3 | 分配大内存 | 确保 ArrayBuffer 落在可预测的内存区域 |
| 4 | 覆盖指针 | 将 backing store 指向可执行内存(如堆) |
| 5 | 注入 Shellcode | 通过 TypedArray 写入机器码 |
| 6 | 触发执行 | 调用 ArrayBufferDetach 或其他函数跳转到 Shellcode |
5. 防御绕过技术
- 绕过 ASLR: 通过
error对象或WebAssembly泄露内存地址,动态定位backing store基址。 - 绕过 DEP: 覆盖
W^X标记,将目标内存页标记为可执行(需利用其他漏洞或 V8 的内存管理缺陷)。 - 绕过 CFI: 使用合法的间接调用目标(如
ArrayBufferDetach的虚表条目)跳转到 Shellcode。
| 漏洞层级 | 根本原因 | 利用方法 |
|---|---|---|
| Maglev 优化 | 未验证 new.target 的动态性 | 通过 Reflect.construct 强制改变 new.target |
| 内存管理 | 对象分配与属性写入的折叠错误 | 越界写入覆盖 ArrayBuffer 的 backing store |
| 控制流劫持 | 可写可执行内存区域的存在 | 注入 Shellcode 并触发执行 |
后续成功复现了应该还会更新,吧。
(2025/3/27新增)
WASM特性
1. WASM内存基础
-
RWX内存段: WASM模块的代码段(存储编译后的WASM指令)在内存中通常具有 可读(R)、可写(W)、可执行(X) 权限。这是为了支持动态生成或修改代码(如JIT编译)。
- 在V8引擎中,WASM代码会被编译为机器码并存储在RWX内存页中。
- 这种设计本身是性能优化的结果,但也可能被攻击者利用。
-
WASM实例结构: 在JavaScript中,
WebAssembly.Instance对象(即wasmInstance)内部维护了对WASM内存和代码段的引用。- V8引擎会将WASM实例的元数据(如内存基址、代码段地址等)存储在堆内存中。
- 通过内存布局分析,可以找到指向RWX内存段的指针(通常位于
wasmInstance对象的固定偏移处)。
2. 利用RWX内存执行Shellcode
攻击步骤:
-
定位RWX内存地址:
- 通过JavaScript访问
wasmInstance对象的内存布局,计算RWX段的起始地址。 - 不同V8版本的偏移量(offset)可能不同,需根据具体版本逆向分析。
- 通过JavaScript访问
-
覆盖WASM代码段:
- 使用
ArrayBuffer或TypedArray直接操作内存,将恶意Shellcode写入RWX段。
- 使用
-
触发代码执行:
- 调用WASM导出的函数(如
instance.exports.main()),此时控制流会跳转到被覆盖的RWX内存区域,执行Shellcode。 - Shellcode可以是任意机器码(如反弹Shell、提权代码等)。
- 调用WASM导出的函数(如
3. 关键技术细节
V8内存布局分析:
-
wasmInstance对象的结构: V8使用C++内部类(如WasmInstanceObject)管理WASM实例。其内存布局包含指向WasmCodeManager和Memory对象的指针。- 通过调试或逆向(如使用
d8调试器),可以找到wasmInstance + OFFSET处的rwx_address字段。
- 通过调试或逆向(如使用
-
绕过内存保护:
- CSP(内容安全策略) :WASM的执行不受CSP限制,因此即使页面设置了CSP,仍可通过WASM绕过。
- 堆地址随机化(ASLR) :需通过信息泄露(如
ArrayBuffer越界读)获取基址,再计算偏移。
版本差异与稳定性:
- 偏移量变化: V8的代码会因版本更新改变内部结构,导致
OFFSET值变化。攻击者需针对目标V8版本动态计算偏移(如通过%DebugPrint()或内存扫描)。 - RWX段的生命周期: 现代V8已逐步限制WASM代码的RWX权限(如仅在初始化时RWX,后续降级为RX),需结合其他漏洞(如UAF)维持可写权限。