The vulnerability is classified as a type confusion bug. Meaning the code failed to verify the type of object that is passed to it. Type confusion can be very dangerous because sending wrong pointers or data to into the wrong piece of code can lead to code execution in some circumstances.
In our case the entry point of the vulnerability is via the FillFromPrototypes method. As the name suggests it is used to set the native elements of an array based on its prototype. FillFromPrototypes internally calls ForEachOwnMissingArrayIndexOfObject with the prototype of the target as its argument. If the prototype is an array the function assumes its a var array and fills the target array with entries corresponding to a var array. So if we manage to send a prototype that looks like an array but is actually native int array, It will treat the entries of the int array as pointers.
As per MDN the Proxy object can be used to define custom behavior for fundamental operations. Proxy object creation requires two things, target, this specifies the object we are proxying for and handler, a placeholder to define actions when certain events occur on the proxy object. One such event is getPrototypeOf, and we can have it return any prototype that we like for eg Native int. So to sum it up when ForEachOwnMissingArrayIndexOfObject tries to find the prototype of proxy object it will return Native int and perform var array operations on it. This allows an integer to be treated as an absolute pointer. A walk through of the vulnerability will paint a much clearer picture of what the bug does.
For our analysis we will use a PoC to replicate the issue. The code targets FillFromPrototypes via Array.prototype.shift. Shift removes the first element from the array and shifts the remaining elements and recalculates the array length. So [1,2,3,4] becomes [2,3,4]. Here the call sequence EntryShift->FillFromPrototypes->ForEachOwnMissingArrayIndexOfObject.
The crash log is clearly shows the ChakraCore trying to load a quad word from an invalid address. Interestingly the value in register rdx = 0000777712345670, is the last two entries from array ‘a’. The application crashes out.
Lets check the contents of array ‘b’ before the crash.
The contents of the array are as expected. After the shift operation ‘0000777712345670’ has been added as the last element of the array so shift.call(b) will result in an invalid address lookup. This shows that it is reading 8 bytes from the array assuming each entry in the array is a pointer. The expected behavior is for the last entry of array ‘b’ to be 0x44444444.
As per the ChakraCore code FillFromPrototypes calls ForEachOwnMissingArrayIndexOfObject on a loop till the prototype of said element is null. So on one of the iteration the array prototype check fails to detect NativeInt and reads values from array ‘a’ assuming it’s a var array.
In the code snippet above, prototype is updated via prototype = prototype->GetPrototype. Below is a code snippet of ForEachOwnMissingArrayIndexOfObject to determine the prototype from argument ‘obj’ and based on the result it will initialize array ‘arr’.
A point to note ChakraCore defines all types as numbers. So the first time ForEachOwnMissingArrayIndexOfObject() is called it determines that the array element is an object and checks if it has a valid object array.
19 represents ‘TypeIds_Proxy’ which is expected, as the PoC manually sets the prototype to proxy.
Now continuing with the loop prototype = prototype->GetPrototype triggers the getPrototypeOf event for the proxy object which is handled by the custom handler getPrototypeOf defined in the PoC. Now ForEachOwnMissingArrayIndexOfObject is called with prototype pointing to address of array ‘a’.
array ‘a’ is sent as an argument to ForEachOwnMissingArrayIndexOfObject . We can verify this using a debugger, clearly showing the contents of NativeIntArray ‘a’.
it tries to read element at index 3 from array ‘a’ and calls SetItem() as an inline function to write it to array ‘b’.
After which EntryShift overwrites the first element in ‘b’ by shifting the remaining elements, it uses the inline function SparseArraySegment<void *>::RemoveElement to do this. Below is the result after shift.
Notice the the missing ‘000002247892d000’. The address space remains the same but points to different values.
What does this say about the vulnerability? It gives us a way to read from any memory address in the process space. From an exploitation point of this is can used to find the base address of a target module through vftable, more of this is discussed in the following section. With ASLR a module may be loaded at any address but it’s offsets remain the same. So finding this address is the starting point for code execution in the exploit chain.
PutDataAndGetAddr is used to obtain the address of array ‘x’ stored on the heap. Once this is obtained we will use the TriggerFillFromPrototypesBug to create a fake DataView object in the data slots of the array ‘x’. We use DataView object because it lets you read and write to a memory location without worrying about endianess of the data. Once it is created we can find the base address for chakra.dll. From here the code walks the stack to find the return address for ScriptEngine::ExecutePendingScripts.
After that it writes the shellcode to an array object and uses PutDataAndGetAddr to find its address on the heap.
Now it builds the ROP chain to set execute permission on the memory pages where the shellcode stored using VirtualProtect and then return to the stack where it is pointing to the address where the shellcode is stored. We overwrite the stack where the return address of ScriptEngine::ExecutePendingScripts is stored with the addresses of the ROP gadgets.
These are very interesting vulnerabilities for study. With the implementation of ASLR attackers have the additional task of finding the address of the target module. In this case there is no need for any heap manipulation by spraying, CVE-2016-7200 provides the exact location on the heap. Usually exploits try to find ROP gadgets dynamically, while in our case they are hard coded, though this reduces the code size and detection it has its demerits. An important point to note here is the exploit bypasses CFG an exploit mitigation technique focusing on indirect function calls . ThreatProtect will continue provide more coverage on these vulnerabilities. We highly recommend our clients to scan their network using QID 91300 to detect vulnerable versions of Microsoft Edge.