Understanding and Abusing Process Tokens — Part I
Introduction
This blog is all about processes, process tokens, and token abuse. We will start with a brief overview of processes and how to create and open them. Then we will steer into process tokens and how the token privileges can be abused to escalate privileges.
Things that the audience of this blog will learn:
- What exactly is the process?
- Different types of processes.
- How to create a process?
- How to open a process?
- What are the minimum privileges required to open any process?
- What are the access tokens?
- How to read access token values in user mode and kernel mode?
- What are the minimum privileges required to open a process token?
- What are seDebugPrivilege and its importance?
- What are the minimum requirements to impersonate the security context of a client’s logon?
- How to impersonate a Token to gain NT Authority privilege?
- What privileges could be abused to perform privilege escalation?
Overview: Process
As per Microsoft documentation, “A process, in the simplest terms, is an executing program.”
The process is a management containment object as shown below with the following attributes:
- It has a private address space, which is used to allocate memory for loading DLLs, heaps, thread stacks, and whatever else is related to memory.
- A private handle table that allows communicating with kernel objects, such as files, mutexes, and so on.
- A private working set within a process, which is the amount of physical memory used by the process, something that the memory manager keeps track of.
- An access token, which is a security context under which this process and its threads are run by default.
- Threads or thread pool that efficiently executes code or asynchronous callbacks on behalf of the application.
Let’s get into the various available process functions which will provide us insights about the process attributes mentioned above:
CreateProcess: This function is used to create a process in user mode.
Create Process as a user: This function creates a new process and its primary thread. The new process runs in the security context of the user represented by the specified token.
EPROCESS: Once the CreateProcess function is invoked in user mode, these created processes will be managed by a structure called an Executive Process or EPROCESS in Kernel Mode. This structure holds all the necessary information to manage a process.
KPROCESS: Within EPROCESS another structure called a KPROCESS is defined, which holds information that is pertinent to the lower layer kernel below the executive. This includes stuff related to thread scheduling.
Let’s create a process from scratch to understand what is the theory mentioned above:
Code:
#include <windows.h>#include <stdio.h>#include <tchar.h>void _tmain(int argc, TCHAR *argv[]){STARTUPINFO si;PROCESS_INFORMATION pi;ZeroMemory(&si, sizeof(si)); // We have to zero down the memory, otherwise there will be garbage valuesi.cb = sizeof(si);ZeroMemory(&pi, sizeof(pi));if (argc != 2){printf(“Usage: %s [cmdline]\n”, argv[0]);return;}// Start the child process.if (!CreateProcess(NULL, // No module name (use command line)argv[1], // Command lineNULL, // Process handle not inheritableNULL, // Thread handle not inheritableFALSE, // Set handle inheritance to FALSE0, // No creation flagsNULL, // Use parent’s environment blockNULL, // Use parent’s starting directory&si, // Pointer to STARTUPINFO structure&pi) // Pointer to PROCESS_INFORMATION structure){printf(“CreateProcess failed (%d).\n”, GetLastError());return;}printf(“PID: (%d).\n”, pi.dwProcessId);// Wait until child process exits.WaitForSingleObject(pi.hProcess, INFINITE);DWORD code;GetExitCodeProcess(pi.hProcess, &code);printf(“Exit Code: (%d).\n”, code);// Close process and thread handles.CloseHandle(pi.hProcess);CloseHandle(pi.hThread);}
[Ref.]: https://docs.microsoft.com/en-us/windows/win32/procthread/creating-processes
I have used Visual Studio to compile the code as shown below:
As we can see in the code, ‘if’ statement is used with the CreateProcess function which will return a Boolean value ‘True’ if the process is created else false.
Let’s take a look into the CreateProcess function syntax:
BOOL CreateProcessA(LPCSTR lpApplicationName, // In our code above, we are taking user input. Therefore, we can assign NULL valueLPSTR lpCommandLine, // User input via command line. Therefore argv[1].LPSECURITY_ATTRIBUTES lpProcessAttributes, // Whether the returned handle to the new process object can be inherited by child processes. We can assign NULL for the default security descriptors.LPSECURITY_ATTRIBUTES lpThreadAttributes, //For this as well we can assign NULL for the default security descriptors.BOOL bInheritHandles, // For now no inheritance required for the child process or threads. We can assign FALSE as it is bool type.DWORD dwCreationFlags, // These flags control the priority class, which is used to determine the scheduling priorities of the process's threads. We can set default value by assigning zero ‘0’.LPVOID lpEnvironment, // A pointer to the environment block for the new process. If this parameter is NULL, the new process uses the environment of the calling process.LPCSTR lpCurrentDirectory, // This specifies the full path to the current directory for the process. If this parameter is NULL, the new process will have the same current drive and directory as the calling process.LPSTARTUPINFOA lpStartupInfo, // Specifies the window station, desktop, standard handles, and appearance of the main window for a process at creation time. This is a required field for invoking a CreateProcess function. Therefore, we have to define the STARTINFO at the starting.LPPROCESS_INFORMATION lpProcessInformation // Contains information about a newly created process and its primary thread.);
Now we know, what all is primarily required for creating a process. One thing we have to keep in mind that after creating a process or thread, we have to define close handle’s to process/ thread as well. That will execute once the task is finished.
Demo: Let’s execute the output executable, to create a notepad process.
In the task manager, we can see that the Notepad process is created:
The Process ID (PID) for notepad.exe is 6664. This is the process we created. Additionally, we are invoking this function within CreateProcess.exe executable. Therefore, there will be a process running with the name ‘CreateProcess.exe’ which will be the parent process for the notepad.
Let’s take look at notepad.exe process properties using process explorer (a Sysinternals tool).
In our CreateProcess function code, we have assigned NULL values to LPSECURITY_ATTRIBUTES. Let’s see how it is reflected in our newly created Notepad process.
Below we can see the security descriptor of the parent process which is exactly the same for Notepad process as we assigned NULL value for inheriting default privileges of the owing process:
Now we observe the process from Kernel mode, for this, we will start Kernel Debugger and look for our created process. Here, we will gain an understanding of the Executive process structure and Kernel process structure that manages the processes in kernel mode.
For listing all the processes, we use “!process 0 0” command.
In the above screenshot, the value (ffffa38ec93a4080) mentioned next to PROCESS is the address of the EPROCESS structure. This EPROCESS structure manages this (notepad) process. To view the process structure, we will run “!process ffffa38ec93a4080” command, as shown below:
Note: This is the same information that we can view in Process Explorer. Additionally, most of the flags shown above are kernel specific which is undocumented in the Windows Driver Kit (WDK) headers.
As we have mentioned in the starting, that process is a management containment object. This containment object is managed via EPROCESS in Kernel mode. Let’s look at EPROCESS structure and what it contains:
Command to display EPROCESS structure is “dt NT!_EPROCESS”.
Note: EPROCESS Contains, Process Control Broker (PCB) which is of type K process means the kernel process structure.
This KPROCESS contains a Dispatcher header and _LIST_ENTRY which is a doubly Linked list that tells how all the processes within the operating system are linked.
Similarly, in user mode, we have something called the Process Environment Block, or PEB, which stores information about the process which is either not interesting enough for the kernel or would require going through kernel mode too many times and so is cached within the Process Environment Block.
Note: This is the PEB value for Notepad process.
Now, we have enough understanding of the Process and its structure both in user mode and kernel mode.
How to Open a Process using OpenProcess() function and why it is required?
The process structure holds the access token attribute that tells about the security context of the running thread. Let’s move forward to understand the security context and what it contains. Before starting access tokens, we have to understand how processes can be interacted or opened and how we can read the access tokens so that we can play around with it. When a user logs in, the system collects a set of data that uniquely identifies the user during the authentication process, and stores it in an access token. This access token describes the security context of all processes associated with the user.
Let’s begin with the OpenProcess function:
OpenProcess(): Opens an existing local process object.
Syntax:
HANDLE OpenProcess(DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId);
The OpenProcess() function accepts three inputs which are explained below:
- dwDesiredAccess: Access to the process object. This access right is checked against the security descriptor for the process. If the caller has enabled the SeDebugPrivilege privilege, the requested access is granted regardless of the contents of the security descriptor.
- bInheritHandle: If this value is TRUE, processes created by this process will inherit the handle. Otherwise, the processes do not inherit this handle.
- dwProcessId: The identifier of the local process to be opened.
Note: If the specified process is the System Process (0x00000000), the function fails and the last error code is ERROR_INVALID_PARAMETER. If the specified process is the Idle process or one of the CSRSS processes, this function fails and the last error code is ERROR_ACCESS_DENIED because their access restrictions prevent user-level code from opening them.
Let’s understand the meaning of above parameters through a demo:
The below screenshot shows the code to invoke OpenProcess() Function.
Compile the program to generate the output executable as shown below:
Code: [OpenProcess()]
#include <windows.h>#include <iostream>#include <Lmcons.h>std::string get_username(){// Snippet to get the current usernameTCHAR username[UNLEN + 1];DWORD username_len = UNLEN + 1;GetUserName(username, &username_len);std::wstring username_w(username);std::string username_s(username_w.begin(), username_w.end());return username_s;}int main(int argc, char** argv){// Printing Curent Usernameprintf("[+] Current user is: %s\n", (get_username()).c_str());// Grab PID from command line argument of the process you want openchar *pid_c = argv[1];DWORD PID = atoi(pid_c);// Call OpenProcess(), print return code and error code//dwDesiredAccess à PROCESS_QUERY_INFORMATION: Required to retrieve certain information about a process, such as its token, exit code, and priority class//bInheritHandle à True: Processes created by this process will inherit the handle//dwProcessId à Process ID to be provided as user inputHANDLE processHandle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, true, PID);if (GetLastError() == NULL)printf("[+] OpenProcess() success!\n");else{printf("[-] OpenProcess() Return Code: %i\n", processHandle);printf("[-] OpenProcess() Error: %i\n", GetLastError());}return 0;}
In the above code, we are taking process identifier as input to get the process handle. In the OpenProcess function, we have assigned PROCESS_QUERY_LIMITED_INFORMATION as dwDesiredAccess which is an access right. The access check takes into account whether the calling process would have PROCESS_QUERY_LIMITED_INFORMATION access to the target process which is a prerequisite for opening the Token. As per Microsoft documentation, there are two access rights that can be assigned to retrieve certain information about a process.
Note: The MSDN documentation has incorrectly mentioned that the minimum access rights required to open the process are PROCESS_QUERY_INFORMATION. This has already been highlighted by many security folks already.
Let’s take a look into the actual minimum required rights to Open the process in Kernel mode:
Note: Access Mask 0x1000 means PROCESS_QUERY_LIMITED_INFORMATION.
Open command prompt. Check the owner of the cmd.exe process, under which the task is running:
Now, run the Demo_OpenProcess.exe with LSASS.exe PID. Here, we will consider two scenarios:
- Using OpenProcess function with the process running under same owner
- Using OpenProcess function with the process running under System owner
With the Same owner:
Cmd.exe is running under the Pentest user. Also, WINWORD.exe is running under the Pentest username.
Privileges of Pentest User are as under:
Note: Even though the user in question is a local administrator, the unelevated cmd.exe shell carries a token restricted to only a handful of privileges. This type of access token is also called as “Restricted Access Token”. When elevated to run as administrator, the process carries the user’s primary token with a larger list of privileges.
It means if we run Demo_OpenProcess.exe without elevation, the process will carry above mentioned limited privileges.
Running Demo_OpenProcess.exe with parameter as Winword.exe process PID (7680):
We can observe that the OpenProcess function was able to open the process object. It means that the Demo_OpenProcess.exe is running with the same or higher privileges than the winword.exe process.
Note: It doesn’t mean all the process objects that run under the same user account can be opened. It totally depends on the parent process or the Owner of the process. Some processes require seDebug privileges to open the process. It means we can open any process in windows with appropriate required rights.
We will open two command prompts, one with administrator privileges and one without and compare the privileges assigned by default.
Let’s take a look at process explorer as well.
When we run cmd.exe with Administrator privileges the BUILTIN\Administrators flag is set to Owner. Which means cmd.exe is running in the security context of Administrator privileges. Through which we can perform below-mentioned operations:
- Impersonate a client after authentication using SeImpersonatePrivilege
- Debug programs (By default: Administrator account is added in the User Rights Assignment — Debug Programs)
We now understand how to open the process and get the process handle and also that we need seDebug privilege to open some of the protected processes such as CSRSS.exe.
How to Open a Process Token using OpenProcessToken() function and why it is required?
Now we will try to open a process and pass the process handle to the OpenAccessToken function. So that we can read the Access Token.
OpenAccessToken()
Let’s check the OpenProcessToken() function to open the access token associated with the process.
Syntax:
BOOL OpenProcessToken(HANDLE ProcessHandle, DWORD DesiredAccess, PHANDLE TokenHandle);
In the above syntax, OpenProcessToken() function takes three inputs described below:
- ProcessHandle: A handle to the process whose access token is opened. The process must have the PROCESS_QUERY_LIMITED_INFORMATION access permission, which we have already learned while calling OpenProcess() function.
- DesiredAccess: Specifies an access mask (i.e., privilege attributes) that specifies the requested types of access to the access token. These requested access types are compared with the discretionary access control list (DACL) of the token to determine which accesses are granted or denied.
- TokenHandle: A pointer to a handle that identifies the newly opened access token when the function returns.
Now, let’s take a look at the code:
Code: [OpenProcessToken()]
#include <windows.h>#include <iostream>#include <Lmcons.h>std::string get_username(){// Snippet to get the current usernameTCHAR username[UNLEN + 1];DWORD username_len = UNLEN + 1;GetUserName(username, &username_len);std::wstring username_w(username);std::string username_s(username_w.begin(), username_w.end());return username_s;}int main(int argc, char** argv){// Printing Curent Usernameprintf("[+] Current user is: %s\n", (get_username()).c_str());// Grab PID from command line argumentchar *pid_c = argv[1];DWORD PID = atoi(pid_c);// A pointer to a handle that identifies the newly opened access token when the function returns.HANDLE currentTokenHandle = NULL;// Call OpenProcess(), print return code and error code//dwDesiredAccess à PROCESS_QUERY_INFORMATION: Required to retrieve certain information about a process, such as its token, exit code, and priority class//bInheritHandle à True: Processes created by this process will inherit the handle//dwProcessId à Process ID to be provided as user inputHANDLE processHandle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, true, PID);if (GetLastError() == NULL)printf("[+] OpenProcess() success!\n");else{printf("[-] OpenProcess() Return Code: %i\n", processHandle);printf("[-] OpenProcess() Error: %i\n", GetLastError());}BOOL getToken = OpenProcessToken(processHandle, TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_QUERY, ¤tTokenHandle);if (GetLastError() == NULL)printf("[+] OpenProcessToken() success!\n");else{printf("[-] OpenProcessToken() Return Code: %i\n", getToken);printf("[-] OpenProcessToken() Error: %i\n", GetLastError());}return 0;}
In the above code, OpenProcess() function takes a process identifier (PID) and returns a process handle. The process handle must be opened with the PROCESS_QUERY_INFORMATION, PROCESS_QUERY_LIMITED_INFORMATION, or the PROCESS_ALL_ACCESS access right to be useable with OpenProcessToken().
Here in our code, we are using PROCESS_QUERY_LIMITED_INFORMATION.
Note: We are able to successfully open the access token associated with the LSASS process (PID 784)
Before understanding how to open process tokens, we have to understand about different types of processes:
Few processes within Operating System runs with Protected rights and are called Protected processes. The purpose of the protected process was to host Digital Rights Management content, which should not be visible by any other process which would be a violation of the DRM license. So, a protected process can only be opened with limited access only. For example, there’s no way to read the address space or the contents of memory within that process. Not even by admin-level processes. So, there are only very few operations that are possible to do with a protected process. And, of course, these types of processes can only load protected-signed DLLs, signed in a very special certificate, and of course, that’s important, because otherwise some other DLL may be masquerading as the real DLL and be loaded inside such a process and violate all the protection protected process is supposed to have. The concept of Protected Processes was further extended and the new process model was formed which is called Protected Process Model and was first introduced in Windows 8.1. Below are the protected processes from my Windows 10 operating system.
Note: The Protection column above shows the Protected Process signers. Each signer comes with its own privileges. I will explain all the signers in more detail in the next section.
In the above screenshot, we can see the Protected Processes are assigned with a protection attribute. This protection attribute or flag is stored in EPROCESS structure which we can view through kernel debugger. In the kernel mode, one can tamper around with EPROCESS structure using kernel-mode drivers. From user mode, an attacker with the admin privilege can inject kernel-mode driver that can access anything within Protected Process or Protected Process Light. Though it would be a violation of the Protected Media Path (PMP) model and considered malicious, and such a driver would likely eventually be blocked. Code Signing policy in the kernel-mode prohibits the digital signing of the malicious code.
Below table shows the various Protection symbols along with its signer attribute, priority level (higher values denote the signer is more powerful) and where they are being used:
For example, let’s take a look at the EPROCESS structure of CSRSS process and its protection attribute:
On clicking the Protection link shown in the screenshot above, it takes us to the Protection attribute definition:
Note: From the above table, we know that the access mask 0x61 is for PS_PROTECTED_WINTCB_LIGHT. Therefore, we can see that the CSRSS process is protected with WINTCB_LIGHT signer. Similarly, as per the required privileges, different protected processes are assigned with different signers.
Now, we have an understanding of the protected processes as well. Let’s try to OpenProcessToken for different processes and try to understand whether it is possible to open process tokens of all the processes or not.
We will consider a few scenarios for this:
- Try to OpenProcessToken of a process running under NT Authority and with protection set to ‘NONE’.
- Try to OpenProcessToken of a process running under NT Authority along with protection set to one of the signer flags.
- Try to OpenProcessToken of a SYSTEM process.
Scenario 1: Try to OpenProcessToken of a process running under NT Authority and with protection attribute set to ‘NONE’.
For this, I will be considering WINLOGON.exe process:
PID: 1056
Running the code:
Note: We are able to open the process token of the winlogon.exe process running under NT Authority/SYSTEM.
We can also check for what all privileges Administrator security context have over winlogon.exe process through process explorer or process hacker:
Scenario 2: Try to OpenProcessToken of a process running under NT Authority along with protection set to one of the signer flags.
For this, I will be considering Sgrmbroker.exe (System Guard Runtime Monitor Broker Service) process running under NT Authority/SYSTEM with a protection bit set to PsProtectedSignerWinTcb.
PID: 7028
Note: We can see that OpenProcess handle running with the security context of administrator privilege is not able to Open the process token.
Let’s take a look at the advanced permissions of the Administrator user for the sgrmBroker.exe process.
It means the Administrator is only allowed to query information. We can check process Token privilege as well via the process hacker.
Administrator privilege can only query the token. For this scenario, we can OpenProcessToken using TOKEN_QUERY DesiredAccess right.
Code:
BOOL getToken = OpenProcessToken(processHandle, TOKEN_QUERY , ¤tTokenHandle);
Note: The administrator account only has Token query access right. Therefore, we cannot abuse these types of processes via the administrator account security context, we need at least duplicate and Impersonate privilege.
I tried to open the process tokens of all the protected processes, the results are mentioned in the table below:
seDebugPrivilege*: You can retrieve a handle to any process in the system by enabling the SeDebugPrivilege in the calling process. The calling process can then call the OpenProcess() Win32 API to obtain a handle with PROCESS_ALL_ACCESS, PROCESS_QUER_INFORMATION, or PROCESS_QUERY_LIMITED_INFORMATION. Below mentioned code will set seDebugPrivilege on the calling process.
Code: seDebugPrivilege
BOOL SetPrivilege(HANDLE hToken, // access token handleLPCTSTR lpszPrivilege, // name of privilege to enable/disableBOOL bEnablePrivilege // True to enable. False to disable.){TOKEN_PRIVILEGES tp;LUID luid;if (!LookupPrivilegeValue(NULL, // lookup privilege on local systemlpszPrivilege, // privilege to lookup&luid)) // receives LUID of privilege{printf("[-] LookupPrivilegeValue error: %u\n", GetLastError());return FALSE;}tp.PrivilegeCount = 1;tp.Privileges[0].Luid = luid;if (bEnablePrivilege)tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;elsetp.Privileges[0].Attributes = 0;// Enable the privilege or disable all privileges.if (!AdjustTokenPrivileges(hToken,FALSE,&tp,sizeof(TOKEN_PRIVILEGES),(PTOKEN_PRIVILEGES)NULL,(PDWORD)NULL)){printf("[-] AdjustTokenPrivileges error: %u\n", GetLastError());return FALSE;}if (GetLastError() == ERROR_NOT_ALL_ASSIGNED){printf("[-] The token does not have the specified privilege. \n");return FALSE;}return TRUE;}
Scenario 3: Try to OpenProcessToken of a SYSTEM process.
Following are the general permissions which administrator user have over SYSTEM:
We have enough privilege to open the process. Let’s check the privileges required to open the process token.
Note: Administrator account doesn’t have any privilege on the SYSTEM process token. Therefore, we cannot open a process token from an administrative security context.
So far, we have built an understanding of process creation, opening a handle to a process, and passing the handle to open the process token. We also learned, how to modify the OpenProcess and OpenProcessToken functions for accessing various protected processes. By looking at the permissions, we can at least discover whether our calling process has the privilege to open process token, and if yes, whether the privileges exposed are limited or can be abused.
In Part II, we will continue with Impersonate privilege and how we can abuse it to gain NT Authority/ SYSTEM access. Additionally, we will look into other privileges as well which can be abused and in what manner. Thanks for the read.
References:
https://www.tiraniddo.dev/2017/05/reading-your-way-around-uac-part-2.html?m=1
Windows Internal 1, 2, 3 [Videos by Pavel Yosifovich]
Windows 10 Internals — Systems and Processes [Videos by Pavel Yosifovich]
Windows 10 Internals — Threads, Memory, and Security [Videos by Pavel Yosifovich]
[OpenProcess()]
[OpenProcessToken()]
https://docs.microsoft.com/en-us/windows/win32/secauthz/access-rights-for-access-token-objects
https://medium.com/palantir/windows-privilege-abuse-auditing-detection-and-defense-3078a403d74e
Note: In case I have missed any reference, please let know and I’ll add that to the list. Apologies in advance. Happy Learning!😊