利用数组展开中的整数溢出漏洞攻破WebKit
2017年6月2日
作者:saelo, niklasb
本文涉及CVE-2017-2536 / ZDI-17-358,这是一个经典的在计算分配大小时发生的整数溢出漏洞,导致基于堆的缓冲区溢出。该漏洞在提交99ed479中被引入,该提交改进了JavaScriptCore处理ECMAScript 6展开操作的方式,并由saelo在2月发现。PoC代码短到可以放入一条推文,并且我们有一个针对Safari 10.1的完整可用漏洞利用,所以这将非常有趣!
漏洞详情
以下代码用于通过展开操作构建数组:
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
|
SLOW_PATH_DECL(slow_path_new_array_with_spread)
{
BEGIN();
int numItems = pc[3].u.operand;
ASSERT(numItems >= 0);
const BitVector& bitVector = exec->codeBlock()->unlinkedCodeBlock()->bitVector(pc[4].u.unsignedValue);
JSValue* values = bitwise_cast<JSValue*>(&OP(2));
// [[ 1 ]]
unsigned arraySize = 0;
for (int i = 0; i < numItems; i++) {
if (bitVector.get(i)) {
JSValue value = values[-i];
JSFixedArray* array = jsCast<JSFixedArray*>(value);
arraySize += array->size();
} else
arraySize += 1;
}
JSGlobalObject* globalObject = exec->lexicalGlobalObject();
Structure* structure = globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous);
JSArray* result = JSArray::tryCreateForInitializationPrivate(vm, structure, arraySize);
CHECK_EXCEPTION();
// [[ 2 ]]
unsigned index = 0;
for (int i = 0; i < numItems; i++) {
JSValue value = values[-i];
if (bitVector.get(i)) {
// We are spreading.
JSFixedArray* array = jsCast<JSFixedArray*>(value);
for (unsigned i = 0; i < array->size(); i++) {
RELEASE_ASSERT(array->get(i));
result->initializeIndex(vm, index, array->get(i));
++index;
}
} else {
// We are not spreading.
result->initializeIndex(vm, index, value);
++index;
}
}
RETURN(result);
}
|
在[[ 1 ]]处,函数计算输出数组的大小,然后在[[ 2 ]]处分配并初始化该数组。然而,大小计算可能溢出,导致分配更小的数组。JSObject::initializeIndex
不执行任何边界检查,如下代码所示:
1
2
3
4
5
6
7
8
9
10
|
/* ... */
case ALL_CONTIGUOUS_INDEXING_TYPES: {
ASSERT(i < butterfly->publicLength());
ASSERT(i < butterfly->vectorLength());
butterfly->contiguous()[i].set(vm, this, v);
break;
}
/* ... */
|
因此,发生堆缓冲区溢出。可以通过以下脚本触发该漏洞:
1
2
|
var a = new Array(0x7fffffff);
var x = [13, 37, ...a, ...a];
|
分配了一个大小为0的JSArray,然后将2^32个元素复制到其中,浏览器对此非常不喜欢。
该漏洞的补丁只是向所有受影响的层(解释器+JIT)添加了整数溢出检查。
漏洞利用
尽管上面给出的PoC代码多次使用单个数组,但JavaScriptCore会为数组字面量的每个展开操作数分配一个JSFixedArray(在slow_path_spread中)。因此,大约需要分配40亿个JSValue,占用32 GiB的RAM。幸运的是,由于macOS内核执行的页面压缩,这不是什么大问题。然而,触发该漏洞大约需要一分钟。
现在剩下的工作是执行一些堆风水,将一些有趣的东西放在堆上,然后溢出到其中。我们使用以下堆喷来利用该漏洞:
- 分配100个大小为0x40000的JSArray并固定它们(即保持引用)。这将多次触发GC并填充堆中的空洞。
- 分配100个大小为0x40000的JSArray,其中只有每隔一个被固定。这会触发GC并在堆中留下大小为0x40000的空洞。
- 分配一个更大的JSArray和一个相同大小的ArrayBuffer。这些最终位于步骤2的喷之后。
- 使用JSArray分配4 GiB的填充。
- 通过连接总大小为2^32 + 0x40000(包含重复字节0x41)的JSArray来触发漏洞。
目标缓冲区将在步骤2的喷洒区域中分配,步骤3中的受害者缓冲区将被覆盖。这将受害者数组的大小增加到喷洒的值(0x4141414141414141),使其与受害者ArrayBuffer重叠。
最后几步立即产生JavaScriptCore phrack论文第1.2节中描述的fakeobj和addrof原语,然后可用于将代码写入JIT页面并跳转到该页面。
在我们的漏洞利用中,我们在一个单独的web worker中执行步骤5,以便在受害者数组被覆盖后立即启动第二阶段shellcode。这样,我们不需要等待完整的覆盖完成,并且堆只在非常短的时间内处于损坏状态,因此垃圾回收不会崩溃(从Safari版本10.1开始并发运行)。完整的漏洞利用可以在我们的GitHub上找到。