利用Safari信息泄露漏洞的技术分析

本文详细分析了Safari浏览器中JavaScriptCore引擎的一个信息泄露漏洞(CVE-2018-4358),该漏洞源于Array.prototype.concat函数在处理未初始化数组时的缺陷,可能导致堆内存数据泄露。

利用Safari信息泄露漏洞

JavaScript中的数组和类数组对象是一些简单但高效优化的主要目标。核心观察发现,许多数组只包含相同基本类型的元素,如32位整数或双精度浮点数。因此,每个主流引擎都实现了特定优化,以允许对不同类型元素进行快速访问和密集存储。

在WebKit使用的JavaScriptCore引擎中,对象中元素的存储方式由IndexingType值表示,这是一个8位整数,代表一组标志组合。其定义可在IndexingType.h中找到。引擎会经常检查(或生成代码检查)对象的索引类型,并据此决定使用哪个专门的快速路径。一个重要索引类型是ArrayWithUndecided,表示所有元素都是undefined,无需存储实际值。这种情况下,引擎可以保持元素未初始化以提高性能。

现在我们来看ArrayPrototype.cpp中实现Array.prototype.concat的旧版本代码:

 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
EncodedJSValue JSC_HOST_CALL arrayProtoPrivateFuncConcatMemcpy(ExecState* exec)
{
    ...

    unsigned resultSize = checkedResultSize.unsafeGet();
    IndexingType firstType = firstArray->indexingType();
    IndexingType secondType = secondArray->indexingType();
    IndexingType type = firstArray->mergeIndexingTypeForCopying(secondType); // [[ 1 ]]
    if (type == NonArray || !firstArray->canFastCopy(vm, secondArray) || resultSize >= MIN_SPARSE_ARRAY_INDEX) {
        ...
    }

    JSGlobalObject* lexicalGlobalObject = exec->lexicalGlobalObject();
    Structure* resultStructure = lexicalGlobalObject->arrayStructureForIndexingTypeDuringAllocation(type);
    if (UNLIKELY(hasAnyArrayStorage(resultStructure->indexingType())))
        return JSValue::encode(jsNull());

    ASSERT(!lexicalGlobalObject->isHavingABadTime());
    ObjectInitializationScope initializationScope(vm);
    JSArray* result = JSArray::tryCreateUninitializedRestricted(initializationScope, resultStructure, resultSize);
    if (UNLIKELY(!result)) {
        throwOutOfMemoryError(exec, scope);
        return encodedJSValue();
    }

    if (type == ArrayWithDouble) {
        [[ 2 ]]
        double* buffer = result->butterfly()->contiguousDouble().data();
        memcpy(buffer, firstButterfly->contiguousDouble().data(), sizeof(JSValue) * firstArraySize);
        memcpy(buffer + firstArraySize, secondButterfly->contiguousDouble().data(), sizeof(JSValue) * secondArraySize);
    } else if (type != ArrayWithUndecided) {
    ...

该函数在[[ 1 ]]处确定结果数组的索引类型,我们可以看到如果索引类型是ArrayWithDouble,它将在[[ 2 ]]处采用快速路径。现在我们来看mergeIndexingTypeForCopying的实现,它负责在调用Array.prototype.concat时确定结果数组的索引类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
inline IndexingType JSArray::mergeIndexingTypeForCopying(IndexingType other)
{
    IndexingType type = indexingType();
    if (!(type & IsArray && other & IsArray))
        return NonArray;

    if (hasAnyArrayStorage(type) || hasAnyArrayStorage(other))
        return NonArray;

    if (type == ArrayWithUndecided)
        return other; [[ 3 ]]
    ...

我们可以看到,当一个输入数组的索引类型为ArrayWithUndecided时,结果索引类型将是另一个数组的索引类型。因此,如果我们调用Array.prototype.concat,其中一个数组的索引类型为ArrayWithUndecided,另一个为ArrayWithDouble,我们最终会在[[ 2 ]]处采用快速路径,通过创建两个数组支持元素的原始副本来连接这两个数组。

这段代码没有确保在调用memcpy之前正确初始化两个butterfly。这意味着如果我们能找到一条代码路径,让我们创建一个未初始化的数组传递给Array.prototype.concat,我们将得到一个包含堆中未初始化值但索引类型不是ArrayWithUndecided的数组。从某种意义上说,这个原语与lokihardt在2017年报告的旧漏洞导致的结果相反。

创建这样一个数组的一种方法是使用NewArrayWithSize DFG JIT操作码(尽管也可以使用纯标准库调用来实现,如另一个利用所示,完全不依赖JIT编译)。查看FTLLowerDFGToB3.cpp中该操作码的FTL实现allocateJSArray,我们可以看到数组将保持未初始化状态。它不必初始化数组,因为索引类型是ArrayWithUndecided。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
ArrayValues allocateJSArray(LValue publicLength, LValue vectorLength, LValue structure, LValue indexingType, bool shouldInitializeElements = true, bool shouldLargeArraySizeCreateArrayStorage = true)
{
    [ ... ]
    initializeArrayElements(
	indexingType,
	shouldInitializeElements ? m_out.int32Zero : publicLength, vectorLength,
	butterfly);

...
void initializeArrayElements(LValue indexingType, LValue begin, LValue end, LValue butterfly)
{
    if (begin == end)
        return;

    if (indexingType->hasInt32()) {
        IndexingType rawIndexingType = static_cast<IndexingType>(indexingType->asInt32());
        if (hasUndecided(rawIndexingType))
            return;  // [[ 4 ]]

因此,当表达式new Array(n)被FTL JIT编译时,将命中[[ 4 ]]并返回一个索引类型为ArrayWithUndecided且元素未初始化的数组。

漏洞利用

根据前面的分析,触发这个漏洞并不难:我们重复调用一个函数,该函数使用new Array()创建一个数组,然后在该数组和一个仅包含双精度数的数组上调用concat。我们调用该函数足够多次,以便它被FTL编译器编译。

该利用程序利用此漏洞泄露目标对象的地址。它通过用我们的对象和标量对象喷洒内存来工作。然后我们触发漏洞并检查返回的数组以找到我们对象的地址。

结论

此漏洞已修复,并在iOS 12和macOS Mojave发布时推出了修复版的Safari。它被分配了CVE-2018-4358,并在提交b68b373dcbfbc68682ceeca8292c5c0051472071中修复。

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