Microsoft Win32k NULL Page Vulnerability Technical Analysis
by Seth Gibson and Dan Zentner
Endgame has discovered and disclosed to Microsoft the Win32 NULL Page Vulnerability (CVE-2013-3881), which has been fixed in Microsoft’s October Security Bulletin, released October 8, 2013. The vulnerability was the result of insufficient pointer validation in a kernel function that handles popup menus. Successfully exploiting this vulnerability would allow an attacker with unprivileged access to a Windows 7 or Server 2008 R2 system to gain access to the Windows kernel, thereby rendering user account controls useless.
In previous versions of Windows, including XP, Server 2003, Vista, and Server 2008 R1, Microsoft actually included code that adequately verified the pointer in question. However, in Windows 7 (and Server 2008 R2), that check was removed, leading to the exploitable condition.
If the product line ended there, it would be easy to imagine that this was an inadvertent removal of what a developer mistakenly thought was a redundant check and to give it little additional thought. However, in the initial release of Windows 8 (August 2012), the pointer validation had been put back in place, long before we reported the bug to Microsoft. We would assume that when a significant security issue comes to light, Microsoft would simultaneously fix it across all affected products. Unless the Windows 8 win32k.sys code was forked from a pre-Windows 7 base, this bug was fixed upstream by Microsoft prior to our disclosure. This is purely speculative, but if our previous supposition is true, they either inadvertently fixed the bug, or recognized the bug and purposely fixed it, but failed to understand the security problem it created.
The good news for Windows users is that Microsoft does have a realistic approach to dealing with vulnerabilities, which resulted in some protection even prior to the release of this patch. One of the simplest security features (at least in concept, if not in implementation) that Microsoft introduced in Windows 8 was to prohibit user applications from mapping memory at virtual address zero. This technique takes the entire class of null-pointer-dereference kernel bugs out of the potential-system-compromise category and moves them into the relatively benign category of user-experience/denial-of-service problems. When Microsoft back-ported this protection to Windows 7, they eliminated the opportunity to exploit this bug on 64-bit systems. This illustrates how the conventional wisdom that “an ounce of prevention is worth a pound of cure” can be turned on its ear in the world of software vulnerabilities. Microsoft will undoubtedly be fixing null pointer dereferences in their products for as long as they support them. However, by applying a relatively inexpensive “cure”, they have limited the consequences of the problems that they will spend years trying to “prevent”.
Part of what makes this type of vulnerability so valuable to attackers is the proliferation of sandbox technologies in popular client-side applications. We have confirmed that this vulnerability can be exploited from within several client-side applications’ sandboxes, including Google Chrome and Adobe Reader, and from Internet Explorer’s protected mode. On the surface, that sounds like bad news. On the other hand, we would not have even considered that question if these mitigation technologies were not making it more difficult for attackers to compromise systems. In order to completely own a target via one of those applications, an attacker must have a vulnerability that leads to code execution, another that allows them to leak memory so as to defeat Microsoft’s memory randomization feature, and finally, a vulnerability like the one described here that allows them to escape the hobbled process belonging to the initial target application.
When an application displays a popup or context menu, it must call user32!TrackPopupMenu or user32!TrackPopupMenuEx in order to capture the action that the user takes relative to that menu. This function eventually leads to the xxxTrackPopupMenuEx function in win32k.sys. Since it is unusual to simultaneously display multiple context menus, there is a global MenuState object within win32k.sys that is ordinarily used to track the menu. However, since it is possible to display multiple context menus, if the global MenuState object is in use, xxxTrackPopupMenuEx attempts to create another MenuState object with a call to xxxMNAllocMenuState. xxxTrackPopupMenuEx saves the result of this allocation attempt and checks to ensure that the result was not 0, as seen in the most recent unpatched 64-bit Windows 7 version of win32k.sys (6.1.7601.18233):
xxxTrackPopupMenuEx+364 call xxxMNAllocMenuState xxxTrackPopupMenuEx+369 mov r15, rax xxxTrackPopupMenuEx+36C test rax, rax xxxTrackPopupMenuEx+36F jnz short alloc_success xxxTrackPopupMenuEx+371 bts esi, 7 xxxTrackPopupMenuEx+375 jmp clean_up
In the event that the allocation fails, the function skips to its cleanup routine, which under normal circumstances will cause a BSOD when the function attempts to dereference unallocated memory at r15+8:
xxxTrackPopupMenuEx+9BA clean_up: ; CODE XREF: xxxTrackPopupMenuEx+375j xxxTrackPopupMenuEx+9BA bt dword ptr [r15+8], 8
However, if we can allocate and correctly initialize the memory mapped at address zero for the process, we can reliably gain arbitrary code execution when the function passes the invalid MenuState pointer to xxxMNEndMenuState.
xxxTrackPopupMenuEx+A76 mov rcx, r15 ;pMenuState xxxTrackPopupMenuEx+A79 call xxxMNEndMenuState
It is possible to reliably create circumstances in which the xxxTrackPopupMenuEx call to xxxMNAllocMenuState will fail. After creating two windows, we use repeated calls to NtGdiCreateClientObj in order to reach the maximum number of handles that the process is allowed to have open. Once we have exhausted the available handles, we attempt to display a popup menu in each of the two previously created windows. Since the global MenuState object is not available for the second window’s menu, xxxTrackPopupMenuEx calls xxxMNAllocMenuState in order to create a new MenuState object. Because there are no available handles due to our previous exhaustion, this call fails and xxxMNEndMenuState is called with a parameter of 0, instead of a valid pointer to a MenuState object.