SSD Advisory – Chrome Turbofan Remote Code Execution

Credit to Author: SSD / Maor Schwartz| Date: Wed, 16 Aug 2017 07:21:39 +0000

Want to get paid for a vulnerability similar to this one?
Contact us at: sxsxdx@xbxexyxoxnxdxsxexcxuxrxixtxy.xcom

Vulnerability Summary
The following advisory describes a type confusion vulnerability that leads to remote code execution found in Chrome browser version 59.

Chrome browser is affected by a type confusion vulnerability. The vulnerability results from incorrect optimization by the turbofan compiler, which causes confusion between access to an object array and a value array, and therefore allows to access objects as if they were values by reading them as if they were values (thus receiving their in memory address) or vice-versa to write values into an object array and thus being able to fake objects completely.

Credit
An independent security researcher has reported this vulnerability to Beyond Security’s SecuriTeam Secure Disclosure program

Vendor response
Google was informed of the vulnerability, and a ticket has been opened: https://bugs.chromium.org/p/chromium/issues/detail?id=746946, because the vulnerability stopped working in Chrome 60 – Google has no plan to address it as a security advisory/patch.

Vulnerability details

Background

Object maps

Every object has a map representing the object’s structure (keys and types of values). Two objects of the same structure (but with different values) will have the same map. The most common representation of an object is as follows:

Where the map field (a pointer to a map) holds the objects map. The two fixed arrays hold extra named properties and numbered properties respectively. The numbered properties are commonly named “Elements”.

Map transitions
When we add a new property to an object, the object’s map is now invalid. A new map is created to fit the new structure, and a transition descriptor is added to the original map to show how to change it into the new map.

For example:

These transitions can later be used by the compiler to re-optimize functions when an inline cache miss occurs.

Elements kind
The elements of an object are, as stated above, the values for numbered keys. These are stored in a regular array pointed to from the object. The object’s map has a special bitfield called ElementsKind. This field describes whether the values in the elements array are boxed, unboxed, contiguous, sparse, etc. Maps that only differ by the elements kind are not connected by a transition.

V8 arrays
Arrays in v8 are typed, and can have either “boxed” or “unboxed” values. This basically determines whether the array only holds doubles (integers are also represented as doubles), and therefore can hold the values directly (usually called “fast” arrays), or the array also holds more complex values, in which case the values will in fact be pointers to objects.

A simplified representation of the two cases:

(The type of the array itself determines whether the values are boxed or unboxed).

So, if we have a fast array such as the left above and then we assign a complex object (such as an array) to one of the slots, the whole array is turned to a boxed one, and all existing values are changed to boxed ones as well.

V8 optimization
The V8 compiler first analyzes the javascript code to generate JIT compiled code with very loose assumptions on types using an inline cache.

The following explanations are taken from Google’s V8 documentation:

“V8 compiles JavaScript source code directly into machine code when it is first executed. There are no intermediate byte codes, no interpreter. Property access is handled by inline cache code that may be patched with other machine instructions as V8 executes….”
“…V8 optimizes property access by predicting that this [the object’s] class will also be used for all future objects accessed in the same section of code and uses the information in the class to patch the inline cache code to use the hidden class. If V8 has predicted correctly the property’s value is assigned (or fetched) in a single operation. If the prediction is incorrect, V8 patches the code to remove the optimisation.”

So the compiler will only compile code that works for specific types. If the next time this code section (or function) executes the type does not match the one compiled, an “inline cache miss” will occur, causing the compiler to recompile the code.

For example, assume we have a function f and two objects o1 and o2 as such:

Now if the function is first called with o1, the compiler will generate code like the following:

when the function is called again with o2, the cache miss occurs, and the function’s JIT code will be changed by compiler code.

The vulnerability

Element kind transitions

When a cache miss occurs and the compiler wants to re-optimize function code, the compiler uses both saved transitions and generates needed ElementsKindTransitions (transitions to a map that only differs by elements kind) on the fly (using the function Map::FindElementsKindTransitionedMap). The reason these are done on the fly is because they only require to change the ElementsKind bit field, and not completely change the map.

Stable maps
Maps are marked stable when the code to access their elements is already optimized.

The vulnerability occurs when the optimization compiler decides that a function is used enough and is worth “Reducing” (trying to further optimize the code to, as it is called, reduce its size). The function ReduceElementAccess is called to reduce accesses to elements of an object. It in turn calls ComputeElementAccessInfos.

ComputeElementAccessInfos is also the function that searches for possible elements kind transitions that can help optimization.

The problem is if such a transition will be generated and used from a stable map. The reason this is problematic is since if such a transition is used, it will only effect the current function, and other functions that use the same stable map will not take the elements kind transition into consideration.

What happens is this: First, a function is reduced in a way that makes it change the elements kind of a stable map. Next, a second function is reduced in a way that simply stores / loads a property in the same stable map. Now, an object of that map is created. The first function is called with that object as the argument, and the elements kind is changed.

The second function is called, and the inline cache does not miss (since, remember, an elements kind transition is not a regular transition into a different map type that would cause the cache to miss).

Since the cache did not miss, the function stores/loads properties as if the object’s elements were still unboxed, so we get a read/write into an array of object pointers.

However, this was actually already addressed in a previous commit (https://chromium.googlesource.com/v8/v8/+/2d856544e5e3cb8abf99a30749b4bfe39c29886a) – “Ensure source map is not stable if elements kind transitions are expected.”

What the compiler does is the following – When a cache miss occurs on the function, the compiler checks if the miss can be rectified using an elements kind transition. This is done in KeyedStoreIC::StoreElementPolymorphicHandlers and KeyedLoadIC::LoadElementPolymorphicHandlers. The diff caused by the commit shows that if the source map for the transition is stable, it is set to unstable (meaning optimized code is decompiled), to make sure that the transition will affect all functions using the map.

So the first time a function call needs to change the map’s Elements Kind, StoreElementPolymorphicHandlers calls FindElementsKindTransitionedMap, finds an elements kind transition, and makes sure to set the source map as unstable, thus assuring that code using the map will be deoptimized and future code will not be optimized on it, making sure elements kind will be handled appropriately.

So, how do we get an elements kind transition from a stable map despite of the above?

Just before we explain this we have to explain what a deprecated map is. A deprecated map is a map that all objects of that map have been changed to a different map. This map is set to be unstable, deoptimized, and is removed from the transition tree (the transition to it is removed, as well as any transitions from it).

Now, if we take a look at ComputeElementAccessInfos code, we can see that just before the call to FindElementsKindTransitionedMap, a call to TryUpdate is performed.

Tryupdate is a function that, upon receiving a deprecated map, attempts to find another map from the same “tree” (meaning coming from the same root map through the same transitions) that is not deprecated, and returns that if such a map exists.

The original source map for the elements kind transition, set to unstable in LoadElementPolymorphicHandlers has become deprecated. TryUpdate finds another map, and switches to that one. But this map was never used in optimizing this function, so it was never set to unstable, and we again get an elements kind transition from a stable map.

The source code actually has a debug check to make sure that a transition was not generated from a stable map (added at the same commit where maps are made unstable), but this obviously does not affect release versions:

Minimal Proof of Concept

Patch
Very simple, an is_stable() check is added before the call to FindElementsKindTransitionedMap.

Full Proof of Concept

The following PoC will run calc when attacking a –no-sandbox chrome version 59.

  1. The vulnerability is used to read the address of arraybuffer.__proto__.
  2. We build a fake ArrayBuffer map (using the address of the arraybuffer proto needed in a map), and use the vulnerability to read the address of the fake map.
  3. Using the address of the fake map, we can build a fake ArrayBuffer object with that map, and use the vulnerability again to get it’s address.
  4. We use the vulnerability to write the pointer to our fake ArrayBuffer into a boxed elements array, allowing us to now access our fake ArrayBuffer regularly from JS code. At the same time, we can edit the fake ArrayBuffer to reflect different parts of usermode memory. So we now have full read/write access. We use the vulnerability one more time to read the address of a compiled function, and then use our R/W capabilities to override that with our shellcode, and eventually call the function to execute the shellcode.

Print Friendly, PDF & Email

https://blogs.securiteam.com/index.php/feed