Jump to content
Nytro

Verifying Windows Kernel Vulnerabilities

Recommended Posts

Verifying Windows Kernel Vulnerabilities

DaveWeinstein| October 30, 2013

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.

original?v=mpbl-1&px=-1

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

ll 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:

original?v=mpbl-1&px=-1

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:

original?v=mpbl-1&px=-1

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:

original?v=mpbl-1&px=-1

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:

original?v=mpbl-1&px=-1

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:

original?v=mpbl-1&px=-1

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:

original?v=mpbl-1&px=-1

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:

original?v=mpbl-1&px=-1

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:

original?v=mpbl-1&px=-1

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

original?v=mpbl-1&px=-1

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:

original?v=mpbl-1&px=-1

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]:

original?v=mpbl-1&px=-1

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 Zero Day Initiative. 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)

Sursa: Verifying Windows Kernel Vulnerabilities - HP Enterprise Business Community

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.



×
×
  • Create New...