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.

Dump Information for Process using GetTokenInformation

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 ๐Ÿ‘‡

WinAPI-RedBlue/Token Dump at main ยท tbhaxor/WinAPI-RedBlue
Source codes of Windows API Exploitation for Red and Blue teams from Pentester Academy - WinAPI-RedBlue/Token Dump at main ยท tbhaxor/WinAPI-RedBlue
๐Ÿ’ก
This post will be so long, that could be broken into different parts. To avoid breaking your attention span I have decided to keep this as it is. Please use the following table in order to navigate to a specific topic.

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

Getting login session for services and console. In the current picture, the console session is active and has two logins

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.

Process ID 3068 is the parent process of powershell.exe process. Thus all the privileges are inherited from the explorer.exe

Let's find all processes other than PowerShell that were started by the explorer.exe process in the login session of the testuser account.

Processes running with the parent process of explorer.exe

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.

The parent process of explorer.exe is terminated providing it the access token

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

HANDLE hProc = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, dwPID);
if (hProc == NULL || hProc == INVALID_HANDLE_VALUE) {
	hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, dwPID);
	if (hProc == NULL || hProc == INVALID_HANDLE_VALUE) {
		PrintError(L"OpenProcess()", FALSE);
	}
}
Try to get the process handle with query information access rights

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.

HANDLE hTok;
if (!OpenProcessToken(hProc, TOKEN_QUERY | TOKEN_QUERY_SOURCE, &hTok)) {
	PrintError(L"OpenProcessToken()", TRUE);
}
Getting a valid token handle from the process's handle with query access rights.

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

if (!GetTokenInformation(hTok, TokenSessionId, &dwSessionId, dwTokLen, &dwRetLen)) {
	PrintError(L"GetTokenInformation()", FALSE);
} else {
	std::wcout << L"[+] Logon Session ID: " << dwSessionId << std::endl;
}
Get the session id stored in the token object

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.

std::wcout << L"\tToken ID: 0x" << std::hex << std::uppercase << pStats->TokenId.HighPart << L"-" << pStats->TokenId.LowPart << std::nouppercase << std::dec << std::endl;
std::wcout << L"\tAuthentication ID: 0x" << std::hex << std::uppercase << pStats->AuthenticationId.HighPart << L":" << pStats->AuthenticationId.LowPart << std::nouppercase  << std::dec << std::endl;
		
Printing the locally unique token id and the authentication id which is the unique id of the logon session

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.

StrFormatByteSize(pStats->DynamicCharged, lpDynamicCharged, MAX_PATH);
StrFormatByteSize(pStats->DynamicAvailable, lpDynamicAvailable, MAX_PATH);
StrFormatByteSize(pStats->DynamicCharged + pStats->DynamicCharged, lpDynamicTotal, MAX_PATH);

std::wcout << L"\tMemory Usage: " << lpDynamicCharged << L" / " << lpDynamicTotal << L"\t(" << lpDynamicAvailable << L" left)" << std::endl;
std::wcout << L"\tTotal Groups Count: " << pStats->GroupCount << std::endl;
std::wcout << L"\tTotal Privileges Count: " << pStats->PrivilegeCount << std::endl;
Formatting the bytes into human-readable format and printing

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.

if (!GetTokenInformation(hTok, TokenUser, (LPVOID)tu, dwTokLength, &dwRetLen)) {
	PrintError(L"GetTokenInformation()");
	return;
}
Populate the buffer with the user SID details

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.

LPWSTR lpSid = SIDSerialize(tu->User.Sid);
if (lpSid == nullptr) {
	PrintError(L"SIDSerialize()");
} else {
	std::wcout << L"\tSID: " << lpSid << std::endl;
}
LocalFree(lpSid);
lpSid = nullptr;
Serialising the SID object to a human-readable string 
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.

WCHAR wAcc[MAX_PATH], wDom[MAX_PATH];
SID_NAME_USE eSidType;
if (!GetDomainAccount(tu->User.Sid, wAcc, wDom, &eSidType)) {
	PrintError(L"GetDomainAccount()");
} else {
	std::wcout << L"\tDomain\\Account (Type): " << wDom << L"\\" << wAcc << L" (" << GetSidType(eSidType) << L")" << std::endl;
}
Get the account name and the account domain from the SID along with the type of account

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.

if (!GetTokenInformation(hTok, TokenGroups, (LPVOID)tg, dwTokLen, &dwRetLen)) {
	PrintError(L"GetTokenInformation()");
	return;
}
Get the information of the groups from token

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

for (DWORD c = 0;c < tg->GroupCount;c++) {
	std::wcout << L"\t[" << std::setw(2) << c + 1 << L"] ";
		
	// Get the SID string for SID pointer
	LPWSTR lpSid = SIDSerialize(tg->Groups[c].Sid);
	if (lpSid == nullptr) {
		PrintError(L"SIDSerialize()");
	} else {
		std::wcout << L"SID: " << lpSid << std::endl;
	}
	LocalFree(lpSid);
	lpSid = nullptr;

	// Get the account name, domain name and its type from the SID value
	WCHAR wAcc[MAX_PATH], wDom[MAX_PATH];
	SID_NAME_USE eSidType;
	if (!GetDomainAccount(tg->Groups[c].Sid, wAcc, wDom, &eSidType)) {
		PrintError(L"GetDomainAccount()");
	} else {
		std::wcout << L"\t     Domain\\Account (Type): " << wDom << L"\\" << wAcc << L" (" << GetSidType(eSidType) << L")" << std::endl;
	}
}
Printing the SID information of the group's account

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.

if (!GetTokenInformation(hTok, TokenPrivileges, (LPVOID)tp, dwTokLen, &dwRetLen)) {
	PrintError(L"GetTokenInformation()");
	return;
}
Getting the token privileges in the buffer

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.

for (DWORD c = 0; c < tp -> PrivilegeCount; c++) {
    LPWSTR lpName = (LPWSTR) VirtualAlloc(NULL, 1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    LPWSTR lpDisplayName = (LPWSTR) VirtualAlloc(NULL, 1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    DWORD dwName, dwLangId, dwDisplayName;
    dwName = dwDisplayName = 1000;

    std::wcout << "\t[" << std::setw(2) << c + 1 << L "] ";

    // Get the name of the privilege from LUID
    if (!LookupPrivilegeNameW(NULL, & tp -> Privileges[c].Luid, lpName, & dwName)) {
        PrintError(L "LookupPrivilegeNameW()");
        continue;
    }
    std::wcout << L "Name: " << lpName << std::endl;

    // Get the description / display for the privilege by its name
    if (!LookupPrivilegeDisplayNameW(NULL, lpName, lpDisplayName, & dwDisplayName, & dwLangId)) {
        PrintError(L "LookupPrivilegeNameW()");
        continue;
    }
    std::wcout << L "\t     Description: " << lpDisplayName << std::endl;

    switch (tp -> Privileges[c].Attributes) {
    case SE_PRIVILEGE_ENABLED:
        std::wcout << L "\t     Status: Enabled\n";
        break;
    case SE_PRIVILEGE_ENABLED_BY_DEFAULT:
        std::wcout << L "\t     Status: Enabled by Default\n";
        break;
    case SE_PRIVILEGE_ENABLED | SE_PRIVILEGE_ENABLED_BY_DEFAULT:
        std::wcout << L "\t     Status: Enabled by Default\n";
        break;
    case SE_PRIVILEGE_REMOVED:
        std::wcout << L "\t     Status: Removed\n";
        break;
    case SE_PRIVILEGE_USED_FOR_ACCESS:
        std::wcout << L "\t     Status: Used for Access\n";
        break;
    case 0x0:
        std::wcout << L "\t     Status: Disabled\n";
        break;
    default:
        std::wcout << L "\t     Status: N/A\n";
    }

    VirtualFree(lpName, 0x0, MEM_RELEASE);
    VirtualFree(lpDisplayName, 0x0, MEM_RELEASE);
    lpName = nullptr;
    lpDisplayName = nullptr;
}
Printing the privilege details and their status

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.

if (!GetTokenInformation(hTok, TokenOwner, (LPVOID)to, dwTokLen, &dwRetLen)) {
	PrintError(L"GetTokenInformation()");
	return;
}
Getting the security descriptor owner for the current token in the buffer

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.

LPWSTR lpSid = SIDSerialize(to -> Owner);
if (lpSid == nullptr) {
    PrintError(L "SIDSerialize()");
} else {
    std::wcout << L "\tSID: " << lpSid << std::endl;
}
LocalFree(lpSid);
lpSid = nullptr;

WCHAR wAcc[MAX_PATH], wDom[MAX_PATH];
SID_NAME_USE eSidType;
if (!GetDomainAccount(to -> Owner, wAcc, wDom, & eSidType)) {
    PrintError(L "GetDomainAccount()");
} else {
    std::wcout << L "\tDomain\\Account (Type): " << wDom << L "\\" << wAcc << L " (" << GetSidType(eSidType) << L ")" << std::endl;
}
Serialize SID from the Owner field and print 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.

if (!GetTokenInformation(hTok, TokenPrimaryGroup, (LPVOID) tp, dwTokLen, & dwRetLen)) {
    PrintError(L "GetTokenInformation()");
    return;
}
Getting the primary group details for the security descriptors of the token

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.

LPWSTR lpSid = SIDSerialize(tp -> PrimaryGroup);
if (lpSid == nullptr) {
    PrintError(L "SIDSerialize()");
} else {
    std::wcout << L "\tSID: " << lpSid << std::endl;
}
LocalFree(lpSid);
lpSid = nullptr;

WCHAR wAcc[MAX_PATH], wDom[MAX_PATH];
SID_NAME_USE eSidType;
if (!GetDomainAccount(tp -> PrimaryGroup, wAcc, wDom, & eSidType)) {
    PrintError(L "GetDomainAccount()");
} else {
    std::wcout << L "\tDomain\\Account (Type): " << wDom << L "\\" << wAcc << L " (" << GetSidType(eSidType) << L ")" << std::endl;
}
Serialise the primary group sid and get the account details from it

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.

if (!GetTokenInformation(hTok, TokenSource, (LPVOID)ts, dwTokLen, &dwRetLen)) {
	PrintError(L"GetTokenInformation()");
	return;
}
Getting the token source details in the buffer ts

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.

std::cout << "[+] Token Source " << std::endl;
std::cout << "\tSource Name: " << ts->SourceName << std::endl;
std::cout << "\tSource ID: " << std::hex << std::uppercase << ts->SourceIdentifier.HighPart << "-" << ts->SourceIdentifier.LowPart << std::nouppercase << std::dec << std::endl;
Printing the details of the token source

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.

if (!GetTokenInformation(hTok, TokenType, (LPVOID)t, dwTokLen, &dwRetLen)) {
	PrintError(L"GetTokenInformation()");
	return;
}
Getting the token type in the buffer

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.

if (!GetTokenInformation(hTok, TokenImpersonationLevel, (LPVOID) il, dwTokLen, & dwRetLen)) {
    PrintError(L "GetTokenInformation()");
} else {
    std::wcout << "[+] Impersonation Level: ";
    switch ( * il) {
    case SecurityAnonymous:
        std::wcout << L "Anonymous - Cannot obtain identification information about the client\n";
        break;
    case SecurityIdentification:
        std::wcout << L "Identification - Can obtain information about the client, but not impersonate it\n";
        break;
    case SecurityImpersonation:
        std::wcout << L "Impersonation - Can impersonate the client's security context on its local system\n";
        break;
    case SecurityDelegation:
        std::wcout << L "Delegation - Can impersonate the client's security context on remote systems\n";
        break;
    default:
        std::wcout << L "N/A\n";
        break;
    }
}
Getting the token impersonation level, if the token is type is impersonation

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.

if (!GetTokenInformation(hTok, TokenElevation, (LPVOID)te, dwTokLen, &dwRetLen)) {
	PrintError(L"GetTokenInformation()");
	return;
}
Getting the token elevation status value in the buffer.

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.

PTOKEN_ELEVATION_TYPE t = (PTOKEN_ELEVATION_TYPE) VirtualAlloc(nullptr, dwTokLen, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!GetTokenInformation(hTok, TokenElevationType, (LPVOID) t, dwTokLen, & dwRetLen)) {
    PrintError(L "GetTokenInformation()");
} else {
    switch ( * t) {
    case TokenElevationTypeDefault:
        std::wcout << "\tType: Default - The token does not have a linked token\n";
        break;
    case TokenElevationTypeFull:
        std::wcout << "\tType: Full - The token is an elevated token\n";
        break;
    case TokenElevationTypeLimited:
        std::wcout << "\tType: Limited - The token is a limited token\n";
        break;
    default:
        break;
    }
}
Getting the type of token's elevation type

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.

if (!GetTokenInformation(hTok, TokenHasRestrictions, (LPVOID)lpHasRestriction, dwTokLen, &dwRetLen)) {
	PrintError(L"GetTokenInformation()");
	return;
}
Getting the boolean status whether the token is restricted or not

This will return a DWORD which can be either nonzero if the token is restricted, otherwise a zero value.

if ( * lpHasRestriction) {
    std::wcout << L "[+] Is Token Restricted: Yes\n";
} else {
    std::wcout << L "[+] Is Token Restricted: No\n";
}
Dereferencing the pointer and checking the status

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.

if (!GetTokenInformation(hTok, TokenSandBoxInert, lpSandBoxInert, dwTokLen, & dwRetLen)) {
    PrintError(L "GetTokenInformation()", FALSE);
} else if ( * lpSandBoxInert) {
    std::wcout << L "[+] Sandbox Inert: Exists in Token\n";
} else {
    std::wcout << L "[+] Sandbox Inert: Does not exists in Token\n";
}
Getting the status of the presence of SANDBOX_INERT flag in the restricted token

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