In April 2020, security researcher Gil Dabah published a paper on a set of vulnerabilities he had discovered within the Win32k subsystem of the Windows operating system. These vulnerabilities demonstrated instances of a new class of bugs, dubbed “Smash the Ref.” Dabah’s research included 13 test cases and later four proof-of-concept (PoC) code samples chronicled in a GitHub repository.  

Windows vulnerabilities that use kernel mode execution for privilege escalation are often of interest to Metasploit’s research team. The win32k subsystem is included on all versions of Windows, and it offers reliable attack surface that is not configuration-dependent. More generally, local privilege escalation (LPE) exploits continue to maintain high relevance in the modern attack landscape; Metasploit has noted several times over the past two years that LPEs have featured prominently in community module contributions, and their prevalence has spurred us to expand support for local exploitation APIs and mixins that make LPE exploit development easier across a number of platforms.

Fellow Metasploit researcher Grant Willcox and I examined the test cases delineated in Dabah’s Smash the Ref research with the goals of evaluating the bug class’s overall exploitability and identifying any candidates that might provide reliable utility for Metasploit Framework users looking to obtain an initial foothold in the context of a standard user account. The rest of this blog post details our experience and findings throughout several cycles of reproduction attempts and crash analysis.

Win32k Overview

The win32k subsystem is composed of three separate drivers on Windows 10: win32k.sys, win32kbase.sys, and win32kfull.sys. Split drivers appeared with the advent of Windows 10, which is based on the OneCore system that runs across a variety of devices—including HoloLens, XBOX One, and a variety of Internet of Things (IoT) devices—each which may require different levels of win32k subsystem functionality. Previous versions of Windows were only designed to run on a PC, which is why there is only one win32k file, win32k.sys, on these versions. Previous versions of Windows used only win32k.sys before splitting into the three drivers used today. These drivers collectively provide the functionality for the win32k subsystem, which is responsible for graphical user interface elements such as windows, menus, and scrollbars. The win32k subsystem is accessible from user mode via a collection of syscalls. These syscalls are then invoked by common API methods exposed through documented functions in the Windows API such as CreateWindowExA and InsertMenuItem.

Bug Class Analysis

According to Dabah’s whitepaper, the Smash the Ref bug class is related to another well-known bug class that affected the win32k subsystem. In the Windows 7 era, it was common to exploit Use-After-Free (UAF) vulnerabilities within win32k by identifying objects that could be freed when the Windows kernel makes a callback to user mode. This pattern of calling back to user mode is still common today, and allows for users to be notified of various events on different objects and UI elements.

In recent years, Microsoft’s approach has been to “lock” the objects prior to making the potentially dangerous call into user mode, which reduces the instances of Use-After-Free vulnerabilities that arise from lack of locking. Dabah states outright in his paper that “Today such bugs hardly exist anymore.” For the purposes of our own exploitability research, it’s worth distinguishing between user-mode callbacks as a vector and UAF vulnerabilities whose root is the absence of object locking. On modern versions of Windows it’s rarer now to find vulnerabilities related to a lack of locking on objects when a user mode callback is made. It should be noted, however, that it is still common for many publicly-released vulnerabilities in modern versions of Windows to leverage callbacks.

Win32k functions receive the “xxx” prefix to their name if they themselves contain a callback into user mode, or otherwise invoke a function that does. This works by allowing the kernel to recognize when there is a lock on the memory, which will cause the kernel to delay freeing it. The object will be marked as destroyed, and the kernel will track these objects until the lock is cleared and they can be freed. These objects are known as zombie objects—a term that refers to their state of continued existence even after they have been destroyed. Zombie objects are a critical concept when understanding this new bug class.

The Smash the Ref class is a modification of this approach: Instead of freeing an object that is exposed via a user-mode callback, Smash the Ref targets child objects that are not always properly protected by the same locking mechanism that prevents UAF exploitability. There are instances where a manually referenced pointer to an object (a “dumb” pointer) is used as part of a user-mode callback. This differs from a smart pointer, which automatically updates its reference count and will automatically destroy the object when the reference count reaches 0. If the referenced object can be freed either directly (or, more likely, indirectly) during the user-mode callback, then subsequent uses of the reference would create a Use-After-Free vulnerability that could be exploited by an attacker.

Test Case Analysis

Each of the 13 test cases was tested on 64-bit Windows 10 v1909 to determine a few key pieces of information as part of a triage process. The intention of this phase was to answer the following questions:

Can the vulnerability be reliably reproduced? Does it produce crashes in a consistent manner?
When writing Metasploit modules, it’s important to understand the reliability of a particular vulnerability. Unreliable exploits may have research or educational value, but they are not often used on professional penetration testing engagements, especially when they might trigger a Blue Screen of Death (BSOD). Determining the reliability and consistency of a vulnerability is almost always a first step in evaluating whether or not it is a viable exploit module candidate.

How does the vulnerability manifest itself?
Most of the test cases resulted in Use-After-Free conditions. We validated this by using Driver Verifier with the Special Pool and Pool Tracking settings enabled for win32k.sys, win32kbase.sys, and win32kfull.sys, along with WinDBG and some carefully placed breakpoints to track allocation and free operations.

One particular test case, #13 (UnlockDesktopMenu), triggered a NULL pointer dereference. Given that NULL pointer dereference bugs were mitigated in Windows 8, and that this mitigation was later backported to Windows 7 x64, we decided not to investigate this test case further.

Smash the Ref Vulnerability Test Cases

Test Case ID Reproducible? Uses “Ultimate” Technique? Notes
1 xxxMnOpenHierarchy Yes No
2 FreeTimer Yes No Triggering the bug was very reliable on Windows 10 1909 but was not able to be reproduced on v1709
3 xxxCreateCaret Yes No Reliable exploitation is unlikely due to the lack of an opportunity to manipulate the heap. Freed object is a thread queue
4 Ultimate Reloading No Yes
5 FreeSPB No No
6 xxxCapture WND Yes No Triggering the bug was very reliable
7 xxxCapture PQ Yes No Reliable exploitation is unlikely due to the lack of an opportunity to manipulate the heap. Freed object is a thread queue
8 zzzAttachThreadInput No No
9 xxxSendMinRectMessages Yes No
10 UnlockNotifyWindow Yes Yes This is most likely the best candidate for reliable code execution
11 CSRSS Arbitrary Free Skipped Yes Skipped due to it being noted as an arbitrary Free within CSRSS
12 Advanced FlashWindow Skipped Yes Skipped due to it being noted as only partially functional
13 UnlockDesktopMenu NULL deref Yes No Not analyzed due to being a NULL pointer dereference

Technique Inspection

Of the 11 test cases we attempted, we were able to successfully reproduce eight of them; none of them appeared immediately exploitable for arbitrary code execution. In order to leverage a Use-After-Free bug to achieve code execution, it is necessary to replace the freed object on the heap. If the original freed object is still present, then the references to it after it has been freed will most likely behave as expected. This is also why it was necessary to use Driver Verifier with the Special Pool and Pool Tracking settings enabled in order to monitor these objects.

In order to leverage the reference to the freed object to do something useful, a suitable object must be put in its place and manipulated to resemble a corrupted object of the type that was freed. At this point it’s also important to identify the heap on which the original object was allocated and its size, as both will affect efforts to replace it. In the interest of writing reliable exploits, we needed to identify whether or not a user-mode callback would be invoked after the object is freed, and before it is referenced/used. If such a user-mode callback site exists, an attacker would have ample opportunity to reallocate the space and perform any necessary heap grooming operations.

With all this in mind, we triaged reproducible test cases according to where the target object was freed, and where the first reference to the freed object was made after it had been freed. Ideal candidates would have a user-mode callback made between the two points—we identified these by following the code path and searching for an invocation of a function prefixed with “xxx”. In the case of test case numbers three (xxxCreateCaret) and seven (xxxCapture PQ), no such callback was identified.

Volume two of the whitepaper addresses this issue with a technique Dabah refers to as “Ultimate Zombie Reloading.” This technique uses a specific object setup combined with a critical call to kernel32!ExitThread, causing win32kfull!xxxCreateWindow to stop mid-execution, leaving the window in a desirable state. The important aspect of this technique is that it permits a user-mode callback via win32kfull!xxxClientFreeWindowClassExtraBytes, which provides an opportunity to replace the freed object. Test cases that used this technique were easily identifiable by Dabah’s use of ClientFreeExtraBytes_No within his source code. This included test case numbers four (Ultimate Reloading), 10 (UnlockNotifyWindow), and 12 (Advanced FlashWindow).

According to Dabah’s source code notes, test case number 12 is supposedly incomplete. This left test cases four (Ultimate Reloading) and 10 (UnlockNotifyWindow) as the most likely candidates for reliable code execution.

While triaging, test case number four (Ultimate Reloading) did not exhibit the desired level of reliability and was noted to be potentially incompatible with older versions of Windows. This note on incompatibility was consistent with the testing performed. Test case 10 (UnlockNotifyWindow), however, was found to affect 64-bit builds of both Windows 7 SP1 and Windows 10 v1909.

Test Case #10: UnlockNotifyWindow

This test case manifested itself as a Use After Free of a tagMENU object within the win32kfull!UnlockNotifyWindow function, which is called via win32kfull!xxxFreeWindow.

Figure 1: UnlockNotifyWindow

The win32kfull!UnlockNotifyWindow function is fairly simple. There are three things to note about it:

  1. The function stores a pointer to arg_pMenu->rgItems which it uses while iterating over var_dwItemCount tagITEMs in the body of the main loop.
  2. The main loop recursively calls into win32kfull!UnlockNotifyWindow if there is a submenu.
  3. Once the loop completes, win32kbase!HMAssignmentUnlock is called on the object.

The way this test case works is that if there are items in the items array (arg_pMenu->rgItems), then the first item in that array will result in a recursive call being made to win32kfull!UnlockNotifyWindow. The vulnerability then occurs not because of the loop within this function, but rather because of step three—whilst unlocking arg_ppObject via a call to HMAssignmentUnlock(), the window’s user-mode callback will be invoked. An attacker can then take this opportunity to add additional items to the top menu that is still being iterated on from the outer invocation of UnlockNotifyWindow(). Adding these additional items causes the item array to be reallocated to provide space, rendering the arg_pMenu->rgItems pointer stored in step one invalid.

Once the user-mode callback returns, and the inner UnlockNotifyWindow() call returns to the outer one, the loop mentioned in step two will (while still using the arg_pMenu->rgItems pointer from step one) continue to process additional items. In order to trigger a bug check, there must be an additional menu item to force another iteration within the loop after the recursive call frees the notification window. This can be accomplished by creating and appending an additional menu to g_hClmMenu within the original test case code.

Figure 2: The additional menu forcing a second iteration

After the items array has been reallocated within the user-mode callback, an attacker would have the opportunity to reallocate the space that it previously occupied and craft it to perform a useful action on the next iteration within UnlockNotifyWindow().

In summary...

New bug classes highlight the value of security research and, in some cases, provide richer context to red and blue teams alike than single vulnerabilities do. As an offensive research team, one of our primary goals is to evaluate exploitability in practical contexts. The outcome of our evaluation here was that most test cases were not readily exploitable for arbitrary code execution due to the inability to control a necessary allocation. The tenth test case (UnlockNotifyWindow) stands out from the rest of Dabah’s enumerated test cases as the best candidate for exploitation, but success would likely require a heap grooming technique, a read/write primitive, and memory leak—all of which are made more complex by modern Windows mitigations.

Dabah notes that there may be additional instances still present within the code base that could present an additional opportunity for further research. This tracks with our general experience as an offensive research team: It's rare for exploitation doors to be fully and finally closed.