【技术分享】如何利用JavaScript数组扩展中的整型溢出漏洞(WebKit)

http://p3.qhimg.com/t01049e9b0a3400118b.jpg

翻译:WisFree

预估稿费:120RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿

写在前面的话

我将在这篇文章中给大家介绍有关漏洞CVE-2017-2536/ZDI-17-358,这是一个典型的整形溢出漏洞,当系统在计算分配空间的尺寸大小时,该漏洞将有可能导致堆缓冲区溢出。我们不仅给大家提供了一个“短小精悍”的漏洞PoC,而且我们还设计出了针对Safari 10.1的完整的漏洞利用方案,所以一切将会非常的有趣!

注:该功能原本是为了让JavaScriptCore能够更好地处理ECMAScript 6的扩展操作而设计的,但saelo在今年二月份发现了其中存在的安全问题。【参考资料


漏洞分析

下面就是JavaScript在进行数组扩展操作时构建新数组所要用到的代码:

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并不会执行任何的边界检测,为了印证这一点,请大家先看看下面这段代码:

/* ... */
case ALL_CONTIGUOUS_INDEXING_TYPES: {
    ASSERT(i < butterfly->publicLength());
    ASSERT(i < butterfly->vectorLength());
    butterfly->contiguous()[i].set(vm, this, v);
    break;
}
/* ... */

如果数据发生越界,那么此时便会出现堆缓冲区溢出的问题。这个漏洞可以通过下面这段脚本代码来触发:

var a = new Array(0x7fffffff);
var x = [13, 37, ...a, ...a];

此时,一个长度为0的JSArray对象将会被创建,然后再向这个JSArray对象中拷贝了2^32个元素进去,我们的浏览器可不想看到这样的事情发生。

当然了,想要解决这个问题也并不难。我们只需要添加一个针对整形溢出问题的检测函数久可以修复这个问题了。【漏洞修复方案


漏洞利用

虽然上面给出的PoC代码会多次使用一个数组,但JavaScriptCore仍然会在每一次的数组扩展操作中分配一个JSFixedArray对象(查看标注[[2]]下面的代码)。这样一来,系统将会分配大约四十亿个JSValues对象,这些JSValues对象将会占用大约32GiB的RAM空间。不过幸运的是,由于macOS内核引入了页面压缩功能,因此这个问题不会对macOS

平台产生较大的影响,但是对于其他平台来说,攻击者可以在一分钟左右的时间里成功触发这个漏洞,因此它的影响还是比较严重的。

我们现在剩下要做的就是通过执行Heep Feng-Shui来替换堆内存中的某些数据,因为我们要尝试触发堆内存溢出。我们可以使用下面这些堆喷射技术来利用这个漏洞:

1.分配一百个大小为0x40000的JSArrays对象,然后保存它们的引用。这将多次触发GC并对堆内存进行填充。

2.分配一百个大小为0x40000的JSArrays对象,然后只保存每一个对象的下一个地址,这样将会触发GC并在堆内存空间中留下大小为0x40000的空白区域。

3.分配一个较大的JSArray对象,并分配一个大小相同的ArrayBuffer。

4.使用JSArryas分配4GiB的填充空间。

5.通过连接这些JSArrays对象来触发漏洞,连接后的大小为2^32+0x40000(包含重复字节0x41)

目标缓冲区(A)将会被分配到第二步的喷射区域中,此时第三步的缓冲区(B)空间将会被覆盖。这样会将目标数组的大小增加至喷射值(0x 4141414141414141),所以这部分内容将会部分覆盖ArrayBuffer。最后一步会立刻生成fakeobj和addrof原语(这部分在JavaScriptCore phrack paper的1.2章节中有过介绍),它们可以用来向JIT页面中写入代码,并跳转到目标页面。

在我们所设计出的漏洞利用技术中,我们在一个单独的Web Worker中执行了上面的第五步,因此我们可以在目标数组被重写之后立刻启动一个Second-Stage Shellcode。这样一来,我们就不用等待覆盖写入操作全部完成了,我们可以保证堆内存处于被破坏的状态,而GC(垃圾回收器)也不会发生崩溃。

完整的漏洞利用代码可以在我们的GitHub仓库中找到。【传送门

(完)