About two weeks ago a blog post was issued from the Windows Offensive Security Research Team (OSR) about hardening Windows 10 against kernel exploitation:
Kernel exploitation on Windows 10 almost always requires a kernel read and/or write primitive. So OSR reported on how Windows 10 Anniversary Update has been hardened to mitigate usage of one of the commonly used primitives. The primitive in question is comes from the tagWND object, which is the kernel mode representation of a window. While reading the blog post, I remembered back to October of last year and some research I did at the time. About two weeks before Black Hat Europe 2016, I was looking into using the tagWND object as a read/write primitive on exactly Windows 10 Anniversary Update. But before I got around to writing up anything about my findings the Attacking Windows By Windows talk from Yin Liang and Zhou Li came out at Black Hat Europe:
So I quit the idea of doing a write-up, since I had essentially the same findings as them. However after reading the blog post by OSR it dawned on me, that the talk by Yin Liang and Zhou Li must have been performed on the 1511 build, without the new mitigations. However when I did my own research I ran into some annoying pointer verifications but found a way around them, and did not think about it at the time. Now I realize that these pointer verifications were exactly the hardening mitigations OSR put in place, and that they can be quite easily bypassed, bringing back the usage of the read/write primitive.
This blog post goes through the process of looking at that hardening and what the problem of it is, and how to enable a bypass. The following analysis was performed on the December 2016 build for the Creators Update of Windows 10.
The Original PoC
I will reuse the concepts from the Black Hat Europe talk, so if you have not read that I suggest you look it over now. The essence of the talk is that in the case of a write-what-where vulnerability, the cbwndExtra field of the tagWND structure may be increased and allow for precision overwrite of subsequent memory. Thus if two tagWND objects are placed next to each other, overwriting the cbwndExtra field of the first tagWND object, may allow the exploit to modify fields of the next tagWND object. Amongst these is the strName, which contains a pointer to where the name of the Window is located, changing this allows the exploit to read and write from anywhere in kernel memory. The following code snippet shows how this may be done using SetWindowLongPtr and NtUserDefSetText:
This creates a new LargeUnicodeString object and tries to write the contents of it at an arbitrary address. The SetWindowLongPtr calls are used to change the pointer to the name of the Window, and then restoring it again. While this worked on all builds before Anniversary Update, it now causes a bugcheck with the following callstack:
This is exactly as described in the blog post by OSR.
To understand why the bugcheck is caused, I started debugging the flow from the function DefSetText. When the function is entered we have the address of the tagWND object in RCX and a pointer to the new LargeUnicodeString object in RDX. The first part of the verification is shown below:
This just makes sure that the tagWND object and the new LargeUnicodeString object are correctly formatted. A bit further down a function appears:
The DesktopVerifyHeapLargeUnicodeString is one of the new hardening functions. It takes the address to use for the LargeUnicodeString, this contains the pointer we changed through the SetWindowLongPtr call. And a pointer to the tagDESKTOP structure for the Desktop the tagWND object is used on. The first part of the new function is verification that the string lengths are correctly formatted and does not have odd lengths, since they are supposed to be Unicode strings:
Then a check is performed to make sure that the length of the LargeUnicodeCode string is not negative:
Then the function DesktopVerifyHeapPointer is called given the pointer to the tagDESKTOP as argument, while remembering that RDX already contains the buffer address. What happens next is what causes the bugcheck. Offset 0x78 and 0x80 in the tagDESKTOP object is dereferenced, which is the base address of the Desktop Heap and the size, this is compared to the address of where we are trying to write the LargeUnicodeString. If that address does not lie within the Desktop Heap a bugcheck is caused. This is the hardening OSR is talking about. It may be seen below:
It is clear that the write primitive no longer works, unless it is used inside the Desktop Heap, which has limited usage.
A New Hope
Not all is lost, the address of the Desktop Heap and its size comes from the tagDESKTOP object. However no verification is ever performed that the pointer to the tagDESKTOP object is correct. So if we create a fake tagDESKTOP object and replace the original one with ours, then we control the offsets 0x78 and 0x80. Since the pointer to the tagDESKTOP is taken from the tagWND structure we may also modify it using the SetWindowLongPtr API. Below is shown the updated function:
Where g_fakeDesktop is allocated at address 0x2a000000 in usermode. This is possible since Windows 10 does not employ SMAP, however even if it did, we could place it in the Desktop Heap instead since the primitive still allows us to write there. Running the updated Proof of concept makes sure the check is passed and returns to the following code snippit:
So another call is made to the same verification function, still with tagDESKTOP as the first argument and now the buffer pointer plus max string length minus one as the second argument instead of the start of the string buffer. This check is also passed and the execution returns to DefSetText.
When we continue execution we cause a new bugcheck with the following call stack:
This is due to the following instruction:
Since R9 contains 0x1111111111111111 it is quite clear that it is populated from the fake tagDESKTOP object. Looking in Ida Pro we find:
Which shows that the content in R9 does indeed come from the tagDESKTOP pointer, and is the second QWORD. So we may update the code to also set this to value. If it is set to 0 the dereference is even bypassed. Running with this update does not result in a crash and show us the following arbitrary overwrite:
So in conclusion it is clear that OSR did harden against use of the primitive, but also that it was no quite enough to stop its usage. The same tatic may also be employed to reenable the usage of InternalGetWindowText as a read primitive.
The code shown above may be found at the following Github: