Handle Elevation
ACL is an illusion
As we know by now, each _EPROCESS structure holds within it a pointer to the _HANDLE_TABLE object, Named the ObjectTable.
This specific pointer, is to the head of the Handle Table, and contains a list of handles which appear one after the other in the memory.
We also know that the order of the handles in memory matches the corresponding indexes of the handles, and that the value that is returned to user mode upon handle creation, is that of the index in the current process in which the relevant handle object resides.
This will later become key to navigating through the handles of a process in order to reach the right handle in memory.
Additionally, we know that the most important member of the handle itself, GrantedAccessBits, stands for the permissions which have been granted to the user towards the target object, latter to creating it in the form of an ACCSESS MASK. The fact that the handle is already created is important since during the creation process of the handle, the kernel operates important checks to monitor that the requested Access is valid to the user by ACL and security context (and also Object callbacks if any are registered), and if a handle is already created, then modifying it surpasses all the mentioned above.
Knowing the running user has already passed relevant Access Control mechanisms, and has already received a handle of some sort, with some permission, and given read and write access to the _HANDLE_TABLE_ENTRY object itself, one can edit the GrantedAccessBits and by thus elevate an existing handle or de-elevate a handle from any permission level to its desired one.
Yes, a handle created for SYNCHRONIZE, READ_CONTROL, QUERY_LIMITED_INFORMATION, can be escalated to FULL_CONTROL, and vice versa!
The true strength of this technique, is that handles for all the various objects that are manageable by handles, are kept in this very list, and are all objects from the same structure type, meaning that this will work for every handle you may receive. Which includes all that is listed in the following MSDN link under “Kernel Objects”:
https://learn.microsoft.com/en-us/windows/win32/sysinfo/object-categories
the downside of this method, is that it cannot create a handle that did not exist before. With that being said any handle permission will suffice, since a handle is what needed, no matter the access type, and most users would be able to request the lowest level of access to most objects as those access types are mostly reserved for querying information regarding the target object, like a process’s start time and image name, or reading a files access control list, since this is how the operating system is built for the most part.
Also, in the next attacks explained in this article, we will see how a user can elevate itself to SYSTEM, which will guarantee a low privileged handle for almost all objects.
When we boil it down to the basic steps one needs to take in order to perform such technique, we are left with the following steps:
1.Iterate through all process objects in the kernel until you find the EPROCESS object that resembles the process which holds the targeted handle
2.Read the pointer to the ObjectTable from that EPROCESS, and begin iterating through the handles
3.Stop iteration when you find the handle that has an Index value that is identical to the target handle value in user mode
4.Read the _HANDLE_TABLE_ENTRY object byte by byte into a Byte Array, and cast that Byte array to a _HANDLE_TABLE_ENTRY object in order to map and view the HANDLE object in user-mode Programmatically
5.Edit the _HANDLE_TABlE_ENTRY object in user-mode, and change GrantedAccess member to the desired value
6.Copy the struct back into a byte array in order to prepare it for writing
7.Write the Array byte by byte back into the handle table in the exact address we read it from.
This is how the core logic behind this technique would look like in code:
The function above received the Address of the ObjectTable head (already explained in the navigation section), and the value of the handle index has received in user-mode.
Those two functions are used in conjunction with ExpLookupHandleTableEntry in order to receive the kernel address of the target Handle Object.
ExpLookupHandleTableEntry, originally, is a function which is not exported. Meaning, neither kernel or user mode applications may have access to call it.
This procedure is part of the handle creation chain, and is referenced from the kernel for each handle creation, so it was only natural to take the already decompiled code which we found online.
The issue that has arisen from this piece of code is natural, we currently defined a function in our user-mode process, which will attempt to reference a kernel-mode memory address.
Each usage of *(QWORD*) will attempt to perform a read operation of a QWORD pointer from the given addresses , and as such the natural edit to make in this piece of code is to change each of those expressions to our primitive kernel memory read, and by thus creating a situation in which a non-exported function which serves modules in kernel mode only, will now serve a user mode process and will achieve the same results, which are ideally ,to retrieve the _HANDLE_TABLE_ENTRY object relevant to the handle received in user mode , using its value as the index in the handle table .
After modification the function looks like this:
After using our modified monster, we currently possess the desired _HANDLE_TABLE_ENTRY address, of the target HANDLE we wish to elevate.
A thing that has already been mentioned, is that _HANDLE_TABLE_ENTRY is a union and not a struct.
This means that unlike structs, we will not be able to simply read / write to the struct address + an offset.
The union memory is constructed of overlapping bits.
So, in order to obtain the values of our handle in user-mode memory, we would need to read the handle object Byte-by-Byte according to the union size, and cast it to _HANDLE_TABLE_ENTRY, in order to present and modify the values of the handle – granted access included.
After editing the object in user mode, we would need to patch the handle in the kernel, and as we already have all the information we need to do so, all that is needed is to copy all the bytes back to a Byte Array, and write it back to where we read it from, Byte-by-Byte.
Here are some results from running this code on a handle that belongs to system process (PID 4)
As this simple yet killer code snippet comes to life, we would be able to observe that any handle object on the system including the handles of every process, is vulnerable to this attack.
This means that we can elevate or de-elevate any permission from any handle on the system simply by using read and write operations.
For those of you who will use the code, and not only the solution, a new world of possibilities would come alive, as all of the mentioned in the previous sentence would not even require a handle to begin with, as it can be ANY handle.
And also, for those of you who would embrace this technique with every malicious driver they work on, an immediate and much more efficient privilege escalation would be made, one which does not include requesting specific suspicious privileges which are often detected, leaving the forensic team a much larger effort to understand how in all 7 hells a handle with query_limited_information has been able to dump LSASS, or how a handle of read control has been able to delete a protected file.
In Kernel Cactus you will find implementation for this type of attack, incorporated into further attack scenarios.
Remember! we wrote a library, not only a toolkit. Use it wisely.
Last updated