利用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中修复。