In this write-up, we will present several techniques used in exploiting a vulnerability in Google Chrome, and the various difficulties presented by its security mechanisms and considerations. We also offer some reflections regarding how some of the techniques used were made irrelevant by mitigations introduced since.
The exploit was developed to exploit a bug in Chrome 33, a winning submission to Pwn2Own 2014 by geohot, which later also awarded him the Best Client-Side Bug pwnie award.
The Bug
The vulnerability existed in Chrome's implementation of ArrayBuffers, and is described in some detail in this issue page in the Chromium repository, along with an impressively concise exploit implemented by geohot himself.
This information was unavailable when we were researching the submission, so we had to make do with the code diff.
To keep things short, let's just take a look at the relevant fix. Surprisingly, the vulnerability and the fix were in the internal JavaScript code used by the V8 engine, and not in a native component:
What we're seeing here is the code that would be invoked whenever a Typed Array of any sort (Uint32/16/8 etc) is constructed on top of an existing ArrayBuffer instance.
On the left, we see that before the fix, the bufferByteLength is determined by reading the byteLength field of the underlying instance.
On the right, we see that the length is now retrieved via a call to %ArrayBufferGetByteLength, which denotes a native function call.
This Byte Length is later used to calculate the element length of the new Typed Array (byteLength/sizeof(element).
The issue then lies with the user's ability to somehow control the byte length field of an ArrayBuffer. Amusingly, this is accomplished by simply overriding the byteLength getter for an ArrayBuffer instance.
Consider this code snippet:
1 2 3 |
faulty_arr_buf = new ArrayBuffer(0x20); faulty_arr_buf.__defineGetter__("byteLength", function() { return 0xFFFFFFFC; }); faulty_arr = new Uint32Array(faulty_arr_buf); |
An ArrayBuffer 0x20 bytes in size is instantiated. Its byteLength getter is then overridden using the __defineGetter__ method.
If you refer to the fixed code above, the issue becomes clear- you can trick the browser into creating a Typed Array of arbitrary size, based on an ArrayBuffer instance of particularly small size.
This is a remarkably elegant bug that takes you directly to relative full memory control.
Exploitation
So, we have an Array object which spans the entire 32bit memory space, enabling us read/write access.
In order to leverage this into code execution we'll need to accomplish two objectives:
- Discover the location of our Array object in memory, in order to upgrade our relative r/w into full blown absolute memory r/w.
- Control EIP: Find a function pointer to corrupt, preferably a vtable of an object we can control.
These two steps are somewhat trivial in other browsers, but Chrome follows several security oriented design principles which make exploitation much more difficult.
PartitionAlloc
PartitionAlloc is a feature of Chromium's WebKit fork – Blink.
It serves the purpose of creating memory sterility by partitioning heap allocations according to their purpose and nature – it avoids juxtaposing metadata or control data with buffer and user-input data, which is rightfully perceived to be more vulnerable.
There are 4 partitions:
- Object Model Partition (Element objects etc)
- Renderer Partition
- Buffer Partition (Where an ArrayBuffer or a string would be allocated)
- General Partition
PartitionAlloc maintains several allocation "entities", from small to large – buckets, superpages and extents.
Super-pages are the building blocks of a partition, and are 0x200000 bytes in size each; an extent describes a sequence of super-pages.
A partition is composed of one or more extents.
Each super-page is transparently divided into buckets which are selected according to the size of a requested allocation by "order" and size.
On top of that, each super page has "guard" areas, which prevent an attacker from sequentially reading/writing/overflowing memory:
Specifically, a super-page comprises a metadata page, an actual data area (0x1f8000 bytes in length) and several guard areas (reserved, inaccessible pages).
What this means for us is that even though we have full relative read, we can't just go around reading memory freely, since our faulty ArrayBuffer will be located inside one of these 0x1f80000 byte areas surrounded by reserved pages.
Worse than that, since PartitionAlloc is well implemented and enforced throughout the project - we are unable to allocate any object with a vtable in our proximity.
Basing our buffer
So, we know we have an array object located somewhere in memory, from which we can read the entire memory space relative to our buffer, but since we don't know its base, we can't convert this into absolute r/w.
The solution we came up within order to overcome this, given PartitionAlloc's obstacles, was to create an object in near vicinity to our faulty buffer, and then spray a significant amount of items which point back to this object.
Both the single object and the sprayed pointers to it would have to be allocated in the buffer partition, same as our faulty array.
This is done by creating a simple string with similar size to our faulty ArrayBuffer, thus placing it in the same or adjacent bucket, and then spraying a moderate amount of attribute objects, with different names but the same value – our string.
The purpose of this is to get Chrome to allocate a few more super-pages, directly following the super-page we're in, and read a pointer to the string adjacent to us from there.
Crudely depicted as follows:
By blindly reading 0x200000 bytes ahead (super-page size), we can read the attribute pairs created and heuristically infer the absolute address of the attribute we created.
Then, by scanning the memory near us, we can attempt to infer the relative offset of the attribute string from our array buffer. Combining the absolute address of the attribute string with its distance relative to us allows us to calculate our address in memory, completing objective #1.
This is the code that achieves the condition described and resolves the location of the corrupted buffer, allowing us to set up absolute r/w abstracts.
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
spray_array = new Array(0x1000); elements = new Array(0x250); for (var i = 0 ; i < spray_array.length ; i++) { if (i == 0x500) { alert("allocating vuln object") faulty_arr_buf = new ArrayBuffer(0x1f8); attribute_string = unescape("%43%43%43%...43%43%43%43").substr(0, 0x1e0); } spray_array[i] = new Uint32Array(0x1f8/4); for (var j = 0; j < spray_array[i].length ; j++ ) { spray_array[i][j] = 0x41414141; } } for (var i = 0 ; i < elements.length ; i++) { elements[i] = document.createElement("div"); for (var j = 0 ; j < 0x100 ; j ++) { elements[i].setAttribute("elem" + j, attribute_string); } } var address_list = {}; for (var i = 0 ; i < 0x20 ; i++) { if ((faulty_arr[(0x200000 - 0x1f8)/4 + i].toString(16) in address_list) == false) { address_list[faulty_arr[(0x200000 - 0x1f8)/4 + i].toString(16)] = 0; } address_list[faulty_arr[(0x200000 - 0x1f8)/4 + i].toString(16)] += 1; } max_val = 0; max_key = NaN; for (var key in address_list) { if (address_list[key] > max_val) { max_val = address_list[key]; max_key = key } } /* so now we have our "CCCCCC" buffer address */ string_address = parseInt("0x" + max_key); var string_start_index = 0; /* scan for the relative offset of the string (i is the amount of dwords forward) */ for (i = 0 ; i < 0xFFFFFFFC/4 ; i++) { if (faulty_arr[(0x1f8/4) + i] == 0x43434343) { string_start_index = i; break; } } /* Compensate for the buffer length (0x1f8) and the additional headers (3 dwords) */ string_start_index -= (-0x1f8 + 12) / 4 /* Now use the absolute string address to calculate the absolute address of our buffer */ faulty_arr_buf_addr = (string_address - string_start_index*4); |
Gaining execution
Our next objective is to gain execution by overriding a vtable for some object.
We actually broke this objective into two subtasks – leaking a pointer and overriding a vtable.
We've already established that we'll have difficulty finding a pointer in our partition.
Ironically, the metadata page of the partition itself solves this issue.
Since we know how partitions are formed in Chrome 33, we can apply some more heuristic logic to bring us to the metadata page of our partition (exactly 1 page up from the super-page base), which happens to contain a few bucket pointers:
Since we don't know exactly where we are relative to the super-pages base (not exactly accurate since we already know our absolute location), we resort to scanning backwards at 0x10000 intervals, plus a constant 0x1028 offset.
We know when we've found a bucket pointer once we find a value which repeats itself 0x20 ahead several times.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var module_value = 0; for ( var i = 1 ; i < 0x1f ; i++ ) { var allocator_bucket_value = readDWORD(((faulty_arr_buf_addr&0xFFFF0000) + 0x1000 + 0x28) - i*0x10000) if ((allocator_bucket_value < 0x01000000) || (allocator_bucket_value == 0x41414141)) { /* if the value is either smaller than 0x01000000 (unlikely chrome_base will get mapped there) or our string from before, continue */ continue; } /* found candidate for base */ /* for added confidence */ var next_allocator_bucket_value = readDWORD(((faulty_arr_buf_addr&0xFFFF0000) + 0x1000 + 0x28 + 0x20) - i*0x10000) if ((allocator_bucket_value & 0xFFFF0000) == (next_allocator_bucket_value & 0xFFFF0000)) { module_value = allocator_bucket_value; break; } } |
At this point we've accomplished half of our second objective, but this doesn't really help us gain code execution.
The approach we took from here on was to simply spray a large amount (400MBs or so) HTMLDivElement objects, which we then tried to overwrite by accessing a constant address.
This sort of makes the previous leak redundant, but we decided to show it anyway since it opens up a few interesting options for exploitation. We'll discuss those in a moment.
Naturally this has a lot of disadvantages, chief among which is the fact that these Divs would be allocated in the DOM partition, and partition bases are randomized, reducing our chances of success somewhat.
Maneuvering around the Chrome memory space
A more sophisticated approach exists, but unfortunately it relies on changes that were only introduced after this bug was fixed.
We tested this method by artificially creating the same corrupt ArrayBuffer condition in a newer version of Chrome (36 was the most recent at the time) using a debugger.
It involves reliance on an interesting detail of the PartitionAlloc structures, namely the invertedSelf member which exists in the PartitionRootBase struct:
Being a static struct, the PartitionRootBase for each partition would be located in chrome_child's data section.
Since we have arbitrary r/w access and a pointer to chrome_child's base, we can definitely find these structures by once again applying a simple heuristic.
The invertedSelf field is located at offset 0x68, so any value which corresponds to ~value == &value – 0x68 will highly likely be a PartitionRootBase struct.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function findChromeAllocators(chrome_base) { data_sec = getDataSectionLocation(chrome_base) var size_index = 0; var data_base = data_sec.base; var match_list = []; while (size_index < data_sec.size - 0x68) { cdword = readDWORD(data_base + size_index) if ((cdword ^ 0xFFFFFFFF) == (data_base + size_index - 0x68)) { match_list.push(data_base + size_index - 0x68) } size_index += 4 } return match_list; } |
Indeed, this function returns exactly 4 matches, one per PartitionRootBase.
Finding the Object Model Partition can then be accomplished by creating a moderate amount of Divs, enough to fill one super-page of its partition with HTMLDivElement objects, and scanning each partitions' currentExtent looking for the pattern created by these 0x34 byte size objects.
Once we find the HTMLDivElement, it’s a simple matter of overriding a vtable and iterating over all the Divs we allocated and calling a specific method.
Using this method it's possible to achieve fairly high reliability with a very small memory footprint.
Conclusion
All in all, exploitation in Chrome is very challenging. Even once all of this has been accomplished, you still need to employ a sandbox bypass in order to achieve full exploitation.
In addition, some of the methods described here are no longer relevant. Specifically, it seems that the Chromium developers have added an additional layer of protection by fragmenting super-pages into writable and reserved pages even within the 0x1f8000 byte area, making resolving our buffers address even harder.
Google Chrome incorporates security considerations in the design of the browser, and significantly so, to a very impressive degree.
Unfortunately, not all software vendors align themselves to the Chromium project's standards. Palo Alto Networks Traps acts to prevent several types of attacks, even those that may circumvent or overcome mechanisms such as in Chrome, fortifying existing defenses while adding significant additional mitigation mechanisms. Learn more about Advanced Endpoint Protection here.