CVE-2024-0517复现

132 阅读9分钟

CVE-2024-0517(还未复现)

看了一段时间了,就是利用不了,一是很不稳定,二是可能理解不够到位,后续还会看几天,要是一个月都打不通,就找现成的exp调试学习了。

TryBuildFindNonDefaultConstructorOrConstruct函数分析

函数通过遍历原型链寻找合适的构造函数,生成高效的对象创建代码,同时处理优化依赖和约束条件,遇到不支持或动态特性时回退。

函数运行流程分析
1. 常量检查与初始化
  • 输入参数this_function(当前函数对象)、new_targetnew操作的目标构造函数)、result(结果寄存器对)。

  • 步骤

    • 尝试将 this_function 常量折叠TryGetConstant),若失败则返回 false(无法优化)。
    • 获取 this_functionMap(对象结构描述),并从其原型开始遍历。
2. 原型链遍历
  • 循环条件while (true),持续向上遍历原型链。

  • 关键检查

    • 非 JSFunction:若当前原型不是 JSFunction,返回 false(无法优化)。
    • 类字段检查:若构造函数需要实例成员初始化(如类字段),或存在私有方法(通过 ClassScopeHasPrivateBrand 检测),则放弃优化(因未实现相关处理)。
3. 构造函数类型判断
  • 函数类型FunctionKind)决定行为:

    • kDefaultDerivedConstructor(默认派生构造函数):

      • 继续向上遍历原型链(current = current_function.map().prototype())。
    • kDefaultBaseConstructor(默认基类构造函数)或 其他类型

      • 依赖保护:确保 ArrayIteratorProtector 未被破坏(避免因运行时数组迭代行为变化导致优化失效)。

      • 生成对象构造代码

        1. 存储标志位result.first):标记是否找到基类构造函数(true/false)。

        2. 对象创建

          • 条件优化:若 new_target 是有效 JSFunction 且初始 Map 合法(HasValidInitialMap),直接调用 BuildAllocateFastObject 生成快速对象。
          • 回退路径:否则调用内置函数 Builtin::kFastNewObject 创建对象。
4. 结果存储与依赖管理
  • 存储结果

    • 若为基类构造函数,将 true 和生成的对象分别存入 result.firstresult.second
    • 若为其他类型,将 false 和当前构造函数存入结果寄存器。
  • 稳定原型链依赖

    • 调用 DependOnStablePrototypeChain,确保原型链在后续执行中不会变化,否则触发 反优化(Deoptimization)。
patch增加的ClearCurrentRawAllocation() 的作用

当调用 BuildAllocateFastObject 完成快速对象分配后,ClearCurrentRawAllocation() 被调用以 清除当前上下文的临时内存分配状态。其核心目的是:

  1. 防止状态残留

    • 确保后续操作不会意外引用已完成的分配状态(例如错误复用已分配的临时内存)。
  2. 内存管理

    • 释放或标记当前分配为已完成,避免内存泄漏(尤其在存在多个嵌套分配时)。
  3. 优化安全性

    • 确保编译器生成的 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) 内存布局控制
  1. 触发优化: 通过多次调用 Reflect.construct(ClassBug, [], ClassParent),迫使 Maglev 生成优化后的代码。

    for (let i = 0; i < 300; i++) {
      Reflect.construct(ClassBug, [], ClassParent); // 累积优化触发阈值
    }
    
  2. 内存污染: 在构造函数中,通过 new new.target()super() 的组合,触发越界写入:

    class ClassBug extends ClassParent {
      constructor() {
        const v24 = new new.target(); // 实际分配 ClassParent
        super(); // 尝试写入 ClassBug 的属性到父类内存区域
      }
    }
    
(2) 劫持 ArrayBuffer 的 backing store
  1. 分配大内存: 创建一个超大的 ArrayBuffer(如 4GB),迫使 V8 分配连续内存区域:

    let buffer = new ArrayBuffer(0x100000000); // 4GB 分配
    
  2. 越界写入指针: 利用漏洞覆盖 ArrayBufferbacking store 指针,指向可写可执行内存区域(如堆或代码页):

    let view = new BigUint64Array(buffer);
    view[0] = 0x7ffff7dd0000n; // 覆盖为可执行内存地址
    
(3) Shellcode 注入与执行
  1. 写入 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
    
  2. 触发代码执行: 通过调用 ArrayBufferDetach 或其他函数触发对篡改内存的访问:

    const shell = new Function('return %ArrayBufferDetach(buffer)');
    shell(); // 跳转到 Shellcode 地址
    
4. 关键漏洞利用步骤
步骤操作V8 内部行为
1多次调用 Reflect.construct触发 Maglev 优化,错误假设 new.target 为常量
2越界写入父类属性破坏 ArrayBufferbacking 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
内存管理对象分配与属性写入的折叠错误越界写入覆盖 ArrayBufferbacking 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
攻击步骤
  1. 定位RWX内存地址

    • 通过JavaScript访问wasmInstance对象的内存布局,计算RWX段的起始地址。
    • 不同V8版本的偏移量(offset)可能不同,需根据具体版本逆向分析。
  2. 覆盖WASM代码段

    • 使用ArrayBufferTypedArray直接操作内存,将恶意Shellcode写入RWX段。
  3. 触发代码执行

    • 调用WASM导出的函数(如instance.exports.main()),此时控制流会跳转到被覆盖的RWX内存区域,执行Shellcode。
    • Shellcode可以是任意机器码(如反弹Shell、提权代码等)。
3. 关键技术细节
V8内存布局分析
  • wasmInstance对象的结构: V8使用C++内部类(如WasmInstanceObject)管理WASM实例。其内存布局包含指向WasmCodeManagerMemory对象的指针。

    • 通过调试或逆向(如使用d8调试器),可以找到wasmInstance + OFFSET处的rwx_address字段。
  • 绕过内存保护

    • CSP(内容安全策略) :WASM的执行不受CSP限制,因此即使页面设置了CSP,仍可通过WASM绕过。
    • 堆地址随机化(ASLR) :需通过信息泄露(如ArrayBuffer越界读)获取基址,再计算偏移。
版本差异与稳定性
  • 偏移量变化: V8的代码会因版本更新改变内部结构,导致OFFSET值变化。攻击者需针对目标V8版本动态计算偏移(如通过%DebugPrint()或内存扫描)。
  • RWX段的生命周期: 现代V8已逐步限制WASM代码的RWX权限(如仅在初始化时RWX,后续降级为RX),需结合其他漏洞(如UAF)维持可写权限。