We recently discovered a new and quietly released Windows kernel exploitation defence. Exploiting a kernel bug by setting the pointer to the SecurityDescriptor to NULL in the header of a process object running as SYSTEM won’t work from Windows 10 v1607 (Build 14393). If you want to know why, keep reading.

One of the most efficient and reliable attack vectors used by kernel privilege escalation exploits is to set the pointer to the SecurityDescriptor of a securable object to NULL. This pointer resides in the header of the object.

By performing this action against a process that runs with high privileges, such as the SYSTEM account, the exploit process can then inject a remote thread and execute code under the security context of the target process.

This kernel object attack vector is quite powerful also for the reason that makes Supervisor Mode Execution Protection (SMEP) and other protections such as Non-Executable Kernel pool memory totally obsolete, since we don’t have to execute our payload anymore in Kernel mode and/or address space in order to elevate our privileges.

In other words, zeroing out the pointer to the security descriptor of a process running as SYSTEM is pretty much equal to a ‘GOD_MODE’ cheat, in the language of video game players.

In Windows 10 using this attack vector with just a write-zero-primitive was only possible until v1511 (Build 10586). Indeed, in the latest Windows 10 v1607 (Build 14393), a mitigation against this attack vector has been added. This doesn’t apply in earlier major Windows versions such as Windows 8.1 and below.

Note: This analysis is based on Windows 10 v1607 (Build 14393) x64.

The SecurityDescriptor



Every kernel object is associated with a header structure that is prepended to the object itself. One of the members of this structure is a pointer to a SecurityDescriptor which contains the Access Control List (ACL) for that object. The ACL contains the information about who can access this very object and with what permissions.

As a side note, not all objects store the pointer to their SecurityDescriptor in their header. For example persistent objects such as files and registry keys use their own mechanism to access this data during the access check to the object.

Using Windbg, let’s get some information about the process of Windows Explorer.

Now let’s get some information about its header.

Now let’s get some more information about this structure.

As you can see, by knowing the address of our object, we can easily get the address where the pointer to the object security descriptor is stored. This is basically located at address: OBJECT_ADDRESS – sizeof(ULONG_PTR).

Going back to the pointer to that security descriptor, remember this address is practically a pseudopointer, meaning that its last 4 bits contain information that is not related to the actual address. To get the actual address, we need to mask those bits. This can be achieved by doing (PSEUDO_POINTER & 0xFFFFFFFFFFFFFFF0).

So let’s see what information is stored there in this case.

Let’s see now how this translates from a high level perspective.

High level overview

By using ProcessExplorer we can get an idea of who has access to the process object and at what level. So let’s see how the ACL looks in ProcessExplorer.

Indeed, this matches the information that we retrieved through the security descriptor. The SYSTEM and the current user (Admin) accounts have full access permissions 0x1FFFFF to this process object.

NULL SecurityDescriptor Attack Vector

Up to Windows 10 v1511 this is what you would see after using the aforementioned attack vector:

The target process spoolsv.exe is running as SYSTEM, but there is no information about who can access this object and at what level. This grants full access to everyone.

Remember, this is not the same as pointing to a valid security descriptor with no Access Control Entries (ACEs) defined, thus an empty Discretionary Access Control List (DACL) . In that case access would be denied to everyone.

The image above depicts the result of a kernel exploit using a write-zero-primitive to set the SecurityDescriptor pointer member of the header of the target process object to zero. In this case when someone requests access to the process object, if that member is set to zero the kernel assumes that this object was created with a NULL DACL, and will grant access to everyone.

Mitigating the NULL SecurityDescriptor Attack Vector

In the latest Windows 10 v1607, this attack vector is now blocked. This is achieved by performing a simple check against a global table describing some characteristics of the NT kernel object types whenever a process attempts to get a handle, thus get access to an object.

So this is how this attack type was mitigated via a few screenshots.

The next check is the crucial one, so we will explain some things first.

If you have a look at image 7, you will notice that the R14 register holds the address of an ObjectType structure. The Kernel maintains a table of pointers (ObTypeIndexTable) to these structures to keep information about the various object types.

So let’s examine the information that this ObjectType structure contains.

Notice the index value is set to 7. This indicates the index where a pointer to this ObjectType structure is stored inside the ObTypeIndexTable, and we also know that the object we are trying to access is a process object.

Let’s move on now with the next check as show below.

We see that it checks if the 3rd bit at [r14+0x42] is not zero and in that case it will jump.

So let’s examine the TypeInfo member of the ObjectType structure, since that offset is part of it.

So basically, if the SecurityRequired flag is set, which it is, then the first jnz jump will be taken and the kernel will trigger a BSoD with a “BAD OBJECT HEADER” stop code.

Here is the code that triggers the crash

Putting everything together, the kernel will now perform an extra security check before even examining if the process that requests access to an object actually can access the object based on the SecurityDescriptor of the object and the security token of the requester.

This could be summed up with the following

1 2 3 4 if ( ObjectHeader . SecurityDescriptor == NULL && ( ObjectType . SecurityRequired || ( ObjectHeader . InfoMask & 2 ) != 0 ) ) { BugCheckEx ( BAD_OBJECT_HEADER ) ; }

Objects created with NULL DACL

In the case of a programming error, bad security practice or if for any other reason an object is created with a NULL DACL (which grants access to everyone), this won’t have any impact in the host. The reason for this is that even if we decide to assign such DACL to the security descriptor, the SecurityDescriptor pointer in the header of the object will still be valid and it will point to a SecurityDescriptor with a DACL set to NULL.

An example is shown below.

Final thoughts

It is clear that the Microsoft folks are continuously trying to mitigate various attack vectors. This examined attack vector, that has been now blocked, is one of the most used when it comes to kernel bug exploitation.

However, as already mentioned, this security check is only effective when the bug only allows you to write a zero in an arbitrary kernel memory address, and the chosen attack vector is the one analysed here. If the bug allows for a read-write primitive as well, then we could leak the pointer to the security descriptor and set a NULL DACL there, which would pass under the radar undetected. On the other hand, if we can control the value that we are writing to an arbitrary kernel memory address then we could choose different exploitation paths. One of them would be to access the token of the exploit process and enable privileges at will, which pretty much would have the same “GOD_MODE” effect.