CVE-2017-0228 以及它的三次补丁

之前的分析文章中对 CVE-2016-7202 漏洞进行过分析,这个漏洞是由于 Array.prototype.reverse 在处理时对 side-effect 情况控制不严导致的,而这个漏洞前后一共修补过两次。在今年年初的 Pwn2Own 上,来自腾讯的科恩实验室同样使用了 Array.prototype.reverse 中的一个漏洞,更为奇特的是这个漏洞前后一共修复了四次。本文将详细分析这个这个修补了四次的漏洞以及前三次修复的前因后果。

漏洞概述

该漏洞是由于 Array 功能函数在设计时考虑不完全,引起的 UAF

漏洞样本

漏洞样本根据 ChakraCore 的源码补丁自行构造

<script>
var arr = new Array(100)

for (i=0;i<arr.length;i++)
{
    arr[i] = new Array(1,2,3,4,5,6);
}

var b = new Array(2,3,4,5);

arr[1].length = 0x4000;

arr[1][arr[1].length-1] = 0xfff;

arr[1].reverse()

b = arr[1].splice(0x1000,0x4000)

</script>

漏洞分析

该漏洞的成因主要有以下三点。设计者与代码的编写者沟通不到位导致了这个问题

  • InlineSegment
  • Array.prototype.reverse
  • Array.prototype.splice

InlineSegment

js 创建 Array 有几种常见的方法 arr = [1,2,3,4,]arr = new Array(10)arr = new Array(1,2,3,4,5)
当以 new Array 形式创建 Array 时,会调用函数 JavascriptNativeIntArray::NewInstance |-> JavascriptLibrary::CreateNativeIntArray 来完成 Array 的创建。其最终调用的函数逻辑如下

        if(length > SparseArraySegmentBase::HEAD_CHUNK_SIZE)
        {
            return RecyclerNew(recycler, className, length, arrayType);
        }
        array = RecyclerNewPlusZ(recycler, allocationPlusSize, className, length, arrayType);
        SparseArraySegment<unitType> *head =
            InitArrayAndHeadSegment<className, inlineSlots>(array, 0, alignedInlineElementSlots, true);
        head->FillSegmentBuffer(0, alignedInlineElementSlots);

若申请的长度不超过 HEAD_CHUNK_SIZE 即 0x10,首先通过 RecyclerNewPlusZ 分配带有冗余空间的 JavascriptArrayObject,再将冗余空间设置为该 JavascriptArray 的 head segment。通过这种方式创建的 Array ,其 head segment 与 ArrayObject 实际上处于同一个内存块中,拥有相同的生命周期。
若申请的长度超过了 HEAD_CHUNK_SIZE 则仅申请固定大小的 ArrayObject 空间,其所需的 segment 将在以后的赋值操作 DirectSetItem 中进行申请和初始化。

当以 arr = [1,2,3,4,] 形式创建 Array 时, 会调用函数 JavascriptLibrary::CreateCopyOnAccessNativeIntArrayLiteral 来完成 Array 的创建。其最终创建逻辑为

        className* array = RecyclerNewZ(recycler, JavascriptCopyOnAccessNativeIntArray, ints->count, arrayType);
        JavascriptLibrary *lib = functionBody->GetScriptContext()->GetLibrary();

        SparseArraySegment<unitType> *seg;

        if (JavascriptLibrary::IsCachedCopyOnAccessArrayCallSite(functionBody->GetScriptContext()->GetLibrary() , arrayInfo))
        {
            seg = lib->cacheForCopyOnAccessArraySegments->GetSegmentByIndex(arrayInfo->copyOnAccessArrayCacheIndex);
        }
        else
        {
            seg = SparseArraySegment<unitType>::AllocateLiteralHeadSegment(recycler, ints->count);
        }

这种方式仅仅申请固定大小的 ArrayObject 空间,其 segment 将单独进行申请。

Array.prototype.reverse

Array.prototype.reverse 操作负责将 Array 中所有数据进行翻转,由于 Array 的成员均存储在 Segment 中,因此 reverse 操作也会对 Segment 进行翻转。其具体代码如下

/*
  ChakraCore v1.4.2
*/
            SparseArraySegmentBase* seg = pArr->head;
            SparseArraySegmentBase *prevSeg = nullptr;
            SparseArraySegmentBase *nextSeg = nullptr;
            SparseArraySegmentBase *pinPrevSeg = nullptr;
            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) ? ((uint32)length) - (seg->left + seg->length) : 0;

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

            pArr->head = prevSeg;

举例来说一个 Array 其 segment 状态为

----------------       ----------------        ----------------        ----------------
|Array  | head |  ->   |    seg1      |   ->   |    seg2      |   ->   |    seg3      |
----------------       ----------------        ----------------        ----------------

将其 Reverse 之后其 segment 状态变为

----------------       ----------------        ----------------        ----------------
|Array  | head |  <-   |    seg1      |   <-   |    seg2      |   <-   |    seg3      |
----------------       ----------------        ----------------        -------^--------
        \_____________________________________________________________________|

Array.prototype.splice

Array.prototype.splice 操作会删除 Array 中的部分数据并在删除的位置添加新的数据,最后返回被删除的部分。由于需要将删除的数据返回,因此还会新建一 Array 并以 删除的数据为其赋值。为了节省效率对于那些整个 segment 都被删除的情况,splice 函数会将这个 segment 直接移动到那个新 Array 中。具体代码如下

/*
  ChakraCore v1.4.2
*/
                // Step (1)  -- WOOB 1116297: When left >= start, step (1) is skipped, resulting in pNewArr->head->left != 0. We need to touch up pNewArr.
                if (startSeg->left < start)
                {
                    if (start < startSeg->left + startSeg->length)
                    {
                        uint32 headDeleteLen = startSeg->left + startSeg->length - start;

                        if (startSeg->next)
                        {
                            // We know the new segment will have a next segment, so allocate it as non-leaf.
                            newHeadSeg = SparseArraySegment<T>::template AllocateSegmentImpl<false>(recycler, 0, headDeleteLen, headDeleteLen, nullptr);
                        }
                        else
                        {
                            newHeadSeg = SparseArraySegment<T>::AllocateSegment(recycler, 0, headDeleteLen, headDeleteLen, nullptr);
                        }
                        newHeadSeg = SparseArraySegment<T>::CopySegment(recycler, newHeadSeg, 0, startSeg, start, headDeleteLen);
                        newHeadSeg->next = nullptr;
                        *prevNewHeadSeg = newHeadSeg;
                        prevNewHeadSeg = &newHeadSeg->next;
                        startSeg->Truncate(start);
                    }
                    savePrev = startSeg;
                    prevPrevSeg = prevSeg;
                    prevSeg = &startSeg->next;
                    startSeg = (SparseArraySegment<T>*)startSeg->next;
                }

                // Step (2) first we should do a hard copy if we have an inline head Segment
                else if (hasInlineSegment && nullptr != startSeg)
                {
                    // start should be in between left and left + length
                    if (startSeg->left  <= start && start < startSeg->left + startSeg->length)
                    {
                        uint32 headDeleteLen = startSeg->left + startSeg->length - start;
                        if (startSeg->next)
                        {
                            // We know the new segment will have a next segment, so allocate it as non-leaf.
                            newHeadSeg = SparseArraySegment<T>::template AllocateSegmentImpl<false>(recycler, 0, headDeleteLen, headDeleteLen, nullptr);
                        }
                        else
                        {
                            newHeadSeg = SparseArraySegment<T>::AllocateSegment(recycler, 0, headDeleteLen, headDeleteLen, nullptr);
                        }
                        newHeadSeg = SparseArraySegment<T>::CopySegment(recycler, newHeadSeg, 0, startSeg, start, headDeleteLen);
                        *prevNewHeadSeg = newHeadSeg;
                        prevNewHeadSeg = &newHeadSeg->next;

                        // Remove the entire segment from the original array
                        *prevSeg = startSeg->next;
                        startSeg = (SparseArraySegment<T>*)startSeg->next;
                    }
                    // if we have an inline head segment with 0 elements, remove it
                    else if (startSeg->left == 0 && startSeg->length == 0)
                    {
                        Assert(startSeg->size != 0);
                        *prevSeg = startSeg->next;
                        startSeg = (SparseArraySegment<T>*)startSeg->next;
                    }
                }
                // Step (2) proper
                SparseArraySegmentBase *temp = nullptr;
                while (startSeg && (startSeg->left + startSeg->length) <= limit)
                {
                    temp = startSeg->next;

                    // move that entire segment to new array
                    startSeg->left = startSeg->left - start;
                    startSeg->next = nullptr;
                    *prevNewHeadSeg = startSeg;
                    prevNewHeadSeg = &startSeg->next;

                    // Remove the entire segment from the original array
                    *prevSeg = temp;
                    startSeg = (SparseArraySegment<T>*)temp;
                }

可以看出,如果 segment 不是 startSeg 且其中树数据均处于删除的范围之内,则直接将该 segment 移动到返回的 Array 中。

漏洞成因

结合以上三个条件,便有可能产生漏洞。

假设 splice 移动的 segment 是一个 InlineSegment ,那么该 InlineSegment 将被认为是新 Array 的segment ,然而 InlineSegment 的生命周期却和其创建者一致。当其创建者 Array 被释放后,InlineSegment将同时被释放。于是新Array 将链接一个已经被释放的 segemnt 空间!!!

然而一般情况下 InlineSegment 一定会是 Array 的第一个 segment,即若出现在 splice 的删除列表中,则一定是 startSeg。然而通过 Reverse 操作,可以将 InlineSegment 变成 Array 的最后一个 segment 从而绕过了这种限制。

举例来说:

一个 Array 其 segment 状态为

----------------       ----------------        ----------------        ----------------
|Array  | head |  ->   |    seg1      |   ->   |    seg2      |   ->   |    seg3      |
----------------       ----------------        ----------------        ----------------

将其 Reverse 之后其 segment 状态变为

----------------       ----------------        ----------------        ----------------
|Array  | head |  <-   |    seg1      |   <-   |    seg2      |   <-   |    seg3      |
----------------       ----------------        ----------------        -------^--------
        \_____________________________________________________________________|

对其调用 splice 删除最后几个seg, 返回值为 New Array

----------------       ----------------        ----------------        ----------------
|Array  | head |   <-  |    seg1      |        |    seg2      |   <-   |    seg3      |
----------------       -------^--------        ----------------        -------^--------
        \_____________________|_______________________________________________|
----------------              |
|   new Array  | _____________| 
----------------  

接着释放 Array

----------------       ----------------
|     FREE     |   <-  |    seg1      |
----------------       -------^--------
                              |
----------------              |
|   new Array  | _____________| 
----------------  

至此 new_Array 便可以访问到已经 free 的一段空间

补丁分析

这个漏洞前后修补了四次,由于笔者原因这里只能对其中三次补丁情况进行分析。

补丁一

补丁在 ReverseHelper 函数中添加了函数判断,如果当前操作的 arr 的第一个 segment 是一个 InlineSegment ,为了避免漏洞的出现,首先将其替换成为一个非 Inline 的 segment

/*
  ChakraCore v1.4.4
*/

// https://github.com/Microsoft/ChakraCore/pull/2959/commits/0cdbf2fe68f452c163ca5307cb9c57b118e966cc
 +            // During the loop below we are going to reverse the segments list. The head segment will become the last segment.
 +            // We have to verify that the current head segment is not the inilined segement, otherwise due to shuffling below, the inlined segment will no longer
 +            // be the head and that can create issue down the line. Create new segment if it is an inilined segment.
 +            if (pArr->head && pArr->head->next)
 +            {
 +                if (isIntArray)
 +                {
 +                    CopyHeadIfInlinedHeadSegment<int32>(pArr, recycler);
 +                }
 +                else if (isFloatArray)
 +                {
 +                    CopyHeadIfInlinedHeadSegment<double>(pArr, recycler);
 +                }
 +                else
 +                {
 +                    CopyHeadIfInlinedHeadSegment<Var>(pArr, recycler);
 +                }
 +            }
 +
 +            SparseArraySegmentBase* seg = pArr->head;
 +
              while (seg)
              {
                  nextSeg = seg->next;

该补丁之后之前的样本将不能再触发漏洞。但是由于其判断 if (pArr->head && pArr->head->next),使得操作忽略了单 segment 的情。
考虑如下样本

var arr = new Array(100)

for (i=0;i<arr.length;i++)
{
    arr[i] = new Array(1,2);
}

var b = new Array(2,3,4,5);

arr[1].length = 4;

arr[1].reverse()

b = arr[1].splice(2,3)

a1 原本只有两个成员,也处于 inlinesegment 中,接着将其 length 修改为 4,length 的长度增加并不会影响segment。继续进入 Reverse 函数,可以完美的绕过补丁代码,进行segment 的转化。
此时 Array length 为4,head segment 的 length 为 2。查看转化部分代码可以看出,segment->left 是根据 length 计算而来。

            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) ? ((uint32)length) - (seg->left + seg->length) : 0;

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

回到样本中,array 经转化之后其 head segment 变为 seg->left = 2; seg.length = 2;。Array 的起始下标规定应该从 0 开始,这样显然不合理,因此 Reverse 会调用函数 EnsureHeadStartsFromZero 在当前的 head 之前再添加一段 起始为 0 的 segment,从而使得原先的 inline head 又被移动到了 array 的末尾。漏洞依然存在~~~~~

补丁二

因此在 v1.7.1 版本中对这里再次进行修补,完善了检测机制

/*
  ChakraCore v1.7.1
*/

// https://github.com/Microsoft/ChakraCore/pull/3509/files
-            if (pArr->head && pArr->head->next)
+           if (pArr->head && (pArr->head->next || (pArr->head->left + pArr->head->length) < length))

补丁三

除没有完全修补好的问题外,Reverse 函数中还存在另一个问题。

为了加快垃圾收集阶段的效率,在 Segment 的分配阶段有这样的判断,对于纯数据的 Segment 如果其没有 next 节点,则使用 leaf HeapBlock 完成内存的分配请求,此时 Chakra 认为其内存区域内不会有对象指针出现,Leaf Segment 在 Mark 阶段将不会进行逐字节的扫描。

    inline SparseArraySegment<int> * SparseArraySegment<int>::AllocateSegment(Recycler* recycler, uint32 left, uint32 length, SparseArraySegmentBase *nextSeg)
    {
        if (DoNativeArrayLeafSegment() && nextSeg == nullptr)
        {
            return AllocateSegmentImpl<true>(recycler, left, length, nextSeg);
        }
        return AllocateSegmentImpl<false>(recycler, left, length, nextSeg);
    }

但是这个设定在 Array.prototype.reverse 函数中将不会得到满足。按照设定 NativeIntArray 的最后一个 segment 中不会有需要标记的对象指针存在,因此使用 Leafblock 分配,但调用 Array.prototype.reverse 之后最后一个 Segment 会变成 Head segment。如果该 Array 是一个稀疏数组那么 Head segment 一定会有 next 指针。但是此时的 segment 处于 Leafblock 中,在Mark 阶段 next 指针将不会被标记,从而有可能在 sweep 阶段被释放,产生 UAF!

Reverse 函数的编写者显然也考虑到了这个问题,于是在 Reverse 函数交换 segment 完毕之后会对 head segment 进行判断,如果 head segment 有 next 成员,并且 head segment 在 leaf HeapBlock 中那么就 Realloc 这个 head。

查看函数的总体逻辑,可以发现 ReallocLeaf 操作是发生在segment 交换完毕之后的,此时 Array 中的segment 已经变成如下所示。此时再进行 Realloc 操作,那么如果 Realloc 操作中触发了 GC ,在这次 GC 中由于 seg3 处于 LeafHeapBlock 中因此其指向 seg2 的指针将不会被标记。从而在 Sweep 阶段 seg2 将会被释放。

----------------       ----------------        ----------------        ----------------
|     Array    |  <-   |    seg1      |   <-   |    seg2      |   <-   |  seg3(leaf)  |  
----------------       ----------------        ----------------        -------^--------
        \_____________________________________________________________________|

对于这个问题,新的补丁中将 Realloc 操作提前,一旦判定 Array 拥有 head segment,并且 head 拥有 next 字段且 Array 是一个数字 Array 就调用 Realloc 操作重新分配 Array 最后一个 segment

             if (pArr->head && pArr->head->next)
             {
                  if (isIntArray)
                  {
                      CopyHeadIfInlinedHeadSegment<int32>(pArr, recycler);
 +                    ReallocateNonLeafLastSegmentIfLeaf<int32>(pArr, recycler);
                  }
                  else if (isFloatArray)
                  {
                      CopyHeadIfInlinedHeadSegment<double>(pArr, recycler);
 +                    ReallocateNonLeafLastSegmentIfLeaf<double>(pArr, recycler);
                  }
                  else
                  {
                     CopyHeadIfInlinedHeadSegment<Var>(pArr, recycler);
                 }
             }

补丁四

最新的补丁中又对 reverse 函数进行了修补,修补理由是 ReallocateNonLeafLastSegmentIfLeaf 函数有可能触发 OOM 异常从而会让 Array 的 lastUsedSegment 字段指向未知的位置。

经笔者分析,并没有能够发现在 OOM 之前修改 lastUsedSegment 的情况,因此未能构造样本

对于这个问题的补丁也十分简单,就是在 Reverse 中加入 AutoDisableInterrupt failFastOnError(scriptContext->GetThreadContext()); 检测异常。

总结

这个漏洞涉及到的情况其实并不算很复杂,但是在漏洞修复方面却出现了如此多的问题,思考起来是由于 Array.prototype.reverse 函数本身的特点,这个函数会造成 Array 自身的结构变动,从而影响到所有与 Array 结构相关的功能。如果在编写代码时没有对这些功能的整体有一个大体的了解就可能造成这样那样的问题,同时在补丁上也难以一步到位。

文章在撰写的过程中难免会有一些疏漏,也有可能出现由于对引擎本身理解不够导致的错误,还请大家予以指正。

Reference

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容