Verifying Windows Kernel Vulnerabilities

Outside of the Pwn2Own competitions, HP’s Zero Day Initiative (ZDI) does not require that researchers provide us with exploits. ZDI analysts evaluate each submitted case, and as part of that analysis we may choose to take the vulnerability to a full exploit.

 

For kernel level vulnerabilities (either in the OS itself, or in device drivers), one of the vulnerabilities that we find is often termed a ‘write-what-where’[1]. For ease of analysis it was worth writing a basic framework to wrap any given ‘write-what-where’ vulnerability and demonstrate an exploit against the operating system.

 

There are three basic steps to taking our arbitrary write and turning it into an exploit, and we’ll explore each of them in turn.

 

The Payload: Disabling Windows Access Checks

 

At the heart of the Windows access control system is the function nt!SeAccessCheck. This determines whether or not we have the right to access any object (file, process, etc) in the OS.

 

The technique we’re going to use was first described by Greg Hoglund in 1999[2], and a variant of this technique was used by John Heasman in 2006[3]; it is the latter that we’ll use as our jumping off point.

 

KernelExploitation_Image0.png

 

The key here is the highlighted field.  If the security check is being done on behalf of a user process, then the OS checks for the correct privileges. However, if the security check is being done on behalf of the kernel, then it always succeeds. Our goal, then, is to dynamically patch the Windows kernel such that it always considers the AccessMode setting to indicate that the call is on behalf of the kernel.

 

On Windows XP, this is a fairly straightforward task. If we examine the kernel in IDA (in this case, we’re looking at ntkrnlpa.exe), we find the following code early in the implementation of SeAccessCheck:

 

                            PAGE:005107BC                 xor     ebx, ebx

                            PAGE:005107BE                 cmp     [ebp+AccessMode], bl

                            PAGE:005107C1                 jnz     short loc_5107EC

 

 

Since KernelMode is defined as 0 in wdm.h, all we have to do to succeed in all cases is to NOP out the conditional jump after the compare. At that point, all access checks succeed.

 

On later versions of the OS, things are slightly more complicated. The function nt!SeAccessCheck calls nt!SeAccessCheckWithHint, and it is the latter that we’ll need to patch. We’ll see why this makes things more complicated when we look at how to gather the information needed to execute the attack. If we look at Windows 8.1, we can see that instead of dropping through to the KernelMode functionality, we branch to it:

 

                            .text:00494613 loc_494613:                            

                            .text:00494613                 cmp     [ebp+AccessMode], al

                            .text:00494616                 jz      loc_494B28

 

All we need to do is replace the conditional branch with an unconditional branch, and we again make every call to SeAccessCheck appear to come from the kernel.

 

Now that we have a target, we still have one more, slight problem to overcome. The memory addresses we need to overwrite are in read-only pages. We could change the settings on those pages, but there is an easier solution. On the x86 and x64 processors, there is a flag in Control Register 0 which determines whether or not supervisor mode code (i.e. Ring 0 code, which is to say, our exploit) pays attention to the read-only status of memory. To quote Barnaby Jack[4]:

 

                             “Disable the WP bit in CR0.


                               Perform code and memory overwrites.


                               Re-enable WP bit.”

 

At this point, the actual core of our exploit looks like this:

 

KernelExploitation_Image1.png

 

There is one additional complication to our manipulation of the WP bit. We need to set the processor affinity for our exploit, to make sure that we stay on the core with the processor settings we chose. While this is likely unnecessary for our manipulation of the WP bit, it is more of an issue in more complicated exploits that require us to disable SMEP (more on that later). Either way, it doesn’t hurt, and all we have to do is a make simple call:

 

                                     SetProcessAffinityMask(GetCurrentProcess(), (DWORD_PTR) 1);

 

Now, one thing you’ll note in the exploit code is that we don’t actually know what the patch is, or where it is going. The exploit just takes information that was already provided, and applies it. We’ll actually determine that information as part of the exploit research.

 

The Attack: Passing control to the exploit

 

We have code ready to run in ring 0. We need two things to make it work, the information it requires about the OS configuration, and a means to transfer control to our code while the processor is in supervisor mode.

 

Since the primitive that we have to work with is a ‘write-what-where’, we are going to use that to overwrite a function in the HalDispatchTable. This technique, described by Ruben Santamarta in 2007[5], will allow us to divert execution flow to our exploit code.

 

The function we’re going to hook is hal!HaliQuerySystemInformation. This is a function that is called by an undocumented Windows function NtQueryIntervalProfile [5][6], and the invoking function is not commonly used. To understand why this is crucial, we need to briefly talk about the layout of memory in Windows.

 

Windows divides memory into two ranges; kernel memory is located above MmUserProbeAddress, and user memory is located below it. Memory in the kernel is common to all processes (although generally inaccessible to the process code itself), while memory in userland is different for each process loaded. Since our exploit code is going to be in user memory, but we are hooking a kernel function pointer, if any other process calls NtQueryIntervalProfile it will almost certainly crash the operating system. Because of this, the first step in our exploit is to restore the original function pointer:

 

KernelExploitation_Image2.png

 

As with our earlier example, you can see that we’re relying on external information as to where the function pointer entry is, and what the original value should be.

 

At this point, our actual exploit trigger looks like this:

 

KernelExploitation_Image3.png

 

For flexibility, our prototype for WriteWhatWhere() also includes the original value for the address, if known. Finding the addresses we need for both the exploit and the exploit hook is the final step. 

 

The Research: Determining the OS Configuration

 

In this case, we’re assuming that we are looking for a local elevation-of-privilege. We have the ability to run arbitrary code on the system as some user, and our goal is to turn that into a complete system compromise. Determining the OS Configuration is much more difficult in the case of a remote attack against a kernel vulnerability.

 

We’ve determined that we need to know the following pieces of information:

  • The address of nt!HalDispatchTable
  • The address of hal!HaliQuerySystemInformation
  • The address of the code in nt!SeAccessCheck or related helper function we need to patch
  • The value to patch

Additionally, we can also look up the original value, which would let us have a different exploit that restored the original functionality. After all, once we’ve done what we need to do, why leave the door open?

 

What we’ll need to know are the base addresses of two kernel modules, the hardware abstraction layer (HAL) and the NT kernel itself. In order to get those, we’ll need to again use an undocumented function – in this case we need NtQuerySystemInformation[6][7]. Since we know that we’re going to need two NT functions, we’ll go ahead and create the prototypes and simply load them directly from the NT DLL:

 

KernelExploitation_Image4.png

 

The next step is to determine which versions of these modules are in use, and where they are actually located in memory. We do this by using NtQuerySystemInformation to pull in the set of loaded modules, and then searching for the possible names for the modules we need:

 

KernelExploitation_Image5.png

 

Our next step is, in almost every case other than this, a bad idea[8]. We’re going to use a highly deprecated feature of LoadLibraryEx and load duplicate copies of the two modules we found:

 

KernelExploitation_Image6.png

 

With this flag set, we won’t load any referenced modules, we won’t execute any code, but we will be able to use GetProcAddress() to search the modules. This is exactly what we want, because we’re going to be using these loaded modules as our source to search for what we need in the actual running kernel code.

 

At this point, we have almost everything we’re going to need to find the offsets we require. We have both the base address of our copies of the kernel modules and the actual base addresses on the system, so we can convert a relative address (RVA) from our copy into an actual system address. And we have read-access to copies of the code, so we can scan the code to look for identifiers for the functions we need. The only thing left is actually a stock Windows call, and we’ll use GetVersionEx() to determine what version of Windows is running.

 

Some things are easy, because the addresses are exported:

 

KernelExploitation_Image7.png

 

But for most of what we need, we’re actually going to have to search. We have two functions to search for, one of which (hal!HaliQuerySystemInformation) does not have an exported symbol, and the other is either nt!SeAccessCheck or a function directly called by it.

 

We’ll look at the last case, because that lets us look at how we handle both exported functions and those that are purely private. First, a look at nt!SeAccessCheck:

 

KernelExploitation_Image8.png

 

And then a look at the portion of nt!SeAccessCheckWithHint that we’re going to patch:

 

KernelExploitation_Image9.png

 

Now, in practice, these two functions are adjacent to each other, but we’re going to go ahead and use the public function to track down the reference to the internal function, and then scan the internal function for our patch location. The code to do that looks like this:

 

KernelExploitation_ImageA.png

 

The function PatternScan is simply a helper routine that given a pointer, a scan size, a scan pattern, and a scan pattern size, finds the start of the pattern (or NULL if no pattern could be found).

 

In the code above, we search first for the relative jump to nt!SeAccessCheckWithHint, and extract the offset. We use that to calculate the actual start of the nt!SeAccessCheckWithHint in our copy of the module, and then we scan for the identifying pattern of the conditional branch we need to replace.  Once we find the location, we can determine the actual address by converting it first to an RVA and then rebasing it off of the actual loaded kernel image. Finally, the replacement value is OS version dependent as well; in this case the replacement for the JZ (0x0f 0x84) is a NOP (0x90) and JMP (0xe9).

 

By gathering the information we need from the copied version of the system modules, we’re able to have the same framework target multiple versions of the Windows Operating System. By searching for patterns within the target functions, we are more resistant to changes in the OS that aren’t directly in the functions we’re looking for.

 

Some final complications

 

Everything we’ve done so far will work, up until we get to Windows 8, or more specifically, the NT 6.2 kernel. For convenience, we have the actual code of the exploit running in user memory. 

 

With the Ivy-Bridge architecture, Intel introduced a feature called Supervisor Mode Execute Protection (SMEP) [9]. If SMEP is enabled, the processor faults if we attempt to execute instructions from user-mode addresses while the processor is in supervisor-mode. The moment control passes from our hooked function pointer in the kernel to our code, we get an exception. Windows supports SMEP as of Windows 8/Server 2012, and the feature is enabled on processors that support it by default. To get around this, we either need to move our exploit into executable kernel memory (something that is also made more difficult in the NT 6.2 kernel)[10] or disable SMEP separately[11][12].

 

The final problem for us was introduced in Windows 8.1. In order to get Windows 8.1 to tell us the real version of the Operating System, we need to take additional steps. According to MSDN[13]:

 

KernelExploitation_ImageB.png

 

With the manifest included, we’re able to correctly detect Windows 8.1, and adjust our search parameters appropriately when determining offsets.

 

Conclusion

 

There is of course, one piece missing. While a framework to prove exploitation is useful, we still need to have an arbitrary ‘write-what-where’ for this to work.  

 

We use this framework internally to validate these flaws, so if you happen to find a new one, you can always submit it to us (with or without an exploit payload) at http://www.zerodayinitiative.com/. We’d love to hear from you.

 

Endnotes

 

[1] The earliest formal reference I can find to this terminology is in Gerardo Richarte’s paper “About Exploits Writing”  (G-CON 1, 2002) where he divides the primitive into a “write-anything-somewhere” and “write-anything-anywhere”. In this case, our “write-what-where” is a “write-anything-anywhere”.

 

[2] Greg Hoglund, “A *REAL* NT Rootkit, patching the NT Kernel” (Phrack 55, 1999)

 

[3] John Heasman, “Implementing and Detecting an ACPI BIOS Rootkit” (Black Hat Europe, 2006)

 

[4] Barnaby Jack, “Remote Windows Kernel Exploitation – Step In To the Ring 0” (Black Hat USA, 2005) [White Paper]

 

[5] Ruben Santamarta, “Exploiting Common Flaws in Drivers” (2007)

 

[6] Although it does not cover newer versions of the Windows OS, Windows NT/2000 Native API Reference (Gary Nebbett, 2000) is still an excellent reference for internal Windows API functions and structures.

 

[7] Alex Ionescu, “I Got 99 Problems But a Kernel Pointer Ain’t One” (RECon, 2013)

 

[8] Raymond Chen, “LoadLibraryEx(DONT_RESOLVE_DLL_REFERENCES) is fundamentally flawed” (The Old New Thing)

 

[9] Varghese George, Tom Piazza, and Hong Jiang, “Intel Next Generation Microarchitecture Codename Ivy Bridge” (IDF, 2011)

 

[10] Ken Johnson and Matt Miller, “Exploit Mitigation Improvements in Windows 8” (Black Hat USA, 2012)

 

[11] Artem Shishkin, “Intel SMEP overview and partial bypass on Windows 8” (Positive Research Center)

 

[12] Artem Shisken and Ilya Smit, “Bypassing Intel SMEP on Windows 8 x64 using Return-oriented Programming” (Positive Research Center)

 

[13] MSDN, “Operating system version changes in Windows 8.1 and Windows Server 2012 R2”

  

Additional Reading

 

Enrico Perla and Massimiliano Oldani, A Guide to Kernel Exploitation: Attacking the Core, (Syngress, 2010)

 

bugcheck and skape, “Kernel-mode Payloads on Windows”, (Uninformed Volume 3, 2006)

 

skape and Skywing, “A Catalog of Windows Local Kernel-mode Backdoor Techniques”, (Uninformed Volume 8, 2007)

 

mxatone, “Analyzing local privilege escalations in win32k”, (Uninformed Volume 10, 2008)

Labels: security
Leave a Comment

We encourage you to share your comments on this post. Comments are moderated and will be reviewed
and posted as promptly as possible during regular business hours

To ensure your comment is published, be sure to follow the Community Guidelines.

Be sure to enter a unique name. You can't reuse a name that's already in use.
Be sure to enter a unique email address. You can't reuse an email address that's already in use.
Type the characters you see in the picture above.Type the words you hear.
Search
Showing results for 
Search instead for 
Do you mean 
About the Author


Follow Us
The opinions expressed above are the personal opinions of the authors, not of HP. By using this site, you accept the Terms of Use and Rules of Participation