利用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