利用Safari信息泄露漏洞:从ArrayWithUndecided到堆数据读取

本文详细分析了Safari中JavaScriptCore引擎的Array.prototype.concat实现漏洞,该漏洞允许通过未初始化数组读取堆内存数据,最终导致信息泄露。漏洞已修复,编号CVE-2018-4358。

利用Safari信息泄露漏洞

2018年9月26日 • 作者:bkth

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

在WebKit使用的JavaScript引擎JavaScriptCore中,元素在对象中的存储方式表示为IndexingType值,这是一个8位整数,表示标志的组合。定义可以在IndexingType.h中找到。

在整个代码中,引擎通常会检查(或发出代码检查)对象的索引类型,并基于此决定使用几个专用快速路径中的哪一个。一个重要的索引类型是ArrayWithUndecided,它表示所有元素都是undefined,并且不需要存储实际值。在这种情况下,引擎可以保持元素未初始化以提高性能。

现在让我们看一下实现Array.prototype.concat的旧版本代码,该代码位于ArrayPrototype.cpp中:

 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
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
13
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时,结果索引类型将是另一个数组的索引类型。因此,如果我们使用一个索引类型为ArrayWithUndecided的数组和另一个索引类型为ArrayWithDouble的数组调用Array.prototype.concat,我们最终将在[[ 2 ]]处采用快速路径,该路径通过创建支持两个数组的元素的原始副本来连接两个数组。

创建这样一个数组的一种方法是使用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
19
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编译器编译。

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

结论

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

显示Disqus评论

github

twitter

@

email

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