前言
Javascript中的数组和数组对象一直都是编程人员优化的主要目标,一般来说,数组只会包含一些基本类型数据,比如说32位整数或字符等等。因此,每个引擎都会对这些对象进行某些优化,并提升不同元素类型的访问速度和密集型表示。
在JavaScriptCore中,JavaScript引擎是在WebKit中实现的,其中每一个存储在对象中的元素都代表着一个IndexingType值,一个8位整数代表一套Flag组合,具体的参数定义可以在IndexingType.h中找到。接下来,引擎会检测一个对象中indexing的类型,然后决定使用哪一条快速路径,其中最重要的一种indexing类型就是ArrayWithUndecided,它表示的是所有元素均为未定义(undefined),而且没有存储任何实际的值。在这种情况下,引擎为了提升性能,会让这些元素保持未初始化。
分析
下面,我们一起看一看旧版本中实现Array.prototype.concat的代码(ArrayPrototype.cpp):
EncodedJSValueJSC_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());
ObjectInitializationScopeinitializationScope(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]]的indexing类型,我们可以看到,如果indexing类型为ArrayWithDouble,它将会选择[[2]]作为快速路径。接下来,我们看一看:
mergeIndexingTypeForCopying的实现代码,这个函数主要负责在Array.prototype.concat被调用时,判断结果数组的indexing类型:
inlineIndexingType 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 ]]
...
我们可以看到在这种情况下,有一个输入数组的indexing类型为ArrayWithUndecided,结果indexing类型将会是另一个数组的indexing类型。因此,如果我们我们用一个indexing类型为ArrayWithUndecided的数组和另一个indexing类型为ArrayWithDouble的数组去调用Array.prototype.concat方法的话,我们将会按照快速路径[[2]]运行,并将两个数组进行拼接。
这段代码并不能保证这两个“butterfly”(JavaScript引擎攻击技术里的一种概念,详情请参考【这篇文章】)在代码调用memcpy之前能够正确初始化。这也就意味着,如果我们能够找到一条允许我们创建一个未初始化数组并将其传递给Array.prototype.concat的代码路径,那我们就能够在堆内存中拥有一个包含了未初始化值的数组对象了,而且它的indexing类型还不是ArrayWithUndecided。从某种程度上来说,这个安全问题跟lokihardt在2017年报告的一个 旧漏洞有些相似,只不过利用方式不同。
在创建这种数组对象时,可以利用NewArrayWithSize DFG JIT的操作码来实现,在对FTLLowerDFGToB3.cpp中FTL所实现的allocateJSArray操作码进行分析之后,我们可以看到这个数组将会包含未初始化的值。引擎根本不需要对数组进行初始化,因为这个数组的indexing类型为ArrayWithUndecided。
ArrayValuesallocateJSArray(LValue publicLength, LValue vectorLength, LValue structure,LValue indexingType, bool shouldInitializeElements = true, boolshouldLargeArraySizeCreateArrayStorage = true)
{
[ ... ]
initializeArrayElements(
indexingType,
shouldInitializeElements ?m_out.int32Zero : publicLength, vectorLength,
butterfly);
...
voidinitializeArrayElements(LValue indexingType, LValue begin, LValue end, LValuebutterfly)
{
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]],然后返回一个indexing类型为ArrayWithUndecided的数组,其中就包含未初始化的元素。
漏洞利用
清楚了之前所介绍的漏洞原理之后,想必触发这个漏洞也并非难事:我们可以不断重复调用一个使用new Array()方法来创建数组的函数,然后调用concat方法将这个数组和一个只包含double类型数据的数组进行拼接。在调用够足够次数之后,FTL编译器将会对其进行编译。
这份【漏洞利用代码】可以利用这个漏洞来泄漏一个目标对象的内存地址,实现机制是通过我们所创建的对象进行内存喷射,在触发这个漏洞之后,我们就能够从代码所返回的数组中找到目标对象的地址了。
总结
这个漏洞目前已经在iOS 12和macOS Mojave的最新版本(Safari)中修复了,该漏洞的CVE编号为CVE-2018-4358。