Posts

Privilege Escalation via a Kernel Pointer Dereference (CVE-2017-18019)

A little while ago, I discovered a vulnerability, CVE-2017-18019, affecting a kernel driver of multiple K7 Computing security products, as well as the products of Defenx, both for Windows.  Both were affected because they were using the same anti virus engine, and both are now patched.

The proof of concept was based on an invalid kernel pointer dereference, which led to a blue screen of death.  That research and the subsequent coordinated disclosure process were, at the time, sponsored and handled by SecuriTeam.  It turns out that the proof of concept could be exploited further, and turned into local privilege escalation.  So, with the permission of SecuriTeam, I decided to create a write-up of that local privilege escalation development process.

Targeting

This article targets the following 64-bit Windows versions: Windows 7 SP1 – Windows 10 v1809.

A Medium integrity level is required in order to exploit this vulnerability in the way that is demonstrated through this article. In order to exploit this from a Low integrity level, you will have to do extra work in order to leak some kernel pointers. This can be done either though other IOCTLs handlers of the target driver itself, or through other Windows driver kernel memory leak bugs.

Bug Analysis

The root cause of this issue is that the author of the following function trusts a pointer to read data from, originating from a user-supplied input buffer, as long as it references an address inside the kernel address space.

The vulnerable function fetches a pointer from the IOCTL’s input buffer and checks if it is greater or equal to nt!MmHighestUserAddress (0x00007ffffffeffff in x64). If that’s true, then the function will proceed by dereferencing that pointer and evaluating the first byte located at that memory address.

Clearly, the purpose (even though the implementation is buggy) of this check is to verify that the pointer address from where further information will be read resides in kernel memory of which, from the developer’s perspective, its virtual address and contents are not supposed to be known and controlled by the user. This, of course, is not entirely true because kernel object addresses may be leaked, and also they may reference directly or indirectly user-supplied data.

The following screenshot shows (in grouped nodes) what we described above.

Figure 1 – Verify it is a kernel pointer.

We can easily crash the host by supplying an arbitrary kernel pointer that references a non-allocated memory page.

The following image shows the output from Windbg the moment the memory access violation occurs.

Figure 2. Arbitrary kernel pointer dereference.

Further Analysis

What we know at this point is that we have a denial of service bug that can be triggered by any user in order to crash the host. So, we analysed this function further in order to find out if there is something more that we can do with it.

The following graph-view screenshot continues directly from what is shown in Figure 1.

Figure 3. Kernel memory buffer data checks.

Assuming that RCX points to a valid kernel address where the first byte is 0x4B, so that the previous check succeeds (cmp byte ptr [rcx], 4Bh), we arrive at the second part of the vulnerable function as shown above.

Here we notice further byte value checks, and specifically the second byte of the buffer referenced by RCX should be 0xFF in order to access the final part of our analysis.

Figure 4. Arbitrary Function Pointer Call.

A couple of pointer dereferences later, we see that the function is treating the last one as a function pointer. We also notice that the first and second parameters passed to RCX and RDX respectively can also be controlled.

To be more specific, the first parameter is taken from the buffer referenced by the arbitrary kernel pointer that we control, and the second one is pointing inside our user-input buffer that is defined through the call to DeviceIoControl function.

Setting things up

At this point, we have all the information we need in order to proceed with the exploitation of the vulnerability. To do that, we must know the address of a kernel object and also control its contents, to a certain extent. As we discussed, the initial pointer from where the rest of data is read leading to a function pointer called, must reference an address inside the kernel address space. This is also the developer’s assumption around the safety of that decision.

In a previous article we talked about Private Namespaces and the ability to insert user-defined data in the body of the associated kernel object. We will be using this type of objects in order to exploit the vulnerability, as they can be used reliably in this case as well.

In order to exploit this vulnerability, we will be using two kernel objects of the aforementioned type. The first object will be used for controlling the subsequent pointer dereferences that allow us to call an arbitrary function pointer, while the second object will be used in order to control the initial kernel pointer check that must reference a known kernel object in memory (first object).

Exploitation in Windows 7 SP1 x64

In the absence of exploitation mitigation such as SMEP (Supervisor Mode Execution Protection), taking advantage of this vulnerability is quite straight forward. We can execute our payload function in userland without taking any additional steps, such as temporarily disable SMEP. We just need to control the instruction pointer and that would be enough.

To start with, we will create a Private Namespace object using a random boundary name and we will use NtQuerySystemInformation function to leak its address.

Figure 5. 1st Object (Win7 SP1 x64).

Then, we will create another object of the same type with a crafted boundary name.

The first and second bytes must be 0x4B and 0xFF respectively (see Figures 1 and 3) to satisfy the byte value checks. Also, in the offset 0x0A (Figure 4 – first pointer dereference) of the crafted boundary name, we will be inserting the address of the first object + the distance in bytes (0x1a0) between that address and the location of the boundary name in that object + an arbitrary offset (0x1A) that contains a value that can be translated to a userland pointer, which satisfies the proof of concept for this version of Windows. Note that we take into account that at the result of the previous calculation, the value 0x0C will be added in order to reach the userland pointer value (Figure 4 – second pointer dereference).

Figure 6. 2nd Object (Win7 SP1 x64).

Let’s have a closer look at how these two objects are ‘inter-connected’.

Figure 7. Objects Interconnection (Win7 SP1 x64).

Finally, we can see the function pointer being called, in order to execute our payload at address 0x1010000.

Figure 8. Call Payload-Function Pointer (Win7 SP1 x64).

Exploitation in Windows 8.1 – 10 v1809 x64

In more recent Windows versions, exploiting a kernel driver bug is more challenging due to exploitation mitigations that have been added. In this case, we take control over the execution flow by calling an arbitrary function. However, due to the SMEP we are not able to directly execute code that resides in the user address space from kernel mode, so we will have to take another approach.

A common solution is to attempt to temporarily disable SMEP by clearing the 20th bit in CR4 register of a specific processor and lock our threads execution to only run on that one, so that we can execute our payload in userland as before. However, we would have to restore CR4 in order to avoid KPP (Kernel Patch Protection/PatchGuard) killing the host.

Another way, which we will be using in this write-up, is to take advantage of the execution flow control in order to turn it into a “write-what-where” primitive, which will enable us to modify arbitrary data in kernel memory. Once that is achieved, there are, again, two common ways of taking advantage of this in order to elevate our privileges.

The first method is to overwrite with a NULL value the SD (Security Descriptor) pointer in the object header of an elevated process running as SYSTEM. This will allow a non-privileged process to inject and execute malicious code in the same security context. However, this method will only work up to Windows 10 v1511 (Build 10586), as described in this article.

Another way to take advantage of a “write-what-where” primitive is to enable privileges in the primary token of a non-privileged process in order to enable it to again inject and execute code in the security context of a process running as SYSTEM. This method still works fine, but it requires a minor modification from Windows 10 v1709 (Build 15063) onwards, as described here. What we are about to describe here can also be used in Windows 7.

Going back to what we have described so far, we have noted that we are also able to control the first two parameters (see Figure 4) passed in RCX and RDX respectively, once our arbitrary function is called. We are going to take advantage of this capability in a moment.

In this case, we first need to leak the address of the primary token of our process, where will be enabling additional privileges. We will be using that address as the target of our exploitation primitive. As in Windows 7, NtQuerySystemInformation can be used for the same purpose from the standard ‘Medium Integrity’ of a user process in order to leak the kernel object and function addresses that we will be using.

We will then create our first Private Namespace object with a custom boundary name, where the first 8 bytes will be set to the kernel address that we will be using as our ‘gadget’ to modify arbitrary kernel data. So, instead of executing a payload in userland, we will be redirecting the execution to kernel function, nt!RtlCopyLuid that will enable us to modify arbitrary kernel data.

Figure 9. nt!RtlCopyLuid.

Since we control both the RCX and RDX registers, we can use this function to complete our “write-what-where” primitive.

We will be needing, again, a second Private Namespace object with a custom boundary name which at offset 0x0A of the name data (Figure 8 – first pointer dereference) must contain the address of the first object + the distance in bytes (0x1a0) between that address and the location of the boundary name in that object. Remember that at the first 8 bytes of the boundary name of the first Private Namespace object, we have inserted the address of nt!RtlCopyLuid. Note that as before, we must take into account that at the result of the previous calculation, the value 0x0C will be added in order to reach our arbitrary kernel function pointer value, loaded at the R10 register (Figure 8 – second pointer dereference).

So, this is how it should look:

*(ULONG_PTR*)(boundaryName + 0x0A) = customPrivNameSpaceAddress + boundaryNameOffsetInDireObject - 0x0C;

Then, we need to take control of the first two parameters.

The first parameter loaded in RCX is read again from our custom boundary name, at offset 2 (the first two bytes of our custom boundary name must be 0x4B,0xFF). So, we will be setting there the address of our process’ token object + the offset (0x40) to reach the nt!_SEP_TOKEN_PRIVILEGES structure member.

Figure 10. nt!_SEP_TOKEN_PRIVILEGES.

It should look as follows:

*(ULONG_PTR*)(boundaryName + 0x02) = tokenAddress + 0x40;

Finally, we can also control RDX since the value of R12 is copied over, which points at the address of our userland input buffer + 0x10 (see Figure 1 – 6th node). This is where we read the data from, to write into an arbitrary kernel address. In this case we will overwrite the ‘Enabled and ‘Present’ privileges members of the aforementioned structure (Figure 10).

It should look like this:

*(unsigned __int64*)(inputBuf + 0x10) = _ULLONG_MAX;

So, our exploit will have to reach the vulnerable function twice in order to complete the attack.

Figure 11. Objects Interconnection – Write-What-Where Primitive.

The image above shows how the two objects are ‘interconnected’ in order to complete our “write-what-where” primitive to finalize the exploit.

Conclusion

This was an interesting bug to examine and exploit, as it shows once more that no input data should ever be blindly trusted. From the developer’s perspective, trusting a kernel pointer to read data from, presumably out of user’s control, was a ‘safe’ decision to take. However, it turned out to become a serious vulnerability in multiple products of two different vendors that use the same SDK.

WinDbg: using pykd to dump private symbols

We’ve recently been conducting some reverse engineering and vulnerability analysis on an Anti Virus (AV) product and wanted to attach Rohitab API Monitor to one of the AV’s running processes so that I could log the Windows API function calls in order to better understand how the AV was implemented.

The AV in question was protecting its user mode process by making use of Kernel callbacks in one of the device drivers. The callbacks were registered using the ObRegisterCallbacks function:

This is the post patch guard method of allowing the AV driver to intercept calls to ZwOpenProcess, amongst others, so that access can be denied to the process handle, or the returned handle can be given less access rights etc.

The OB_CALLBACK_REGISTRATION and _OB_OPERATION_REGISTRATION structures are not defined in the Microsoft public symbols, so the WinDbg command x nt!_OB* doesn’t help. Both structures are well documented on MSDN, so after intercepting the function call to ObRegisterCallbacks in WinDbg I started decoding the structures by calculating the offsets and dumping memory addresses.

After a couple of debugging runs, this soon becomes tedious; my heart sank when I thought I might have to write a WinDbg script to automate the process. If you’re like me and you don’t use WinDbg scripts that often then you will know how time consuming it can be to re-learn WinDbg scripting each time you need it.

Then I remembered pykd, a python scripting module for Python, which I had heard about but never tried.

Pykd installation

If you are using the x64 version of WinDbg then you also need to install a 64 bit version of python. I chose the 2.7.x version as I already have some build scripts written for Python 2.7.x. At the time of writing, the latest version was 2.7.14:

Once installed head over to the pykd repository. The home of pykd has recently moved to githomelab.ru:

Download the bootstrapper zip, which contains pykd.dll:

pykd.dll has to be copied into the WinDbg “winext” folder, which for me was in the following location:

  • C:\Program Files\Windows Kits\10\Debuggers\x64\winext

The pykd module need to be added to your Python installation. I used pip from a command prompt to achieve this:

Pykd can then be loaded into WinDbg by using the command .load pykd

Note that the pykd documentation is in Russian, however Google Translate did an excellent job of translating it to English. Documentation can be found here:

Dumping OB_CALLBACK_REGISTRATION using pykd

Pykd allows type information to be dynamically created using the typeInfo class. It is also possible to retrieve existing type information (similar to the dt WinDbg command). Using these two capabilities the _OB_CALLBACK_REGISTRATION structure can be defined:

Using the typeInfo function we can obtain the type information for UNICODE_STRING which is a public symbol:

We can create an instance of a type using the typeVar function which takes parameters typeInfo and address. In pykd an address is simply an integer.

Once we have a typeVar instance, we can dump the information in a similar way to using dt nt!_ _OB_CALLBACK_REGISTRATION <address> (if the symbol was public), by casting to a string. Dumping the data in this way doesn’t recursively dump the information, so the output can be improved by iterating over the OperationRegistration array and individually dumping each item.

The final step is to parse the script command line. I wanted to be able to use a register as a parameter as well as an address, so for this so I did some ghetto input parsing:

The script can be run in WinDbg as follows, using the rcx register as input:

Output

Executing the script, while on a breakpoint at nt!ObRegisterCallbacks gives the following output.

Ideas for enhancing the script

Breakpoint Links

It is possible to output debugger markup language (DML) from pykd. It would be quite simple to emit links from the script that allow breakpoints to be set.  DML is documented here:

The dprintln function takes an additional parameter that specified if DML should be emitted:

Specifying True for the second parameter will output DML.

Patching a ret into the callback functions

Using pykd it is also possible to manipulate memory, so the callback functions could be automatically patched to return immediately, and in fact this is what I did to bypass the process protection implemented by the AV:

Other WinDbg scripting options

If you don’t want to use python/pykd then other options are available. Here are two of the most recent ones.

WinDbg Preview (JavaScript)

The WinDbg preview version available here:

It has an updated modern UI and allows JavaScript to be used for scripting

LINQ Debugger Objects

WinDbg can also be queried with LINQ, if you are familiar with LINQ then this might be a good bet