
翻译文章,CVE-2015-0057漏洞在32位和64位系统上的利用。Exploiting the win32k!xxxEnableWndSBArrows use-after-free (CVE 2015-0057) bug on both 32-bit and 64-bit(Aaron Adams of NCC )
Author: Aaron Adams
Translation: 55-AA
Translator's Note: Some parts of this article are freely translated. Please refer to the original text for any questions.
Terminology:
Earlier this year, I encountered an interesting vulnerability in win32k.sys (CVE-2015-0057) and achieved stable exploitation on both 32-bit and 64-bit systems, ranging from XP to Windows 8.1 (with some exceptions). This article details how I accomplished the exploitation on these two platforms, with some additional content at the end. It also describes how to achieve exploitation at low integrity level on Windows 8.1 with SMEP enabled.
This article is quite long. I have tried to provide as many details as possible to show the complexity of exploiting this vulnerability, rather than hiding them. Of course, I have also omitted some details. I hope these details are helpful to everyone.
On February 10, 2015, Microsoft disclosed the details of MS15-010. This bug was first discovered by Udi Yavo from enSilo. Udi gave an excellent analysis on the breaking malware blog: "one bit rule-bypassing windows 10 protections using single bit". I recommend reading this article carefully to understand the bug deeply, although I will provide as many details as possible in this article, covering some obstacles that must be overcome when triggering the vulnerability. The exploitation of this vulnerability is very interesting. Many details come from Udi's blog. Here is his statement:
Reasonable disclosure: Although this blog is technical, we will not disclose any code or complete details to prevent any technical expert from reproducing this exploit.
As an additional reward for exploiting this vulnerability, we get a Pokémon evolution: Tech Pokémon. I think I need to give Udi some credit for discovering this bug and providing relevant information and details on the blog, which were extremely useful.
Before this, I had never exploited a win32k.sys vulnerability, nor was I familiar with user-mode callbacks and many related APIs. Therefore, I also thank some well-known security researchers for providing novel resources on the internet, such as Skywing, Tarjei Mandt, Alex Ionescu, and j00ru. These people have provided so much technical information publicly; they all deserve praise. I heavily referenced an article by Tarjei Mandt: Win32k.sys exploitation paper.
While I was writing this exploit, an excellent reverse engineer achieved stable exploitation of CVE-2015-1701. The example code for user-mode callbacks was very useful. Thanks to the author.
It is worth noting that my analysis below was done on Windows 7 because it seems to be the only version where all structures in win32k.sys have corresponding symbols. Most of these symbols are applicable to structures in other versions of win32k.sys. For some reason, Microsoft removed these symbols starting from Windows 8.
Finally, I want to say that my exploitation method is quite complex. It is entirely possible that there is an easier way to do it; I just haven't found it. I would love to hear about different methods used by others. Either way, I hope all of this is helpful for researching win32k.sys vulnerabilities.
Let's take a look at the bug in the disassembly of win32k!xxxEnableWndSBArrows. This is a rather subtle bug:
Unpatched version:
.text:FFFFF97FFF1B157D mov r8d, r13d
.text:FFFFF97FFF1B1580 mov rdx, r14
.text:FFFFF97FFF1B1583 call xxxDrawScrollBar ; triggers usermode callback
.text:FFFFF97FFF1B1588 jmp short loc_FFFFF97FFF1B1519
[...]
.text:FFFFF97FFF1B1519 mov eax, [rbx] ; references tagSBINFO pointer without checking
.text:FFFFF97FFF1B151B mov ebp, 0FFFFFFFBh
.text:FFFFF97FFF1B1520 xor eax, esi
In the code above, Win32K!xxxDrawScrollBar can call back to user space under appropriate circumstances. In user space code, the tagSBINFO pointer may be freed by the attacker. When returning to the code above, the instruction at 0xFFFFF97FFF1B1519 will reference an invalid pointer.
Patched version:
.text:FFFFF97FFF1D69C3 xor r8d, r8d
.text:FFFFF97FFF1D69C6 mov rdx, rbp
.text:FFFFF97FFF1D69C9 call xxxDrawScrollBar ; triggers usermode callback
.text:FFFFF97FFF1D69CE cmp rbx, [rdi+0B0h] ; checks if tagSBINFO pointer is correct
---.text:FFFFF97FFF1D69D5 jz short loc_FFFFF97FFF1D69E4; if correct, continue original flow
| .text:FFFFF97FFF1D69D7 mov rcx, rbp
| .text:FFFFF97FFF1D69DA call _ReleaseDC
| .text:FFFFF97FFF1D69DF jmp loc_FFFFF97FFF1D6958 ; jump to function exit
|->.text:FFFFF97FFF1D69E4 mov eax, [rbx] ; safely use correct tagSBINFO pointer
.text:FFFFF97FFF1D69E6 xor eax, r14d
In the patched version above, we see a null check before using the tagSBINFO pointer. Relevant structure information will be provided later.
In implementing this exploit, we performed multiple corruptions. The vulnerability was triggered during one of these corruptions.
The technical root cause of this bug is a use-after-free (UAF) on the desktop heap. Initially, this confused me because I was unfamiliar with the user-mode callback mechanism in win32k.sys and how it works. Therefore, I thought it was a lock race condition leading to UAF. In fact, the locks on that structure were correctly used, and the flow was as expected. In short, the real reason is:
That's it. Without considering user-mode callbacks, this stage is quite straightforward.
But how do we perform the corruption, and why? As mentioned in Udi's blog, you can set or clear 2 bits in a location that the system code considers the WSBflags field in the tagSBINFO structure. This is not a standard UAF exploitation approach, but the article gave a hint on how to do it, which I will explain in the following sections. First, let's understand how to manipulate these bits.
tagSBINFO structure (consistent on 32-bit and 64-bit):
kd> dt -b !tagSBINFO
win32k!tagSBINFO
+0x000 WSBflags : Int4B
+0x004 Horz : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
+0x014 Vert : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
The UAF vulnerability is in win32k!xxxEnableWndSBArrows(), which enables or disables the arrows of one or both (horizontal or vertical) scrollbar controls. A scrollbar control is a special window used to manipulate scrollbars. It can be created using CreateWindow() with the built-in "SCROLLBAR" window class.
Function prototype of win32k!xxxEnableWndSBArrows():
BOOL xxxEnableWndSBArrows(PWND wnd, UINT WSBflags, UINT wArrows);
The parameter WSBflags has the same meaning as defined in WinUser.h, indicating which scrollbars will be manipulated:
#define SB_HORZ 0
#define SB_VERT 1
#define SB_CTL 2
#define SB_BOTH 3
The parameter wArrows indicates whether the arrows are enabled or disabled. Bits set mean arrows are disabled, otherwise enabled. The lowest two bits of wArrows represent the horizontal scrollbar, the next two bits represent the vertical scrollbar, and the remaining bits are irrelevant for this exploit's purpose.
The following code is taken from win32k!xxxEnableWndSBArrows(). If SB_HORZ or SB_BOTH is set, it sets or clears the relevant bits for the horizontal arrow:

The bug exists when setting the flags for the horizontal and vertical scrollbars. After refreshing the horizontal scrollbar, once the corresponding window is visible on the desktop, win32k!xxxEnableWndSBArrows() calls win32k!xxxDrawScrollBar(), which, as mentioned earlier, may trigger a potential user-mode callback.
Before we discuss the user-mode callback, let's continue with what happens after calling win32k!xxxDrawScrollBar(). This is actually the same logic as for the horizontal scrollbar, with just a few bit differences. If we choose to disable the vertical scrollbar and assume we trigger the UAF, then 2 bits will be written to some location in the tagSBINFO heap block. Therefore, if the original value was 0x2, it becomes 0xe. As shown in the figure below.

This bit change is enough to lead to eventual code execution. I have not explored how to achieve exploitation by clearing bits, but it is possible.
The main point above is that to manipulate both horizontal and vertical scrollbars simultaneously, you must create a scrollbar control with both elements. This is done by calling CreateWindow() with the WS_HSCROLL and WS_VSCROLL flags. The code is as follows:
g_hSBCtl = CreateWindowEx(
0, // No extended style
"SCROLLBAR", // class
NULL, // name
SBS_HORZ | WS_HSCROLL | WS_VSCROLL, // vertical + horizontal
10, // x
10, // y
100, // width
100, // height
g_hSpray[UAFWND], // modeless parent window
(HMENU)NULL,
NULL, // window owner
NULL // extra params
);
You can ensure it is visible with the following code (usually default, but explicitly called here):
result = ShowWindow(g_hSBCtl, SW_SHOW);
Scrollbars are enabled by default. When we are ready to trigger the vulnerability code, we can disable the scrollbars to corrupt the bits we need:
result = EnableScrollBar(g_hSBCtl, SB_CTL | SB_BOTH, ESB_DISABLE_BOTH);
Although we described the bug details and how to trigger the relevant code above, we still skipped the most important step: intercepting the user-mode callback initiated by win32k!xxxDrawScrollBar() so that we can change the heap content before win32k!xxxEnableWndSBArrows() continues execution. We need to actually trigger the bug, but without any knowledge of win32k.sys and related APIs, as I was at the beginning, this becomes an adventure for ourselves.
Previous articles provide a good call stack diagram showing the process in depth: triggering through win32k!xxxDrawScrollBar(), then ClientLoadLibrary() is called, and dispatched through KeUserModeCallback(). We need to understand the call to KeUserModeCallback() precisely to hook it in our own process.
I found some good papers that more or less cover user-mode callbacks. The parts related to win32k are very useful:
Typically, each process has a table of user-mode callback function pointers, pointed to by PEB->KernelCallBackTable. When the kernel wants to call a user-mode function, it passes a function index to KeUserModeCallBack(). In the example above, the index points to the user-mode __ClientLoadLibrary() function.
KeUserModeCallBack() looks up the function at the index in PEB->KernelCallBackTable and executes it, eventually calling KiUserModeCallbackDispatch() in user mode.
To hook a specific entry point, you should find the index of __ClientLoadLibrary() in PEB->KernelCallBackTable and replace it with your own function. Note that this index varies by operating system version and hardware platform.
If we want to view PEB->KernelCallBackTable, we can find the address of this table using WinDbg. Comparing 32-bit and 64-bit platforms, there are no significant differences.
kd> dt !_PEB @$peb
ntdll!_PEB
+0x000 InheritedAddressSpace : 0 ''
+0x001 ReadImageFileExecOptions : 0 ''
+0x002 BeingDebugged : 0 ''
+0x003 BitField : 0x8 ''
+0x003 ImageUsesLargePages : 0y0
[...]
+0x02c KernelCallbackTable : 0x76daf620 Void
kd> dds 0x76daf620
76daf620 76d96443 user32!__fnCOPYDATA
76daf624 76ddf0e4 user32!__fnCOPYGLOBALDATA
76daf628 76da736b user32!__fnDWORD
76daf62c 76d9d603 user32!__fnNCDESTROY
76daf630 76dc50f9 user32!__fnDWORDOPTINLPMSG
76daf634 76ddf1be user32!__fnINOUTDRAG
76daf638 76dc6cd0 user32!__fnGETTEXTLENGTHS
76daf63c 76ddf412 user32!__fnINCNTOUTSTRING
76daf640 76d9ce49 user32!__fnINCNTOUTSTRINGNULL
[...]
76daf724 76da3962 user32!__ClientLoadLibrary
kd> ?? (0x76daf724-0x76daf620)/4 int 0n65
In the example above, we know the index of __ClientLoadLibrary is 65, which is what we will hook. After hooking, I found that __ClientLoadLibrary is called many times by win32k-related code! First, we need to notify our hook code when the call we are interested in occurs, so we know we have hooked the right place. Therefore, the hook code uses a global variable flag that only performs the relevant operation when set.
Now there are two obstacles:
Thus, the hook function looks like the following:
void ClientLoadLibraryHook(void * p)
{
CHAR Buf[PGSZ];
memset(Buf, 0, sizeof(Buf));
if (g_PwnFlag)
{
dprintf("[+] __ClientLoadLibrary hook called\n");
if (++g_HookCount == 2)
{
g_PwnFlag = 0; // execute only once..
ReplaceScrollBarChunk(NULL);
}
}
fpClientLoadLibrary(&Buf); // call original function
}
Once we confirm we are called from win32k!xxxDrawScrollBar(), we can try to trigger the bug. At this point, we only need to call DestroyWindow(g_hSBCtl). This will cause the window's tagSBINFO structure to be freed, but the window structure itself is not immediately freed because its reference count is still held by the original call. However, tagSBINFO has no such reference counting mechanism, so it is freed immediately.
At this point, we have triggered the bug. Although we have not reallocated a heap block containing tagSBINFO, we can still write the two bits indicating "disable" onto the freed heap. The next step is to replace this freed heap block with something we control, so we can do something more interesting than just setting a few bits. To do this, we need some background on the desktop heap.
win32k.sys uses the desktop heap to store GUI objects related to a given desktop. This includes window objects and their associated structures, such as property lists, window text, and scrollbars. Tarjei's article mentions this, but it is important to note that the desktop heap is essentially a simplified version of a user-mode back-end allocator, using RtlAllocateHeap() and RtlHeapFree() for operations. The desktop heap is maintained by a _HEAP structure, and since there is no front-end allocator, there are no Low Fragmentation Heap (LFH) or lookaside lists, etc.
Each created desktop has a corresponding desktop heap serving it. This means we can allocate a new desktop to get a "clean" desktop heap where our operations are more predictable. However, this is meaningless for processes with low integrity, as such processes are not allowed to create a new desktop.
The main issue now is tracking the allocation process (more details on metadata etc. will be covered later).
To monitor desktop heap allocations and deallocations, I used WinDbg scripts:
64-bit heap monitoring
ba e 1 nt!RtlFreeHeap ".printf\"RtlFreeHeap(%p, 0x%x, %p)\", @rcx, @edx, @r8; .echo ; gc";
ba e 1 nt!RtlAllocateHeap "r @$t2 = @r8; r @$t3 = @rcx; gu; .printf \"RtlAllocateHeap(%p, 0x%x):\", @$t3, @$t2; r @rax; gc";
32-bit heap monitoring
ba e 1 nt!RtlAllocateHeap "r @$t2 = poi(@esp+c); r @$t3 = poi(@esp+4); gu; .printf \"RtlAllocateHeap(%p, 0x%x):\", @$t3, @$t2; r @eax; gc";
ba e 1 nt!RtlFreeHeap ".printf\"RtlFreeHeap(%p, 0x%x, %p)\", poi(@esp+4), poi(@esp+8), poi(@esp+c); .echo ; gc"
In addition to these debug scripts, since the desktop heap is just a simplified form of a user-mode back-end allocator, we can also use the built-in !heap command in WinDbg.
To exploit this bug, we need to replace the recently freed tagSBINFO heap block, and we also know how to use these typical bugs, which is to corrupt adjacent data. This basic requirement is to pre-allocate some heap blocks near the structure we want to corrupt. To predict where a heap block will be allocated, we must control the entire heap layout (or as much as possible). To meet this, a feasible method is to allocate as many heap blocks as possible to fill the holes left by freed blocks, so that newly allocated blocks are contiguous. When we need a hole, we can dig one (by freeing an allocated block) at a predictable location.
This part is a simple understanding of the factors affecting allocation. The WinDbg scripts above can help us. Tarjei mentioned in his win32k PPT the main objects allocated on the desktop heap, which matches what I observed. These are:
The desktop heap is quite interesting. Most allocations are directly associated with window objects and managed through the tagWND structure, meaning if we want to allocate a heap block of arbitrary size (so-called "small block for small hole"), we must first allocate a window related to it. You can think of the window structure as the interface for heap allocation. Another interesting point is that many heap blocks allocated via window operations cannot be freed immediately unless the window itself is destroyed, which obviously affects the heap. Finally, let's assume we allocate a heap block of size N through a window, as in the example above. If we want to allocate many heap blocks of size N, we can be sure that the allocated window structures, regardless of size, are not stored in a linked list. Therefore, each window can control one heap allocation of size N. That is, if you need to allocate many heap blocks of size N, you must first create many windows, using the windows to assist heap allocation.
There are three other important data types allocated on the desktop heap that we can indirectly use through window objects to control heap data. We extensively used these data types to implement exploitation and construct heap spray. These three data types are:
Figure 2 shows the relationship between these data types:

To initialize the heap, I created a large number of tagWND structures (by creating window objects). This filled many large holes in the heap and provided an interface for allocating other heap blocks we need. On Win8 and Win8.1, allocating a new window causes a tagPROPLIST structure to be automatically allocated (can be observed via the WinDbg script mentioned earlier). On Win7 and earlier, we allocate a new tagPROPLIST ourselves to fill small holes.
Here, all the window objects we spray do not have window text strings. However, if needed, we can still use it to allocate or free heap blocks of arbitrary size. Once created, you cannot remove an existing property list unless the window is destroyed, but we can control the reallocation of this list to accommodate new properties. This mechanism can be used to create holes in previous locations. All you need to do is set a new property (distinguished by atomKey) that does not exist in the original list.
Interestingly, the desktop heap is mapped into user space, albeit read-only. This means we can verify the spray layout we constructed and ensure it works correctly. First, we need to determine where the desktop heap is mapped in user space. This is mentioned in Tarjei's paper on win32k. There is an undocumented structure Win32ClientInfo in the TEB related to this. Its approximate definition is:
typedef struct _CLIENTINFO {
ULONG_PTR CI_flags;
ULONG_PTR cSpins;
DWORD dwExpWinVer;
DWORD dwCompatFlags;
DWORD dwCompatFlags2;
DWORD dwTIFlags;
PDESKTOPINFO pDeskInfo;
ULONG_PTR ulClientDelta; // incomplete. See reactos
} CLIENTINFO, *PCLIENTINFO;
The PDESKTOPINFO structure is defined as:
typedef struct _DESKTOPINFO {
PVOID pvDesktopBase;
PVOID pvDesktopLimit; // incomplete. See reactos
} DESKTOPINFO, *PDESKTOPINFO;
The first field pvDesktopBase points to the kernel-mode address of the desktop heap, which we note. The ulClientDelta field in Win32ClientInfo is the difference between the kernel-mode address and the user-mode address. With this information, we can get what we want.
However, instead of parsing the heap structure ourselves, we want to have a user32 handle, like the value of an HWND, which can be converted to a user-mode mapped address, so we can determine if it is associated with other heap allocations. To find this handle, we need a structure called gShared, usually located in user32.dll. On Win7 and later, it is exported, so it can be easily found.
On most systems, this structure is defined as:
kd> dt !tagSHAREDINFO
win32k!tagSHAREDINFO
+0x000 psi : Ptr32 tagSERVERINFO
+0x004 aheList : Ptr32 _HANDLEENTRY
+0x008 HeEntrySize : Uint4B
+0x00c pDispInfo : Ptr32 tagDISPLAYINFO
+0x010 ulSharedDelta : Uint4B
+0x014 awmControl : [31] _WNDMSG
+0x10c DefWindowMsgs : _WNDMSG
+0x114 DefWindowSpecMsgs : _WNDMSG
In the structure above, aheList points to an array of _HANDLEENTRY, each containing a handle pointing to a kernel-mode address. We can use the "difference between kernel-mode and user-mode addresses" to get a usable user-mode address. Unfortunately, this is not possible before Win7 because gSharedInfo is not exported. Tarjei's article says the undocumented function CsrClientConnectToServer can be used to obtain a copy of gSharedInfo, but I haven't found a working example. Annoyingly, the structure needed by this function varies in size across systems, so based on my experience, you cannot fully trust what you see in ReactOS.
Once we calculate the mapped location, we can construct a function that tells us the location of a window object on the desktop heap. Then, if we want to know where the heap block for the corresponding property list or text string is allocated, we only need to parse the user-mode structure.
Now we are finally approaching the exploitation of this vulnerability. We already have methods to control heap blocks, verify the correctness of heap block positions, and trigger the bug. Therefore, we can finally replace the freed tagSBINFO heap block with a chosen tagPROPLIST property list. Note that since tagPROPLIST is just the head of a larger list, we can make the list size match the scrollbar heap block size. The part after tagPROPLIST is essentially an array of tagPROP structures, or property list; therefore, I will not distinguish between the array and the list. The tagPROPLIST structure on 64-bit systems is:
kd> dt -b !tagPROPLIST
win32k!tagPROPLIST
+0x000 cEntries : Uint4B
+0x004 iFirstFree : Uint4B
+0x008 aprop : tagPROP
+0x000 hData : Ptr64
+0x008 atomKey : Uint2B
+0x00a fs : Uint2B
As mentioned earlier, window objects have an associated property list. This list is created via SetProp(). It looks for an existing property by matching atomKey. If the property does not exist, a new property entry is created in the property list. If there is no property list at all, a property list is created and attached to the tagWND structure.
If we have sprayed a buggy tagWND and created associated tagPROPLIST entries, the final layout is as shown in Figure 3:

Once this is set up, we can allocate the scrollbar control we want to exploit. This will result in the situation shown in Figure 4:

Then we manipulate the scrollbar to trigger the user-mode callback hook. In the hook function, we try to free the tagSBINFO structure by destroying the window. This leads to the situation in Figure 5:

On 64-bit, the tagSBINFO structure is 0x28 bytes, and a tagPROPLIST array entry is 0x18 bytes, with a default tagPROP of 0x10 bytes. Therefore, a property list with two array entries is 0x28 bytes (0x8 + 0x10 + 0x10), which is a perfect fit. Assuming we have sprayed memory to fill the hole, we simply need a window with a property list. Immediately after freeing the tagSBINFO structure (as shown in the previous figure), we add a new property list entry. This process frees the previous 0x18-byte tagPROPLIST heap block. Since the heap has been sprayed, there are no free heap blocks nearby, so no heap block coalescing occurs, and there is not enough space to accommodate the newly allocated 0x28 bytes. Thus, the just-freed tagSBINFO location is used (its size is exactly 0x28 bytes). This situation is shown in Figure 6:

After returning from our hooked callback function, the UAF is triggered, and a few bits are written to the cEntries field in tagPROPLIST. The original cEntries value was 0x2, indicating we created two property list entries. After the overflow, it becomes 0xe, with bits 3 and 4 (counting from 1) set to 1.
At this point, we have completed the new heap overflow, increasing the number of entries in this property list to greater than 0xc. Next, we will overflow adjacent heap blocks in what we call Phase 2 Corruption.
This is as far as Udi's blog explained. Before this, it was called a "typical heap overflow". However, based on my experience, achieving arbitrary address read/write or code execution from this point is difficult. Let's look at the tagPROPLIST structure on 64-bit again:
kd> dt -b !tagPROPLIST
win32k!tagPROPLIST
+0x000 cEntries : Uint4B
+0x004 iFirstFree : Uint4B
+0x008 aprop : tagPROP
+0x000 hData : Ptr64
+0x008 atomKey : Uint2B
+0x00a fs : Uint2B
Phase 1 corruption gave us a corrupted tagPROPLIST array, allowing us to increase the number of tagPROP entries. tagPROPLIST has only two fields:
When a new entry is inserted into the list, a function is called to scan each entry until a suitable iFirstFree index is found. If not found, it checks whether iFirstFree is greater than cEntries. If the atomKey is not in the list, it checks whether iFirstFree != cEntries. If not equal, an entry is inserted at the iFirstFree index. If equal, a new property list large enough to accommodate the inserted property is allocated, the original entries are copied over, and the new entry is inserted.
The atomKey field corresponds to LPCTSTR lpString. As stated in the MSDN documentation for SetProp(), the caller can pass either a string pointer or a 16-bit atom value. When passing a string pointer, it is automatically converted to an atom value before being stored in the property list. Since we can pass arbitrary atom values to SetProp(), we have the ability to control these two bytes, but with some constraints. That is, the atomKey value we corrupt must not duplicate existing keys; otherwise, when setting a new property entry, it will replace the existing entry with the same atom value. Additionally, the fs field is uncontrollable; a value of 0 indicates atomKey < 0xBFFF, which matches integer atom values. A value of 2 indicates atomKey >= 0xC000.
Another thing to note is that tagPROP is only 0xc bytes. This structure is aligned to 0x10 bytes on 64-bit systems, so there are an extra 4 bytes that cannot be corrupted when inserting a tagPROP entry. The last important point is that the first 8 bytes of a tagPROPLIST heap block define the size of the list entries, meaning each newly inserted tagPROP entry will always be written to an 8-byte aligned offset.
For each inserted tagPROP on 64-bit systems, the situation is:
* Offset 0x0: 8 bytes of fully controllable data (hData)
* Offset 0x8: 2 bytes of mostly controllable data (atomKey)
* Offset 0xa: 2 bytes of uncontrollable data (fs)
* Offset 0xc: 4 bytes of unmodifiable data (padding)
This is much better than just 2 bits, but still not perfect. Unless we can overwrite something with the first 8 bytes, which come from the fully controllable hData field, we are otherwise quite restricted. If we need to write deeper into adjacent structures, we cannot avoid uncontrollable corruption of certain values. I spent some time searching for various objects on the desktop heap. Considering the corruption restrictions mentioned above, the only way I could think of to bypass these restrictions and achieve arbitrary address read/write was to corrupt the strName field of tagWND, which is a _LARGE_UNICODE_STRING structure:
kd> dt !_LARGE_UNICODE_STRING
win32k!_LARGE_UNICODE_STRING
+0x000 Length : Uint4B
+0x004 MaximumLength : Pos 0, 31 Bits
+0x004 bAnsi : Pos 31, 1 Bit
+0x008 Buffer : Ptr64 Uint2B
If we could corrupt the Buffer field of this structure, we could read or write up to MaximumLength bytes from a given address by manipulating the window text. This is what I did. You may have noticed this structure in the previous section, which described how to create a heap block of arbitrary size and value on the desktop heap, so the same situation applies here.
Now we know how to corrupt data using tagPROPLIST entries, which parts we can control, and more importantly, the restrictions we will face, which differ between 32-bit and 64-bit. What we did on 64-bit will not work on 32-bit. Soon we will move from Phase 2 corruption (writing through the tagPROP structure) to another corruption "primitive" that allows us to write fully controllable data. This is what I call Phase 3 corruption.
The goal is to corrupt the strName field in an adjacent tagWND. We already know it is a _LARGE_UNICODE_STRING structure, but let's look at more details of the tagWND structure, which looks like this:The structure above is for 64-bit. You can see that the offset of the _LARGE_UNICODE_STRING structure we want to overwrite is 0xd8. You'll also notice an important field at the beginning of this structure. I had hoped to be able to wreak havoc with it, but there are many pointers in _THRDESKHEAD that require us to stay alert, and unfortunately, we cannot control where we write, limited by reasons discussed earlier.
The _THRDESKHEAD structure definition:
kd> dt !_THRDESKHEAD
win32k!_THRDESKHEAD
+0x000 h : Ptr64 Void
+0x008 cLockObj : Uint4B
+0x010 pti : Ptr64 tagTHREADINFO
+0x018 rpdesk : Ptr64 tagDESKTOP
+0x020 pSelf : Ptr64 UChar
The problem with _THRDESKHEAD not only confuses us, but also forces us to re-examine the alignment constraint. Regardless of the offset where the new tagPROP list item is placed, our write operation will directly overwrite the start of _LARGE_UNICODE_STRING:
win32k!_LARGE_UNICODE_STRING
+0x000 Length <-- hData (fully controllable) overwrites here
+0x004 MaximumLength <-- and here
+0x004 bAnsi <-- and here
+0x008 Buffer <-- atomKey and fs (partially controllable) overwrites here
Clearly, we want to overwrite the Buffer pointer to access memory at arbitrary addresses. However, even if we can safely corrupt other fields of this structure, we cannot control the pointer we need.
We cannot corrupt arbitrary data. Solving this problem is no longer about corrupting the tagPROPLIST but about a completely different corruption mechanism.
On Windows XP and later, the heap chunk header (i.e., _HEAP_ENTRY) of the user-mode backend allocator (e.g., the kernel desktop heap) is stored on the heap and located before the actual content of the chunk. The desktop heap itself is managed through the _HEAP structure, which gives us some freedom when exploiting this heap chunk.
The _HEAP_ENTRY structure is defined as follows:
kd> dt !_HEAP_ENTRY
ntdll!_HEAP_ENTRY
+0x000 PreviousBlockPrivateData : Ptr64 Void
+0x008 Size : Uint2B
+0x00a Flags : UChar
+0x00b SmallTagIndex : UChar
+0x00c PreviousSize : Uint2B
+0x00e SegmentOffset : UChar
+0x00f UnusedBytes : UChar
The heap chunk header is 0x10 bytes total. The first 8 bytes are PreviousBlockPrivateData, used to accommodate the actual previous block data when the requested size exceeds the normal 0x10 (aligned to 8 bytes if less than 8 bytes). This is briefly described in the Leviathan blog entry, as well as in earlier articles about the user-mode heap. Size and PreviousSize represent the size of the current chunk and the size of the previous chunk, in units of 0x10 bytes. Flags indicate whether the chunk is free, etc. If the _HEAP_ENTRY security mode is enabled in _HEAP, SmallTagIndex will contain the XOR checksum of the data in the chunk.
Although the alignment constraint is against us, it does exist. If you call tagPROPLIST, it is always at least 0x18 bytes, plus 0x10 bytes for each tagPROP. For a tagPROPLIST with two list entries (0x28 bytes), it will be placed into a 0x20-byte heap chunk, and the extra bytes indicated by PreviousBlockPrivateData use the adjacent heap chunk. This means when we add a third list entry, the adjacent heap chunk will be corrupted, and the controllable 8 bytes of hData will overwrite the top of the _HEAP_ENTRY.
What we want to do is exploit this so that we can somehow write arbitrary data to the location of the Buffer pointer. First, we modify the heap layout so that near our corrupted tagPROPLIST heap chunk, during the control process, we have a small heap chunk containing a window-related text string, which we call the "overwrite heap chunk". Adjacent to this "overwrite heap chunk", we place a tagWND so that we can corrupt this tagWND. The following Figure 7 illustrates this process. Note that we have omitted previously sprayed heap chunks to save space, so these should now be considered implicit.

Next, we insert the third tagPROP into the tagPROPLIST list, which will overwrite the last 8 bytes of the _HEAP_ENTRY and the first 8 bytes of the "overwrite heap chunk". This allows us to modify the _HEAP_ENTRY of the "overwrite heap chunk" to make its size larger than its actual size and large enough to include the adjacent tagWND structure.
Now free the corrupted "overwrite heap chunk" so that the heap manager places it into the free list corresponding to a chunk size (each free list corresponds to a fixed size) larger than the actual size of the "overwrite heap chunk". Then reuse this chunk by modifying the window text (which we can fully control). However, there is a small problem to solve. When the "overwrite heap chunk" is freed, the heap manager tries to find the previous adjacent chunk, which depends on the corrupted Size field. The heap manager checks whether this adjacent chunk is free to merge it. We want to control this reference anyway and set an in-use flag. Slightly modifying our heap layout can achieve this. At this point, we place a fake heap chunk with its heap header set to the in-use flag and its PreviousSize set to the value of the corrupted Size field. We can simply implement this by assigning a window text to another window. The new heap layout is as follows (Figure 8):

Now we can free the corrupted "overwrite heap chunk" by updating the text string of the window associated with it to a length greater than the original 0x10 bytes. Thus, the corrupted "overwrite heap chunk" is first freed and placed in the free list, but its size is corrupted, claiming to be larger than its actual size. This size can be adjusted according to our actual needs. Then our string data is written to this "overwrite heap chunk", and we use this "overwrite heap chunk" to corrupt the adjacent tagWND into arbitrary data. As follows (Figure 9):

This is the corruption for Phase 3. Now we can overwrite the pointer to strName.Buffer with any data we want. However, corrupting other data of tagWND is still somewhat troublesome, but this is not a problem because the desktop heap is mapped into user space! Therefore, before corrupting everything, we read all the contents of tagWND, modify the contents of the strName structure to what we want, and send all the data by modifying the window text.
Using strName not only gives us an "arbitrary read/write primitive", but also allows us to repeatedly modify strName, thanks to the modification mechanism of the window text. As long as the length of the written string is not greater than the value of MaximumLength, the same heap chunk can be reused. Therefore, every time we want to modify the address of strName to read a value somewhere, we use a new string to append our data and update the "overwrite heap chunk". This reuse is shown in Figure 10. Note that I have again magnified the bird's-eye view granularity to show each corruption in detail.

This means we ultimately only need to corrupt two additional things (besides the original tagPROPLIST list items):
Now, if we want to read some bytes from a memory location, we query the window text via the InternalGetWindowText() function, where the corrupted strName entry resides. We can read the number of bytes declared by the Length field. Similarly, if we want to write to an arbitrary memory location, we use the NtUserDefSetText() function to update the corrupted window text, but the amount written should not exceed the value declared by the MaximumLength field (which we can also set). This way, the existing buffer is reused and points to the memory address we want.
Although heap encoding has been used in the user-mode backend allocator since Windows Vista, the desktop heap never enabled it until Windows 8. Therefore, on Windows 8 and later, when we perform the overwrite of the "overwrite heap chunk", this creates an obstacle. However, in fact, the _HEAP structure that contains the heap includes this cookie and uses it to encode the entire heap header. Therefore, we can read this cookie from the desktop heap mapped into user space, and then use it to encode the header of the "overwrite heap chunk". The encoding method can be obtained by reverse engineering the allocator code; mimicking its operation, the allocator will accept this operation.
First, note that on 32-bit systems, the tagPROP structure is 8 bytes, not 0xc bytes as on 64-bit, and the hData field we control is only 4 bytes, not 8 bytes as on 64-bit. There are also no extra padding bytes; on 64-bit there is 8 bytes of padding, making the entire structure exactly 8 bytes. This means we cannot fully corrupt the adjacent heap chunk header if we can only partially control the data. On some versions of Windows this is possible because we can control the most important fields, but on Windows 8 and 8.1 the heap header is encoded, and we end up unsafely overwriting part of the heap header via the fs field. The _HEAP_ENTRY header on 32-bit looks similar but lacks the PreviousBlockPrivateData field.
We still cannot corrupt all parts of tagWND because we cannot avoid truncated pointers. And I still haven't found an object that satisfies this. Given that _LARGE_UNICODE_STRING works well on 64-bit, I want to use it on 32-bit as well.
My idea is that if we can corrupt the iFirstFree field of the tagPROPLIST structure (the index of the first freed property item in the property list) by increasing the index value, then we can make it point to a farther location on the heap. For example, we could point it to the top of tagWND.strName. Figure 11 illustrates this idea:

For clarity of the process, we now use two tagPROPLIST structures: "PropertyList A" used for UAF, and "PropertyList B". We need to know exactly which parts of the tagPROP inserted into "PropertyList A" will overwrite the iFirstFree field of "PropertyList B". We must also remember that we can only write 8 bytes at a time, so we must insert at least one extra tagPROP into "PropertyList A": the first corruption overwrites the adjacent heap header, and the second hits the tagPROPLIST fields of "PropertyList B". These may vary depending on the operating system and different chunk sizes, and in my exploitation, it must adapt to various heap layouts. Figure 12 shows how we corrupt. Note that in the figure, the first tagPROPLIST is not broken down into individual fields, so tagPROP[0] is implicit. However, in the second tagPROPLIST, we break it down to show our corruption process. That's why tagPROP[0] is displayed:

First, note that if we write 8 bytes for each tagPROP, that means we can only partially control the overwrite of iFirstFree (since it comes from atomKey and fs fields), which is our main concern. Because we can fully control at least two key bytes through the value of atomKey, when this value is small enough, the fs field will become 0. Therefore, we use the value of hData to overwrite cEntries with a reasonable value, and use atomKey to make iFirstFree point to the tagWND, where the strName.Buffer pointer we want to overwrite resides. If we cannot directly overwrite the values of Length and MaximumLength, we can pre-allocate a string to the target window to ensure its length is already set to a certain value.
Let's look at the 32-bit tagWND structure to see what we can get. Note that this time I use the -b parameter so that we can easily calculate the offset of Buffer in strName.
kd> dt -b !tagWND
win32k!tagWND
+0x000 head : _THRDESKHEAD
+0x000 h : Ptr32
+0x004 cLockObj : Uint4B
+0x008 pti : Ptr32
+0x00c rpdesk : Ptr32
+0x010 pSelf : Ptr32
+0x014 state : Uint4B
+0x014 bHasMeun : Pos 0, 1 Bit
[SNIPPED FLAGS]
+0x014 bDestroyed : Pos 31, 1 Bit
+0x018 state2 : Uint4B
[SNIPPED FLAGS]
+0x018 bWMCreateMsgProcessed : Pos 31, 1 Bit
+0x01c ExStyle : Uint4B
+0x01c bWS_EX_DLGMODALFRAME : Pos 0, 1 Bit
[SNIPPED FLAGS]
+0x01c bUIStateFocusRectHidden : Pos 31, 1 Bit
+0x020 style : Uint4B
+0x020 bReserved1 : Pos 0, 16 Bits
[SNIPPED FLAGS]
+0x020 bWS_POPUP : Pos 31, 1 Bit
+0x024 hModule : Ptr32
+0x028 hMod16 : Uint2B
+0x02a fnid : Uint2B
+0x02c spwndNext : Ptr32
+0x030 spwndPrev : Ptr32
+0x034 spwndParent : Ptr32
+0x038 spwndChild : Ptr32
+0x03c spwndOwner : Ptr32
+0x040 rcWindow : tagRECT
+0x000 left : Int4B
+0x004 top : Int4B
+0x008 right : Int4B
+0x00c bottom : Int4B
+0x050 rcClient : tagRECT
+0x000 left : Int4B
+0x004 top : Int4B
+0x008 right : Int4B
+0x00c bottom : Int4B
+0x060 lpfnWndProc : Ptr32
+0x064 pcls : Ptr32
+0x068 hrgnUpdate : Ptr32
+0x06c ppropList : Ptr32
+0x070 pSBInfo : Ptr32
+0x074 spmenuSys : Ptr32
+0x078 spmenu : Ptr32
+0x07c hrgnClip : Ptr32
+0x080 hrgnNewFrame : Ptr32
+0x084 strName : _LARGE_UNICODE_STRING
+0x000 Length : Uint4B
+0x004 MaximumLength : Pos 0, 31 Bits
+0x004 bAnsi : Pos 31, 1 Bit
+0x008 Buffer : Ptr32
+0x090 cbwndExtra : Int4B
+0x094 spwndLastActive : Ptr32
+0x098 hImc : Ptr32
+0x09c dwUserData : Uint4B
+0x0a0 pActCtx : Ptr32
+0x0a4 pTransform : Ptr32
+0x0a8 spwndClipboardListenerNext : Ptr32
+0x0ac ExStyle2 : Uint4B
+0x0ac bClipboardListener : Pos 0, 1 Bit
[SNIPPED FLAGS]
+0x0ac bChildNoActivate : Pos 11, 1 Bit
The offset of strName is 0x84, and the offset of Buffer is 0x8c. We know we have the index of the tagPROP list entry, and we know we can write 8 bytes. Therefore, we can easily know whether iFirstFree indexes to offset 0x88 of the window pointed to by MaximumLength. Since we can only control two bytes of Buffer, the write operation is not feasible. Given that our goal is to use this as our "arbitrary read/write primitive", this result is unacceptable. If we write an index that points to 0x90, then we will overwrite cbwndExtra, which is not what we are after.
Reviewing what we can control during heap feng shui, let's see if there are any interesting offsets in tagWND that we can control. At offset 0x70 in tagWND is the pSBInfo field. This offset is divisible by 8, so we can overwrite this pointer with part of the data from a fake tagPROP's hData.
Can we overwrite pSBInfo to directly point to strName in the same tagWND structure? Perhaps we can use scroll bar APIs to corrupt strName for our purposes.
pSBInfo points to a tagSBINFO structure, which was mentioned in the initial UAF process.
kd> dt -b !tagSBINFO
win32k!tagSBINFO
+0x000 WSBflags : Int4B
+0x004 Horz : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
+0x014 Vert : tagSBDATA
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
We recall that WSBflags does not give us much control, but we at least know that when the scroll bar is enabled, it is set to 1, and when disabled, it is set to 0. This flag cannot be set to arbitrary values; by reverse engineering the relevant functions, we found that if the scroll bar state is not changed, this flag remains unchanged. The values in the tagSBDATA structure seem more interesting. If we read the documentation for SetScrollInfo(), we can understand the meanings of these values well. It seems we can set parameters via the SCROLLINFO structure to SetScrollInfo(). As long as there is a scroll bar control near the window we want to corrupt, we can directly manipulate the pSBInfo pointer (it will send a special window message to the associated window control). Obviously, we can unconditionally control posMin and posMax. The page and pos fields are a bit tricky because they are restricted to a certain range, which we try to avoid now. Set the SIF_RANGE flag in the SCROLLINFO structure to declare where we want to set the minimum and maximum values.
We want to overwrite Buffer with arbitrary data, which means we want posMin to overwrite it, so we can overwrite pSBInfo to point to strName.MaximumLength. As long as we don't enable or disable the scroll bar, the WSBflags field will not be overwritten, ensuring the integrity of strName.MaximumLength. This means that no matter how we set posMin (via nMin in SCROLLINFO), it will overwrite Buffer, and posMax will be written to cbwndExtra. This is not a big problem; on 64-bit systems, we can pre-read this value and restore it later. The general idea of the overflow is shown in Figure 13:

Now we illustrate the attack process on 32-bit using Figure 14. Before corrupting any address far from UAF, let's first step back to see the relevant heap chunks and heap layout in the figure. Now that we know more details, the next steps become obvious.

Next, insert two property items into "PropertyList A", which will corrupt data near "PropertyList A" thanks to the previous UAF corruption, and at the same time make the iFirstFree of "PropertyList A" point to pSBInfo. Note that this will also corrupt the nearby pSBInfo value, but we can pre-read it to restore it after corruption.

We insert a new tagPROP into "PropertyList B" with an atom identifier different from the existing ones in the list, so that the tagPROP is inserted at the next free index. This corrupts pSBInfo, making it point to strName.MaximumLength in the same tagWND.

Finally, refresh the scroll bar to corrupt the strName.Buffer field (as shown in Figure 17):

Note that, unlike the 64-bit case, we cannot corrupt the length value of strName. We can pre-allocate a window text string of appropriate length so that its value is already in use. Afterwards, whether we want to read or write some data from a kernel address, we just call SetScrollInfo() to manipulate the target window to update the Buffer value, and then use the window text API to operate.
Now we have a reusable arbitrary address read/write "primitive" on 32-bit!
From now on, assume we have an arbitrary read/write "primitive". Therefore, when I say leak/read a value or overwrite a value, it means executing the "primitive" established in the previous corruption phase. This "primitive" is roughly the same on both platforms. All that remains is to overwrite a function pointer and make it point to some shellcode payload. The common method is to overwrite the second entry of nt!HalDispatchTable, which corresponds to the HalQuerySystemInformation() function. Then call the NtQueryInternalProfile() function from user mode to trigger it.
We need to know the base address of the kernel module to calculate the kernel address of nt!HalDispatchTable. For this, we can call NtQuerySystemInformation() from user mode to get module information, which includes the module base.
// Enumeration value 11 represents SystemModuleInformation, which is undocumented...
rc = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)11, pModuleInfo, 0x100000, NULL);
Afterwards, we load ntoskrnl.exe in user mode to find the offset of nt!HalDispatchTable, so we can get its kernel-space address. Then we use the "read primitive" to read the kernel address of HaliQuerySystemInformation() (an unexported function) to modify it, and then use the "write primitive" to corrupt that function's pointer to point to the address of the shellcode (either in kernel address space or user address space, detailed later). The number of bytes read/written is the same on 32-bit and 64-bit systems.
Windows 8 and 8.1 introduced support for SMEP, and some security products can enable it on Windows 7 as well, so we assume it exists. SMEP prevents us from executing code in user space with kernel privileges, making it infeasible to modify the entry of nt!HalDispatchTable to point to a user-space address. Therefore, we want it to point to a controllable location in kernel space, where code can modify the cr4 register to disable SMEP, allowing us to jump to user space. The MWR article introduces an interesting technique on 64-bit by self-mapping page table entries to obtain a kernel-mode effective address for any virtual address. Then, using the "write primitive", we directly modify the page table entry and modify its mask bits. I ported this technique to 32-bit, but there are some differences between systems with PAE enabled and those without.
The obvious method to achieve this is to map a user-space address into kernel space, then use the "write primitive" to make the page table entry have system privileges instead of user privileges. This is the first step. I encountered an interesting problem when implementing it on Windows 8. Windows 8 and later have the Desktop Window Manager (dwm.exe) periodically scanning windows on the desktop and querying their names, for reasons I did not investigate. This operation does not send a message to the window, but there is a corresponding window procedure that calls GetInternalWindowText(). Therefore, the problem is that when using the window structure's strName fields to overwrite the page table entry containing shellcode (which belongs to our own process's page table), when dwm.exe retrieves the window name from the kernel, the modified page table entry causes the kernel to check if strName.Buffer is null, indirectly referencing that address; if the address is invalid, it will cause a system crash.
To satisfy dwm.exe's query, I used a kernel address as the payload. This way, regardless of what the current process loads, the page table entry associated with that address is always valid. I chose to place it on the desktop heap, as we can calculate its kernel address using the methods mentioned earlier. We still use the self-mapping page table entry technique, but now the page table is already marked with high privileges but not executable. So all we need to do is set the execute bit.
Steps:
nt!HalDispatchTable to point to the kernel address found in Phase 1.NtQueryInternalProfile() to jump to the payload.On Windows 8.1, there is another issue: NtQuerySystemInformation() checks the low integrity SID, meaning only medium integrity or above can obtain the kernel base. This can be easily bypassed using the well-known sidt technique. We save the IDT address to user mode (no privilege check required), then use the "read primitive" to read the IDT entry we need, which usually points to kernel address space. Therefore, we can leak the kernel address of an interrupt handler, then look up the offset in the corresponding PE file of the kernel module.
Once we have the kernel base address, we can calculate the address of nt!HalDispatchTable.
The usual method is to load the ntoskrnl.exe file and interpret its symbol offsets, adding the leaked kernel base address. However, this fails for enhanced mode sandboxes due to file system restrictions: you cannot read C:\windows\system32\ntoskrnl.exe. To bypass this, we use our "leak primitive" to parse the symbol addresses we need from the in-memory kernel PE image.
That's all the material. Thank you for reading. Using the techniques presented in this paper, I can achieve stable exploitation on 32-bit and 64-bit systems including XP, Vista, 7, 8, 8.1, and Server 2012. Windows 2003 and 2008 are not exploitable by default because user-mode callbacks cannot be hooked, so these systems cannot be attacked unless the required conditions are met. The exploitation process is quite complex with many obstacles to overcome, but it also provides a lot of fun and learning. Many of the viable methods and research results used in this paper have been mentioned in articles by other researchers. To my knowledge, there is only one mitigation that can prevent exploitation of win32k.sys: the Google Chrome sandbox uses, which effectively blocks win32k kernel system calls at runtime. I welcome any improvements and feedback. If you find any shortcomings in the techniques I have presented, let me know, and I will update this document. You can contact me via Twitter @fidgetingbits or email [email protected].