Click here to Skip to main content
15,878,945 members
Articles / Desktop Programming / Win32

Win32 Security: Retrieving Privileges and Groups for an Access Token

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
22 Sep 2022MIT7 min read 8.5K   321   8   2
This article shows how to retrieve the Privileges and Groups that are part of an access token such as the current thread token.
This article shows how we can read the thread token and inspect the Privileges and Groups that are assigned to it. Aside from the things we can do with a token, it can be convenient to be able to display them for troubleshooting purposes. In this article, I will show how to retrieve that information.

Introduction

I'm working on some code which will run in kiosk mode, where a user will log in with their personal account which will unlock certain features in the application. At that point, the application itself will run as that user.

Since the behavior depends on which groups the user belongs to - and to a lesser extent which privileges they have - it will be very useful for testing and troubleshooting to have a convenient way for the application to display that information.

Background

Any code running on a Windows computer is running in the security context of a user, represented by an access token. The user can be an actual person, a Windows Service account, or one of the built-in System accounts.

The access token is an opaque object that contains the security identifier of every group the user belongs to, every privilege the user has, as well as other information. Everyone is familiar with group based permissions, where access to an operating system resource or a file is granted or denied based on the groups. I'm not going to go beat that dead horse further here.

Privileges are different. They give the user global power and warrant more explanation.

Windows Privileges

Usually, a person is either a user, or an administrator. That's as much as we usually think about what that means in terms of permissions. The truth is more nuanced.

If the user has a privilege and activates it, the operating system allows that user to do certain things regardless of which groups they belong to. A user account typically only has a few. For example, SeShutdownPrivilege is such a privilege. Any user who has that privilege can initiate a reboot or shutdown. Another good example is SeBackupPrivilege. A user with that privilege can access any file on the system regardless of which access group they belong to.

One of the most powerful ones is SeTcbPrivilege. The privilege allows the user to act as part of the operating system, allowing them effectively to do anything they want. A user without admin access could simply create a new user token for themselves, and add any group they want into the token, or access resources belonging to other users.

So you see, privileges are a very powerful feature and you have to be very careful with them. In most cases, users do not need to worry about them. Typically, only system administrators think about them when they configure the security on a system. However, in special cases, an application may depend on the user having a certain privilege in order to function correctly. That is why

  1. retrieving that information for troubleshooting is useful and
  2. implementing proper application diagnostics / error checking can prevent further application errors.

Source of the Privileges

Privileges are managed by LSASS (the Local Security Authority Subsystem Service) which has a configuration database. The contents of that database come from Local Security Policy or Group Policy. If you open Administrative tools -> Local Security Policy, you can see this:

Image 1

The privilege to backup files and folders on my laptop is granted to the Administrators and Backup Operators group. Users who have the SID for that group in their token will be able to touch files regardless of access permissions on individual files. It would have been very handy if the explanation explicitly specified that this is the SeBackupPrivilege but that is sadly not the case.

A similar set of parameters can be configured via Group Policy, and the resulting set will be formulated following the normal rules of policy processing. When a user logs on to the system, LSASS will recursively put all groups in the user token, and then use the user and group SIDs to filter its own database and apply all privileges the user is entitled to.

Lastly, just having a privilege doesn't grant any powers just yet. And application that wants to do something with that privilege needs to explicitly enable that specific privilege with a call to AdjustTokenPrivilege. The reason for doing this is to make sure that processes don't have the possibility of accidentally doing something they didn't explicitly want to do.

Using the Code

The first step is to acquire an access token. A Windows program always has at least 1 access token: the process token. There can be more than 1 token in an application. A thread can have its own token. This happens when the thread is running an impersonation token, meaning that specific is running as a user who is different from the user who started the application.

It is possible to inspect these tokens individually but the Win32 API has a convenient helper function GetCurrentThreadEffectiveToken() which gives you the effective token for a thread. This will be the process token if no impersonation is going on, or the thread token if there is.

Getting Information About the Token

At first, it may seem strange that we can just get hold of this token if it is so sensitive. However, we cannot do anything with it. Once created, a token can never change so it is safe from tampering. And code can only retrieve the token from inside the application, which means there is no breach of sensitive information.

An exception would be a process that is running with debugging privilege. But that too is not a breach because it has that privilege because the administrator has configured security policy to indicate that the user running that process is to be trusted with that information.

All token information can be retrieved with one function: GetTokenInformation.

C++
WINADVAPI
BOOL
WINAPI
GetTokenInformation(
    HANDLE TokenHandle,
    TOKEN_INFORMATION_CLASS TokenInformationClass,
    LPVOID TokenInformation,
    DWORD TokenInformationLength,
    PDWORD ReturnLength
    );

The TokenHandle is what we want to get more information about. TokenInformationClass is an enum that specifies which information we want to retrieve. There is a long list of possible options. TokenInformation is a void pointer. The actual type it points to depends on the TokenInformationClass enum. For example, if we specify TokenPrivilege as the desired data, then TokenInformation must be a pointer to a TOKEN_PRIVILEGE struct. For TokenGroups, it must be a pointer to a TOKEN_GROUPS struct, etc.

The exact size of those structures is variable, because it depends on how many privileges are assigned, or how many groups the user is a member of. This function follows the standard Win32 pattern of executing a function with a buffer and a buffersize in two steps. First, you use it to retrieve the required size of the buffer. Then you allocate the memory, and execute it again. It should be noted that there will never be a size conflict because tokens are immutable, so the required size for a piece of information will never change for that given token.

Since we have to repeat this for every type of information, we have a helper for it.

C++
DWORD w32_GetTokenInformation(
    HANDLE hToken,
    TOKEN_INFORMATION_CLASS InfoType,
    PVOID &info,
    DWORD &bufSize)
{
    DWORD retVal = NO_ERROR;
    //get the required buffer size for the privilege struct
    if (!GetTokenInformation(
        hToken, InfoType, NULL, 0, &bufSize)) {
        retVal = GetLastError();
    }

    //allocate the required size.
    //the struct has a variable array size
    if (retVal == ERROR_INSUFFICIENT_BUFFER) {
        retVal = NO_ERROR;
        info = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, bufSize);
        if (!info)
            retVal = GetLastError();
    }

    //get the privilege information
    if (retVal == NO_ERROR && !GetTokenInformation(
        hToken, InfoType, info, bufSize, &bufSize)) {
        retVal = GetLastError();
    }
    
    //release the memory if there was an error
    if (retVal != NO_ERROR) {
        if (info)
            HeapFree(GetProcessHeap(), 0, info);
        info = NULL;
    }

    return retVal;
}

The memory that was allocated by this function needs to be released by the caller. After the function has completed, the info pointer will point to the structure for the data that was requested.

Requesting User Data

This is the simplest option. We can request user information for a token like this:

C++
ULONG w32_GetTokenUser(HANDLE hToken, w32_CUser& user) {
    DWORD bufLength = 0;
    DWORD retVal = NO_ERROR;
    PTOKEN_USER pUser = NULL;

    retVal = w32_GetTokenInformation(hToken, TokenUser, (PVOID&)pUser, bufLength);

    if (retVal == NO_ERROR) {
        //Allocate buffers
        TCHAR name[w32_MAX_GROUPNAME_LENGTH];
        DWORD nameSize = sizeof(name);
        TCHAR domain[w32_MAX_DOMAINNAME_LENGTH];
        DWORD domainSize = sizeof(domain);
        SID_NAME_USE sidType;

        PSID pSid = pUser->User.Sid;
        LPTSTR sidString = NULL;

        //get the name of the group and the domain it belongs to
        if (!LookupAccountSid(
            NULL, pSid, name, &nameSize, domain, &domainSize, &sidType)) {
            retVal = GetLastError();
        }

        //Get a human readable version of the SID
        if (!ConvertSidToStringSid(pSid, &sidString)) {
            retVal = GetLastError();
        }

        user.Attributes = pUser->User.Attributes;
        user.Domain = domain;
        user.Sid = sidString;
        user.Name = name;
        user.SidType = sidType;

        LocalFree(sidString);
    }

    if (pUser)
        HeapFree(GetProcessHeap(), 0, pUser);

    return retVal;
}

The w32_CUser class is a simple helper class with a number of properties, which we fill in. After retrieving the TOKEN_USER data, we have the user SID. The Win32 API has a helper function that translates the SID to a username and a domain name for us. We also save a readable version of the SID itself.

That's it for the user information. It seems a bit overkill to do things like this, but that is because the GetTokenInformation function was designed to be a one size fits all function.

Getting Token Privileges

This is slightly more complex because the struct contains a variable sized array.

C++
//Get the privileges that are associated with the token
ULONG w32_GetTokenPrivilege(HANDLE hToken, vector<w32_CPrivilege>& privilegeList) {
    DWORD bufLength = 0;
    DWORD retVal = NO_ERROR;
    PTOKEN_PRIVILEGES pPrivileges = NULL;

    retVal = w32_GetTokenInformation(hToken, TokenPrivileges, 
                                    (PVOID&)pPrivileges, bufLength);
    
    if (retVal == NO_ERROR) {

        //allocate buffers
        TCHAR privilegeName[w32_MAX_PRIVILEGENAME_LENGTH];
        DWORD bufSize = sizeof(privilegeName);
        DWORD receivedSize = 0;
        privilegeList.resize(pPrivileges->PrivilegeCount);

        for (DWORD i = 0; i < pPrivileges->PrivilegeCount; i++) {
            LUID luid = pPrivileges->Privileges[i].Luid;

            //This needs to be reset because it is an in - out parameter and
            //otherwise the LookupPrivilegeName thinks the buffer is too small
            bufSize = sizeof(privilegeName);
            if (!LookupPrivilegeName(NULL, &luid, privilegeName, &bufSize)) {
                retVal = GetLastError();
                break;
            }

            privilegeList[i].Luid = luid;
            privilegeList[i].Flags = pPrivileges->Privileges[i].Attributes;
            privilegeList[i].Name = privilegeName;
        }
    }

    if(pPrivileges)
        HeapFree(GetProcessHeap(), 0, pPrivileges);

    if (retVal != NO_ERROR)
        privilegeList.clear();

    return retVal;
}

Privileges in Windows have a unique identifier called a LUID. After retrieving the identifiers, we have to translate each of them to a human readable name. If we want to do something with the privilege later on, we need to do that via the LUID. The names are constants, but the LUID is something that can change per system.

Each privilege also has a number of attribute flags which can be found online, and which indicate whether the privilege is enabled for example.

Getting Token Groups

This is very similar so not a whole lot of extra explanation is needed.

C++
ULONG w32_GetTokenGroups(HANDLE hToken, vector<w32_CUserGroup>& groups) {
    DWORD bufLength = 0;
    DWORD retVal = NO_ERROR;
    PTOKEN_GROUPS pGroups = NULL;

    retVal = w32_GetTokenInformation(hToken, TokenGroups, (PVOID&)pGroups, bufLength);

    if (retVal == NO_ERROR) {
        groups.resize(pGroups->GroupCount);

        TCHAR name[w32_MAX_GROUPNAME_LENGTH];
        DWORD nameSize = sizeof(name);
        TCHAR domain[w32_MAX_DOMAINNAME_LENGTH];
        DWORD domainSize = sizeof(domain);
        SID_NAME_USE sidType;

        for (DWORD i = 0; i < pGroups->GroupCount; i++) {
            //Allocate buffers

            DWORD nameSize = sizeof(name);
            DWORD domainSize = sizeof(domain);

            PSID pSid = pGroups->Groups[i].Sid;
            LPTSTR sidString = NULL;

            //get the name of the group and the domain it belongs to
            if (!LookupAccountSid(
                NULL, pSid, name, &nameSize, domain, &domainSize, &sidType)) {
                retVal = GetLastError();
                break;
            }

            //Get a human readable version of the SID
            if (!ConvertSidToStringSid(pSid, &sidString)) {
                retVal = GetLastError();
                break;
            }

            groups[i].Attributes = pGroups->Groups[i].Attributes;
            groups[i].Domain = domain;
            groups[i].Sid = sidString;
            groups[i].Name = name;
            groups[i].SidType = sidType;

            LocalFree(sidString);
        }
    }

    if (pGroups)
        HeapFree(GetProcessHeap(), 0, pGroups);

    if (retVal != NO_ERROR)
        groups.clear();

    return retVal;
}

Points of Interest

There are more things that can be retrieved for a token. You can find the full list here. For my purposes, the user, privileges and groups are what was important. Should you need any other information, my examples can easily be expanded.

The test application with this article simply retrieves the current token, and then retrieves and shows the user information, privileges and groups for this token. It is essentially a simpler version of whoami.exe.

Image 2

History

  • 5th September, 2022: First version

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Software Developer
Belgium Belgium
I am a former professional software developer (now a system admin) with an interest in everything that is about making hardware work. In the course of my work, I have programmed device drivers and services on Windows and linux.

I have written firmware for embedded devices in C and assembly language, and have designed and implemented real-time applications for testing of satellite payload equipment.

Generally, finding out how to interface hardware with software is my hobby and job.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Shao Voon Wong10-Oct-22 20:34
mvaShao Voon Wong10-Oct-22 20:34 
GeneralRe: My vote of 5 Pin
Bruno van Dooren10-Oct-22 23:47
mvaBruno van Dooren10-Oct-22 23:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.