从某种程度上来说, 这个安全问题跟 lokihardt 在 2017 年报告的一个 旧漏洞有些相似, 只不过利用方式不同.
前言
JavaScript 中的数组和数组对象一直都是编程人员优化的主要目标, 一般来说, 数组只会包含一些基本类型数据, 比如说 32 位整数或字符等等. 因此, 每个引擎都会对这些对象进行某些优化, 并提升不同元素类型的访问速度和密集型表示.
在 JavaScriptCore 中, JavaScript 引擎是在 webKit 中实现的, 其中每一个存储在对象中的元素都代表着一个 IndexingType 值, 一个 8 位整数代表一套 Flag 组合, 具体的参数定义可以在 IndexingType.h 中找到. 接下来, 引擎会检测一个对象中 indexing 的类型, 然后决定使用哪一条快速路径, 其中最重要的一种 indexing 类型就是 ArrayWithUndecided, 它表示的是所有元素均为未定义(undefined), 而且没有存储任何实际的值. 在这种情况下, 引擎为了提升性能, 会让这些元素保持未初始化.
分析
下面, 我们一起看一看旧版本中实现 Array.prototype.concat 的代码():
- 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->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.
- Visual C# 2005 从入门到精通
- Microsoft Visual C# 功能强大, 使用简单. 本书全面介绍了如何利用 Visual Studio2005 和 NET Framework 来进行 C# 编程. 作者将 C# 的各种特性娓娓...
来源: http://netsecurity.51cto.com/art/201811/586198.htm