Deconstructing a Winning Webkit Pwn2Own EntryAugust 24, 2017 | Jasiel Spelman
Let's start by looking at what happens if you run the proof-of-concept on a vulnerable version:
The proof-of-concept is pretty small, so let's look at it:
Looking at the proof of concept, the faulting address is 0x414141414145 and $r14 is 0x0000414141414140, but where is that value coming from? Within arr1, the value 3.5448480588962e-310 is used a couple times. This is an 8-byte double representing the value 0x414141414140.
The crash occurs within operationValueAdd, and the only time any addition is performed occurs within the bar function. If we look at that line, there is a negative index into the array that is used. That function itself is pretty innocuous, which makes sense since it will just operate on the values that are passed to it. Let’s instead look at some of the code that handles the array bounds check optimizations, as that will affect what ends up being sent to operationValueAdd.
The handleBlock function is bit unwieldly. I've only shown a small snippet of it here, but the gist is that nodes are visited to try to determine if the bounds of array accesses can be safely eliminated. As each node is visited, a range of minimum and maximum values is stored. When possible, this range of minimum and maximum values is hoisted to a node higher in the syntax tree, then finally, integer check nodes are removed when redundant. This is a fairly typical compiler example. Let’s say we are accessing an array at indexes four and five. We can then just check to make sure the array size is greater than five rather than also having to check to see if it is greater than four.
Let's look at this method again, but this time, we’ll look more closely at what happens before node elimination:
We are specifying an integer constant, so we end up entering the if block instead of the else block. As a result, minNode will get set to zero and will effectively be ignored, while maxNode is set to the largest value in the range. If you look at the else block, you'll see that minNode is set to a value that references range.m_minBound. Later, this will be used to insert a CheckInBounds node as well. This means that if an integer constant is provided that is negative along with another integer constant that is positive, a check to ensure it is within bounds will not occur.
Unfortunately, there is an issue even if the bounds check occurred. If the integer was to be treated as an unsigned integer during the comparison to the array size, this would be fine. However, the comparison is performed as a signed comparison. For example, here's how the FTL JIT handles CheckInBounds nodes:
This function will treat the values as signed integers and will only perform a signed integer comparison to ensure that an OutOfBounds exit will occur if the value in child1 is equal to or larger than child2. Going back to the bug, this means that a function that enters the DFG JIT with integer constant accesses into an array will not properly check bounds. This allows a negative value to access data before the bounds of the buffer being indexed. The end result is the ability to read and write arbitrary values to preceding buffers, resulting in code execution within the WebKit sandbox.
Apple patched this with commit f2476d46820b744450133f6b00a85e5265db1915 in the repository, which modified dfg/DFGIntegerCheckCombiningPhase.cpp. The patch issues a check against the minimum bounds such that if it is found to be negative, the JIT engine will force an exit from the DFG JIT to a lower layer where more checks will be performed on accesses.