Learn about how privileged process handles, when inherited, can lead to UAC bypass/privilege escalation.
Introduction
Abusing inherited handles seems like a hot topic recently, so I decided to do some research and blog about it.
First of all, what are handles? Handles are Window’s way of providing access to objects. These objects include processes, threads, tokens, mutexes etc.
Handles are of type HANDLE, as defined below.
1 | typedef PVOID HANDLE; |
Although its type definition seems useless, this is just the POV of user processes. To the kernel, handles are indexes into a handle table, unique to every process. Each row in the handle table is typed SYSTEM_HANDLE
1 | typedef struct _SYSTEM_HANDLE |
and gives the kernel information such as
- dwProcessId: the pid of the process holding the handle
- bObjectType: what object is the handle referring to (process, thread, mutex etc)
- bFlags: some information regarding the handle, such as whether it is inherited from a parent process
- wValue: the actual handle value returned to the user process
- pAddress: address of the object the handle is referring to in kernelspace
- GrantedAccess: type of access the handle has over the object (in terms of process, an example will be PROCESS_ALL_ACCESS for full access)
In essence, if a process possesses a handle to an object, it can perform a range of actions on that object, depending on the access the handle has.
In terms of a process handle, access types include
- PROCESS_ALL_ACCESS: full control over the process
- PROCESS_CREATE_PROCESS: able to create arbitrary processes under that process, thus inheriting its privileges
- PROCESS_CREATE_THREAD: able to run arbitrary code in the process’ context, thus with its privileges too
- PROCESS_DUP_HANDLE: able to duplicate the current handle in the process’ context with arbitrary access rights, including PROCESS_ALL_ACCESS
Now this starts to sound dangerous doesn’t it.
Imagine this scenario. There’s a privileged service running as system on the machine, we call it test.exe
.
test.exe
opens up a handle to a privileged process to do some work, in this case itself, with full privileges.
1 | hProc = OpenProcess(PROCESS_ALL_ACCESS, TRUE, GetCurrentProcessId()); |
It sets the second argument of OpenProcess
to TRUE, which means the handle can be inherited by any child process it creates, if it chooses to.
Afterwards, test.exe
spawns a process client.exe
with the privileges of the logged on user.
This is commonly done for user management services such as SSH services, which have to spawn a shell as the logged on user.
1 | // get PID of user low privileged process |
It does this by getting the token of explorer.exe
, which is the same privilege as the logged on user by default.
If we look closely at the CreateProcessAsUserA
call, one of the arguments is TRUE. This allows the child process client.exe
to inherit all inheritable handles from test.exe
, which includes the process handle to test.exe
.
Due to these insecure programming practices, client.exe
now possesses a token with full access to a privileged process.
If we control client.exe
, we can easily retrieve the process token. However, let’s try to write a tool that can automatically search for such leaked tokens in all the processes running on the system, and exploit them for happiness.
Environment Setup
As mentioned above, we’ll need to simulate a vulnerable service and client in order to create the scenario where this vulnerability is present.
test.c
1 |
|
Found this piece of code somewhere on the internet, I don’t write c++ syntax :p
client.c
1 |
|
Simulates a “shell” process, that runs for a long time.
Now start test.exe
as a service and the setup should run perfectly.
Overview
We’ll use some unexported function and types, so let’s define them first.
1 |
|
The undocumented function is NtQuerySystemInformation()
, which we will resolve at runtime. Other functions are self explanatory and detailed code will be shown later.
We’ll be using two linked lists. One to map pid
, handle
and object address
. The second is to collect potential targets that we’ll attempt to exploit.
Why do we need the first linked list? Because NtQuerySystemInformation()
only gives us the PID of the process possessing the handle. However, we need the PID of the process the handle refers to, in order to check its integrity level and determine whether we should attempt to exploit it. For example, if the process the handle refers to is of lower or same integrity level as us, then we won’t benefit from exploiting it.
By creating the map(linked list), we can map the object address(actual address of the process’ EPROCESS structure in kernelspace) to the pid of the process the handle is pointing to.
This way, when we get the object address from NtQuerySystemInformation()
, we can look up its corresponding pid.
Before going into the details, let’s see the main function first.
1 |
|
The main function is really simple, it just calls all the other functions and does error checking.
resolve_symbols
1 | NTSTATUS resolve_symbols(void) |
In this case we only have one function to resolve, but I wrote it with multiple functions in mind.
init_map
1 | NTSTATUS init_map(void) |
This function does slightly more. First of all it takes a snapshot of all the processes in the system. Then it attempts to open all the processes with the lowest access type required, PROCESS_QUERY_LIMITED_INFORMATION
. This allows us to open even high integrity processes. Online resources state that system processes can also be opened, but I was unable to achieve so.
After opening up all the processes, it fills up the pid
and handle
fields of the linked lists, initializing it so the next function can insert the object addresses at the correct locations.
query_handles
1 | NTSTATUS query_handles(_Out_ PSYSTEM_HANDLE_INFORMATION *handles) |
This is just a helper function to call NtQuerySystemInformation()
and retrieve all the handles currently opened in the system, which of course includes those we opened in init_map()
.
fill_map
1 | NTSTATUS fill_map(_In_ PSYSTEM_HANDLE_INFORMATION handles) |
In this function, we first filter out all handles not belonging to our process. This leaves us with all the handles we opened in init_map()
. Then we fill in the object
field, based on the handle value we retrieved.
Now, the complete mapping of pid
, handle
and object
is completed.
Since each unique pid
has an unique object
address, we can use this to locate the pid that the handle refers to, by looking up the linked list with its object
address.
search_targets
1 | NTSTATUS search_targets(_In_ PSYSTEM_HANDLE_INFORMATION handles) |
This function again uses the data retrieved by NtQuerySystemInformation
. It filters through the handles to locate a potentially exploitable handle.
The handle must satisfy the following conditions:
- It should be a process handle (although thread handles are exploitable too)
- It must currently belong to a medium integrity or lower process, so we can open it and retrieve the handle
- It should have the desired access rights in order for it to be exploitable
- It should be inherited (so there’s a chance for it to be privileged)
- It should refer to a privileged process (we’ll use the linked list to locate the pid, then check its integrity level)
Since we can’t open system processes to map their pids, let’s be optimistic and treat all unknown pids as handles to system processes.
Finally, the targets linked list is populated.
Full Code
The rest of the code are just helper functions, and can be found on the internet as well, so I’m lazy to explain them. Full code can be found on my github. (https://github.com/Y3A/Unhandled)
Conclusion
The curious reader can extend this POC and implement the functions to exploit PROCESS_DUP_HANDLE rights(pretty simple) as well as the search and exploitation of thread handles. However, I found the exploitation of thread handles to be rather unstable across builds, because we have to use ROP gadgets. More information regarding this topic can be found in the references links below, as well as the references links under their blog posts :)