Dump Information for Process using GetTokenInformation
In this post, you will get a very thorough step-by-step walkthrough on building your own process token dumper in the c++ which will help you in knowing your target better before launching another post exploitation attack.
Hello, world! In the previous posts on windows api exploitation for the red-blue team, you have seen that each process has a user and groups associated with it which provides different privileges to each process running in the system and such a system is called a multi-user multi-process operating system.
In the information-gathering phase, the more information you have about the target, the easy it gets to exploit the vulnerabilities on the system. So in this post, I will guide you through how to dump the access token information of the user from the process id.
The source of the topic can be found below ๐
Table of Contents
Overview of Tokens
Before moving forward, let me give you a quick introduction to the tokens. This is will give you a heads up on why I am writing this post and how it can be helpful for you while performing red teaming steps on the target organization.
In simple words โ A token is basically an object that holds authorization information about the user and groups and a set of privileges which is then inherited by a process and then its threads. A process then tends to access certain resources and based on this token the OS then verifies whether the process is allowed to perform actions on the protected resources or not.
How does a Token is assigned to the Session?
Note: This information from the bird's eye. For more details and in-depth knowledge, please goto the links mentioned in the resources section
When a user provides their login credentials, they are securely hashed and provided to the Local Security Authority (LSA). The LSA provides a set of APIs to perform these checks from the SAM database which actually contains the passwords and usernames for the local accounts. If this authentication is done for an account connected to a domain, then LSA simply negotiates the authentication process with the Domain Controller (DC).
Once the authentication succeeds, the LSA will generate a login session for the account and also an access token. Every login session has a Locally Unique ID (LUID
) and multiple access tokens that contain Authentication ID which is then linked to the LUID of the session. This means that there is a Many to One relationship between login sessions and access tokens.
The following image explains the token creation process from the LSA and how this is stored with the login session.
This image has been shamelessly copied from the elastic.co blog and might go down in future.
You can get the logon sessions using query session
command in the cmd prompt
How does a Token is Inherited by the Processes?
After the authentication of a user session is successful, a process is used to create the explorer.exe
in an interactive session and then it exists. During the child process creation, it is inherited by the initial process and then explorer.exe
stays running. Then other processes are started from the parent process explorer.exe which inherits the token information and so on.
For instance, in this case, I have created a user name testuser and after login, I have started powershell.exe
as the first interactive process. In the screenshot below, you can see the process details has explorer.exe
as the parent process with process id 3068.
Let's find all processes other than PowerShell that were started by the explorer.exe
process in the login session of the testuser
account.
Now you must be wondering what is the parent process id of the explorer process itself. Well, that process is already finished and our poor explorer.exe is now an orphan process.
Getting Process and Token Handle
The process of dumping tokens starts with opening the process handle and then getting the process token from that handle. So let's start by implementing this Source.cpp
file.
To get the token of the process it is required to open the process with PROCESS_QUERY_INFORMATION
access rights. Therefore, use this in OpenProcess()
function here
In the case of protected processes, the function will fail when PROCESS_QUERY_INFORMATION
is passed as the desired access right. Therefore to get at least minimal details about the process, let's use PROCESS_QUERY_LIMITED_INFORMATION
access rights. For more details of available access rights, you can check this link.
Once this process handle is opened, print the basic details of the process like its name and process id and parent process id. The code related to this functionality is already implemented in the ProcessDetails.h
file. These details are fetched from the CreateToolhelp32Snapshot
function which is already explained in the Windows Process Listing using ToolHelp32 API.
Let's now get the token from the process using the OpenProcessToken()
function with TOKEN_QUERY
and TOKEN_QUERY_SOURCE
access rights used to query the information from the valid token handle. All the available access rights for the token can be found here.
Dumping Information from the Token Handle
Each part of the token dumping class is defined in the different header files starting with the "Token" in the name. Let's start off in the order of dumping. All the following functions discussed will be using GetTokenInformation()
function with the appropriate value from TOKEN_INFORMATION_CLASS
.
Dumping Session ID
As we have discussed each login has a different set of access tokens and each access token has an authentication id field which is the unique identifier of the session id. But in the system, the session is represented as a DWORD
value. Use TokenSessionId
value from the token information enumeration for this information. The source code of this can be found in the TokenSessionId.h
file
Dumping Statistics (Memory Usage and Token ID)
As you know each token has its unique identity and also contains the information of the login session. Apart from this it also contains the total memory usage and is available to store further information about the access rights of the user. Such details are supposed to be stored somewhere in the token, thus having their place in token statistics. You can find the implementation in the TokenStatistics.h
file. Use TokenStatistics
class from the enumeration which will return the buffer of TOKEN_STATISTICS
structure.
The TokenId
and AuthenticationId
fields are in the form of LUID structure, thus containing the high part and the low part. This is a numerical form but you can print the hexadecimal format with std::hex
and restore the decimal format using std::dec
format specifiers.
The properties DynamicCharged
and DynamicAvailable
contains the information of the size of information stored in the token and total space left in the container to hold more information. These values change on the basis of the type of token, user and privileges that exist or are enabled in it. Since these are in the form of bytes, you can use StrFormatByteSize
which is a macro defined for StrFormatByteSizeW()
function.
Dumping Domain Name and Account Name
Tokens also contain the current user who has been logged in and this token associated with. In fact, when we have tried to get the user from the processes via WTS Api, the function internally looked into the process token to obtain this information. The implementation can be found in the TokenUser.h
.
You can use the TokenUser
property from the enumeration which will return the TOKEN_USER
structure in the buffer containing only one field containing information of the user.
The SID structure has pretty weird fields that aren't in the scope of this post. Since this serialization will be used in groups, so I have created a utility function SIDSerialize(PSID)
which internally calls the ConvertSidToStringSidW()
function.
To free the memory allocated by the serializing function, use the LocalFree()
function.
Similar to the above case, account name and domain name lookup will be used in the following dumping process. Therefore I have created another utility function GetDomainAccount()
to carry out this workload for us. This function internally calls the LookupAccountSidW()
function to get the details from the SID.
The function also returns the type of SID which is then serialized by the GetSidType(SID_NAME_USE)
function from the local utility file.
Dumping Groups Details
To make things easy, the operating system or the administrator assigns certain groups to the user account that provides special privileges to all the users under that umbrella. Since there can be multiple groups to provide multiple rights to the same user, the TokenGroups
enumeration will return the TOKEN_GROUPS
structure containing information of total groups and their SID information. The implementation can be found in the TokenGroup.h
file.
The GroupCount
property will contain the total number of elements in the Groups
array. It can be used to iterate over this list as shown below
Dumping Privileges and Status
We have already discussed privileges in the WTS Api #2, that provides additional access to the processes to do a specific task even though the process is running with an administrator user. It can be dumped this information by providing the TokenPrivileges
enum value in the function, and implemented in the TokenPrivilege.h
file.
This will return a buffer of structure TOKEN_PRIVILEGES
, which contains the PrivilegeCount
field, which will contain the total count of the privileges in the Privileges
array. Remember we used the function to get the LUID of the privilege from its name, well there is a function that will give you the name of the privilege from its LUID, that is LookupPrivilegeNameW
. As a bonus, there is a function used to get the short description of the privileges, like what it is primarily used for. You can get this information via LookupPrivilegeDisplayNameW
.
The description and names of this status can be found here โ https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-token_privileges
Dumping the Token Owner
Well in windows, you can create the resources with certain security descriptors that provide additional access information for them. Such information can be retrieved via TokenOwner
enum value. The implementation can be found in the TokenOwner.h
file.
This will return the information in form of the TOKEN_OWNER
structure containing Owner
field as the reference to the SID of the security descriptor's owner. Let's use our utility function to get the serialized value of the SID and its account details.
Dumping Primary Group of the Token
With the owner details of the token security descriptor, you can also find the group details using TokenPrimaryGroup
value from the enumeration and the implementation can be found in the TokenPrimaryGroup.h
file.
This will return the buffer of type TOKEN_PRIMARY_GROUP
structure and only one SID reference for the group โ PrimaryGroup
. Now call the functions to get the serialised SID string and the account details as shown below.
Dumping Source of the Token
The token is of course issued by the LSA but the actual source can be different. This information is based on the type of user or group is being authenticated and the type of resources that the account is being authenticated for. This information can be retrieved by TokenSource
enum value and is implemented in the TokenSource.h
file.
The buffer returned will be of a type TOKEN_SOURCE
that contains two fields, but the field we are interested in โSourceName
, which is used to identify the token sources. For example, one of the most common values is User32 which is for the tokens created by Session Manager for interactive logins.
Since the LUID consists of two fields โ HighPart
and LowPart
. Both of these are actually numbers and you can print uppercase hexadecimal format by using std::uppercase
and std::hex
cout stream formatters. And to reset this you can use std::dec
for the decimal base of numbers and std::nouppercase
to prevent enforcing uppercase letters during formatting.
Dumping Type of the Token and Impersonation Level
In windows, there are basically two types of tokens โ primary
which are created by the windows kernel and provide default security descriptions, and impersonation
which are created in the userland and is used to create the resources on behalf of another user. You can get whether the specific token of a process is primary and impersonated, by using TokenType
enum value. This is implemented in the TokenType.h
file.
The buffer will return the DWORD
of enum type TOKEN_TYPE
. Additionally, you can print the level of the impersonation, when the token is of impersonation type. This can be done by providing TokenImpersonationLevel
enum value. This will provide you with the buffer of type SECURITY_IMPERSONATION_LEVEL
.
Dumping Elevation Status
If a privileged user logs into the system that can perform privileged actions on the system, then the token must be elevated. You can get this information by providing ย TokenElevationType
enum value in the function and find its implementation in the TokenElevation.h
file.
This will return the data of TOKEN_ELEVATION
structure which contains only one field โ TokenIsElevated
is a DWORD (an overkill I would say) which only contains a nonzero value if the token has elevated privileges; otherwise, a zero value. Well, if this is a truthy value, you can also get the elevation type from it, which will be one of three values in the TOKEN_ELEVATION_TYPE
enumeration.
Dumping Restricted Token Status
With an existing access token, you use the CreateRestrictedToken function to create a new token with restricted or limited access to the resources by relinquishing the one allowed in the original token, thus the token is called a restricted token. You can get this status of whether or not the current is a restricted one using the TokenHasRestrictions
value from the enumeration. This is implemented in the TokenRestriction.h
file.
This will return a DWORD which can be either nonzero if the token is restricted, otherwise a zero value.
Dumping Sandboxing Status
While creating the restricted token, you can pass an additional flag SANDBOX_INERT
to it. If this value is used, the system does not check AppLocker rules or apply Software Restriction Policies. You can get this value by using theTokenSandBoxInert
value from the enum and implementation can be found in the TokenSandBoxInert.h
file.
The buffer receives a DWORD value that is nonzero if the token includes the SANDBOX_INERT
flag.
Code in Action ๐
The following video contains a demonstration of token information dumping of notepad.exe
, elevated powershell.exe
and procexp64.exe
processes and each execution will show a different set of information about users, groups, token privileges etc.
Note: After recording this video the program was changed and so does the output. There will be some difference, but the code works in the similar way.
Resources
- https://www.elastic.co/blog/introduction-to-windows-tokens-for-security-practitioners
- https://docs.microsoft.com/en-us/windows-server/security/windows-authentication/credentials-processes-in-windows-authentication
- https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/query-session
- https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/what-s-in-a-token/ba-p/394954
- https://docs.microsoft.com/en-us/windows/win32/secauthz/access-tokens
- https://docs.microsoft.com/en-us/windows/win32/com/software-restriction-policy
- https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd723678(v=ws.10)
- https://docs.microsoft.com/en-us/windows/win32/secauthz/restricted-tokens
- https://docs.microsoft.com/en-us/windows/win32/secauthn/lsa-logon-sessions
- https://docs.microsoft.com/en-us/windows/win32/secauthz/security-descriptors
- https://en.cppreference.com/w/cpp/io/manip/hex
- https://en.cppreference.com/w/cpp/io/manip/uppercase