V8-PATCH GAPPING GOOGLE CHROME
PATCH-GAPPING GOOGLE CHROME
原文链接 https://blog.exodusintel.com/2019/09/09/patch-gapping-chrome/
Object Layout In v8
JS engines implement several optimizations on the property storage of objects. A common technique is to use separate backing stores for the integer keys (often called elements) and string/Symbol keys (usually referred to as slots or named properties). This allows the engines to potentially use continuous arrays for properties with integer keys, where the index maps directly to the underlying storage, speeding up access. String keyed values are also stored in an array but to get the index corresponding to the key, another level of indirection is needed. This information, among other things, is provided by the map (or HiddenClass) of the object.
The storage of object shapes in a HiddenClass is another attempt at saving storage space. HiddenClasses are similar in concept to classes in object-oriented languages. However, since it is not possible to know the property configuration of objects in a prototype-based language like JavaScript in advance, they are created on demand. JS engines only create a single HiddenClass for a given shape, which is shared by every object that has the same structure. Adding a named property to an object results in the creation of a new HiddenClass, which contains the storage details for all the previous properties and the new one, then the map of the object is updated, as shown below (figures from the v8 dev blog).
img These transitions are saved in a HiddenClass chain, which is consulted when new objects are created with the same named properties, or the properties are added in the same order. If there is a matching transition, it is reused, otherwise a new HiddenClass is created and added to the transition tree.
img The properties themselves can be stored in three places. The fastest is in-object storage, which only needs a lookup for the key in the HiddenClass to find the index into the in-object storage space. This is limited to a certain number of properties, others are stored in the so-called fast storage, which is a separate array pointed by the properties member of the object, as shown below.
imgIf an object has many properties added and deleted, it can get expensive to maintain the HiddenClasses. V8 uses heuristics to detect such cases and migrate the object to a slow, dictionary based property storage, as shown on the following diagram.
image Another frequent optimization is to store the integer keyed elements in a dense or packed format, if they can all fit in a specific representation, e.g. small integer or float. This bypasses the usual value boxing in the engines, which stores numbers as pointers to Number objects, thus saving space and speeding up operations on the array. V8 handles several such element kinds, for example PACKED_SMI_ELEMENTS, which denotes an elements array with small integers stored contiguously. This storage format is tracked in the map of the object and needs to be kept updated all the time to avoid type confusion issues. Element kinds are organized into a lattice, transitions are only ever allowed to more general types. This means that adding a float value to an object with PACKED_SMI_ELEMENTS elements kind will convert every value to double, set the newly added value and change the element kind to PACKED_DOUBLE_ELEMENTS.
preventExtensions, seal and freeze
JavaScript provides several ways to fix the set of properties on an object.
- Object.preventExtensions: prevents new properties from being added to the object.
- Object.seal: prevents the addition of new properties, as well as the reconfiguration of existing ones (changing their writable, enumerable or configurable attributes).
- Object.freeze: the same as Object.seal but also prevent the changing of property values, thus effectively prohibiting any change to an object.
PoC analysis
The vulnerability arises because v8 follows map transitions in certain cases without updating the element backing store accordingly, which can have wide-ranging consequences. A modified trigger with comments is shown below.
// Based on test/mjsunit/regress/regress-crbug-992914.js
function mainSeal() {
const a = {foo: 1.1}; // a has map M1
Object.seal(a); // a transitions from M1 to M2 Map(HOLEY_SEALED_ELEMENTS)
const b = {foo: 2.2}; // b has map M1
Object.preventExtensions(b); // b transitions from M1 to M3 Map(DICTIONARY_ELEMENTS)
Object.seal(b); // b transitions from M3 to M4
const c = {foo: Object} // c has map M5, which has a tagged `foo` property, causing the maps of `a` and `b` to be deprecated
b.__proto__ = 0; // property assignment forces migration of b from deprecated M4 to M6
a[5] = 1; // forces migration of a from the deprecated M2 map, v8 incorrectly uses M6 as new map without converting the backing store. M6 has DICTIONARY_ELEMENTS while the backing store remained unconverted.
}
mainSeal();
在POC代码中,a和b初始的layout是相同的,然后a变为了sealed,b调用了Object.preventExtensions 和 Object.seal,,导致a转换成了带有HOLEY_SEALED_ELEMENTS的map,b变为了慢速的储存类型,element kind 为 DICTIONARY_ELEMENTS 的 map。
漏洞触发点在10-13行,这些操作首先创造了一个c对象,有incompatibly的属性foo,导致创建一个新的带有foo属性的map,a和b的map会被废弃掉,他们会在设置值操作的时候迁移到一个新的map。
This causes a type confusion to happen between a FixedArray (the Properties array shown in the Object Layout In v8 section) and a NumberDictionary (the Properties Dict).
A type confusion the other way around is also possible, as demonstrated by another regression test in the patch. There are probably also other ways this invalid map transition could be turned into an exploitable primitive, for example by breaking assumptions made by the optimizing JIT compiler.
Exploitation
The vulnerability can be turned into an arbitrary read/write primitive by using the type confusion shown above to corrupt the length of an Array, then using that Array for further corruption of TypedArrays. These can then be leveraged to achieve arbitrary code execution in the renderer process.
FixedArray and NumberDictionary Memory Layout
FixedArray is the C++ class used for the backing store of several different JavaScript objects. It has a simple layout, shown below, with only a map pointer, a length field stored as a v8 small integer (essentially a 31-bit integer left-shifted by 32), then the elements themselves.
pwndbg> job 0x065cbb40bdf1
0x65cbb40bdf1: [FixedDoubleArray]
map: 0x1d3f95f414a9
length: 16
0: 0.1
1: 1
2: 2
3: 3
4: 4
…
pwndbg> tel 0x065cbb40bdf0 25
00:0000 0x65cbb40bdf0 -> 0x1d3f95f414a9 <- 0x1d3f95f401
01:0008 0x65cbb40bdf8 <- 0x1000000000
02:0010 0x65cbb40be00 <- 0x3fb999999999999a
03:0018 0x65cbb40be08 <- 0x3ff0000000000000
04:0020 0x65cbb40be10 <- 0x4000000000000000
…
The NumberDictionary class implements an integer keyed hash table on top of FixedArray. Its layout is shown below. It has four additional members besides map and length:
- elements: the number of elements stored in the dictionary.
- deleted: number of deleted elements.
- capacity: number of elements that can be stored in the dictionary. The length of the FixedArray backing a number dictionary will be three times its capacity plus the extra header members of the dictionary (four).
- max number key index: the greatest key stored in the dictionary.
The vulnerability makes it possible to set these four fields to arbitrary values in a plain FixedArray, then trigger the type confusion and treat them as header fields of a NumberDictionary.
pwndbg> job 0x2d7782c4bec9
0x2d7782c4bec9: [NumberDictionary]
- map: 0x0c48e8bc16d9 <Map>
- length: 28
- elements: 4
- deleted: 0
- capacity: 8
- elements: {
0: 0x0c48e8bc04d1 <undefined> -> 0x0c48e8bc04d1 <undefined>
1: 0 -> 16705
2: 0x0c48e8bc04d1 <undefined> -> 0x0c48e8bc04d1 <undefined>
3: 1 -> 16706
4: 0x0c48e8bc04d1 <undefined> -> 0x0c48e8bc04d1 <undefined>
5: 0x0c48e8bc04d1 <undefined> -> 0x0c48e8bc04d1 <undefined>
6: 2 -> 16707
7: 3 -> 16708
}
pwndbg> tel 0x2d7782c4bec9-1 25
00:0000 0x2d7782c4bec8 -> 0xc48e8bc16d9 <- 0xc48e8bc01
01:0008 0x2d7782c4bed0 <- 0x1c00000000
02:0010 0x2d7782c4bed8 <- 0x400000000
03:0018 0x2d7782c4bee0 <- 0x0
04:0020 0x2d7782c4bee8 <- 0x800000000
05:0028 0x2d7782c4bef0 <- 0x100000000
06:0030 0x2d7782c4bef8 -> 0xc48e8bc04d1 <- 0xc48e8bc05
...
09:0048 0x2d7782c4bf10 <- 0x0 // key
0a:0050 0x2d7782c4bf18 <- 0x414100000000 // value
0b:0058 0x2d7782c4bf20 <- 0xc000000000 // PropertyDescriptor
0c:0060 0x2d7782c4bf28 -> 0xc48e8bc04d1 <- 0xc48e8bc05
...
0f:0078 0x2d7782c4bf40 <- 0x100000000
10:0080 0x2d7782c4bf48 <- 0x414200000000
11:0088 0x2d7782c4bf50 <- 0xc000000000
Elements in a NumberDictionary are stored as three slots in the underlying FixedArray. E.g. the element with the key 0 starts at 0x2d7782c4bf10 above. First comes the key, then the value, in this case a small integer holding 0x4141, then the PropertyDescriptor denoting the configurable, writable, enumerable attributes of the property. The 0xc000000000 PropertyDescriptor corresponds to all three attributes set.
The vulnerability makes all header fields of a NumberDictionary, except length, controllable by setting them to arbitrary values in a plain FixedArray, then treating them as header fields of a NumberDictionary by triggering the issue. While the type confusion can also be triggered in the other direction, it did not yield any immediately promising primitives. Further type confusions can also be caused by setting up a fake PropertyDescriptor to confuse a data property with an accessor property but these also proved too limited and were abandoned.
The capacity field is the most interesting from an exploitation perspective, since it is used in most bounds calculations. When attempting to set, get or delete an element, the HashTable::FindEntry function is used to get the location of the element corresponding to the key. Its code is shown below.
// Find entry for key otherwise return kNotFound.
template <typename Derived, typename Shape>
int HashTable<Derived, Shape>::FindEntry(ReadOnlyRoots roots, Key key,
int32_t hash) {
uint32_t capacity = Capacity();
uint32_t entry = FirstProbe(hash, capacity);
uint32_t count = 1;
// EnsureCapacity will guarantee the hash table is never full.
Object undefined = roots.undefined_value();
Object the_hole = roots.the_hole_value();
USE(the_hole);
while (true) {
Object element = KeyAt(entry);
// Empty entry. Uses raw unchecked accessors because it is called by the
// string table during bootstrapping.
if (element == undefined) break;
if (!(Shape::kNeedsHoleCheck && the_hole == element)) {
if (Shape::IsMatch(key, element)) return entry;
}
entry = NextProbe(entry, count++, capacity);
}
return kNotFound;
}
The hash tables in v8 use quadratic probing with a randomized hash seed. This means that the hash argument in the code, and the exact layout of dictionaries in memory will change from run to run. The FirstProbe and NextProbefunctions, shown below, are used to look for the location where the value is stored. Their size argument is the capacity of the dictionary and thus, attacker-controlled.
The hash tables in v8 use quadratic probing with a randomized hash seed. This means that the hash argument in the code, and the exact layout of dictionaries in memory will change from run to run. The FirstProbe and NextProbefunctions, shown below, are used to look for the location where the value is stored. Their size argument is the capacity of the dictionary and thus, attacker-controlled.
inline static uint32_t FirstProbe(uint32_t hash, uint32_t size) {
return hash & (size - 1);
}
inline static uint32_t NextProbe(uint32_t last, uint32_t number, uint32_t size) {
return (last + number) & (size - 1);
}
Capacity is a power-of-two number under normal conditions and masking the probes with capacity-1 results in limiting the range of accesses to in-bounds values. However, setting the capacity to a larger value via the type-confusion will result in out-of-bounds accesses. The issue with this approach is the random hash seed, which will cause probes and thus out-of-bounds accesses to random offsets. This can easily results in crashes, as v8 will try to interpret any odd value as a tagged pointer.
A possible solution is to set capacity to an out-of-bounds number k that is a power-of-two plus one. This causes the FindEntry algorithm to only visit two possible locations, one at offset zero, and one at offset k (times three). With careful padding, a target Array can be placed following the dictionary, which has its length property at just that offset. Invoking a delete operation on the dictionary with a key that is the same as the length of the target Array will cause the algorithm to replace the length with the hole value. The hole is a valid pointer to a static object, in effect a large value, allowing the target Array to be used for more convenient, array-based out-of-bounds read and write operations.
While this method can work, it is nondeterministic due to the randomization and the degraded nature of the corrupted NumberDictionary. However, failure does not crash Chrome and is easily detectable; reloading the page reinitializes the hash seed so the exploit can be attempted an arbitrary number of times.
Arbitrary Code Execution
The following object layout is used to gain arbitrary read/write access to the process memory space:
- o: the object that will be used to trigger the vulnerability.
- padding: an Array that is used as padding to get the target float array at exactly the right offset from o.
- float_array: the Array that is the target of the initial length corruption via the out-of-bounds element deletion on o.
- tarr: a TypedArray used to corrupt the next typed array.
- aarw_tarr: typed array used for arbitrary memory access.
- obj_addrof: object used to implement the addrof primitive which leaks the address of an arbitrary JavaScript object.
The exploit achieves code execution by the following the usual steps after the initial corruption:
- Create the layout described above.
- Trigger the vulnerability, corrupt the length of float_array through the deletion of a property on o. Restart the exploit by reloading the page in case this step fails.
- Corrupt the length of tarr to increase reliability, since continued usage of the corrupted float array can introduce problems.
- Corrupt the backing store of aarw_tarr and use it to gain arbitrary read write access to the address space.
- Load a WebAssembly module. This maps a read-write-executable memory region of 4KiB into the address space.
- Traverse the JSFunction object hierarchy of an exported function from the WebAssembly module using the arbitrary read/write primitive to find the address of the read-write-executable region.
- Replace the code of the WebAssembly function with shellcode and execute it by invoking the function.
The complete exploit code can be found on our GitHub page and seen in action below. Note that a separate vulnerability would be needed to escape the sandbox employed by Chrome.