利用数组展开中的整数溢出漏洞攻破WebKit

本文详细分析了CVE-2017-2536漏洞,这是一个在WebKit的JavaScriptCore中由数组展开操作引发的整数溢出漏洞,导致堆缓冲区溢出。文章涵盖了漏洞原理、触发方式及完整的利用过程,包括堆布局和最终实现代码执行的技术细节。

利用数组展开中的整数溢出漏洞攻破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内核执行的页面压缩,这不是什么大问题。然而,触发该漏洞大约需要一分钟。

现在剩下的工作是执行一些堆风水,将一些有趣的东西放在堆上,然后溢出到其中。我们使用以下堆喷来利用该漏洞:

  1. 分配100个大小为0x40000的JSArray并固定它们(即保持引用)。这将多次触发GC并填充堆中的空洞。
  2. 分配100个大小为0x40000的JSArray,其中只有每隔一个被固定。这会触发GC并在堆中留下大小为0x40000的空洞。
  3. 分配一个更大的JSArray和一个相同大小的ArrayBuffer。这些最终位于步骤2的喷之后。
  4. 使用JSArray分配4 GiB的填充。
  5. 通过连接总大小为2^32 + 0x40000(包含重复字节0x41)的JSArray来触发漏洞。

目标缓冲区将在步骤2的喷洒区域中分配,步骤3中的受害者缓冲区将被覆盖。这将受害者数组的大小增加到喷洒的值(0x4141414141414141),使其与受害者ArrayBuffer重叠。

最后几步立即产生JavaScriptCore phrack论文第1.2节中描述的fakeobj和addrof原语,然后可用于将代码写入JIT页面并跳转到该页面。

在我们的漏洞利用中,我们在一个单独的web worker中执行步骤5,以便在受害者数组被覆盖后立即启动第二阶段shellcode。这样,我们不需要等待完整的覆盖完成,并且堆只在非常短的时间内处于损坏状态,因此垃圾回收不会崩溃(从Safari版本10.1开始并发运行)。完整的漏洞利用可以在我们的GitHub上找到。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计