Win32 Security: retrieving Privileges and Groups for an

This article shows how we can read the thread token and inspect the Privileges and Groups 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 dispay 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. Typcially 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 a) retrieving that information for troubleshooting is useful and b) implementing proper application disgnostics / 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 fies 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 explicity 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 1 function: GetTokenInformation.

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 2 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.

DWORD w32_GetTokenInformation(
    HANDLE hToken,
    TOKEN_INFORMATION_CLASS InfoType,
    PVOID &info,
    DWORD &bufSize)

    DWORD retVal = NO_ERROR;
        if (!GetTokenInformation(
        hToken, InfoType, NULL, 0, &bufSize)) 
        retVal = GetLastError();
    

            if (retVal == ERROR_INSUFFICIENT_BUFFER) 
        retVal = NO_ERROR;
        info = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, bufSize);
        if (!info)
            retVal = GetLastError();
    

        if (retVal == NO_ERROR && !GetTokenInformation(
        hToken, InfoType, info, bufSize, &bufSize)) 
        retVal = GetLastError();
    
    
        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.

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) 
                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;

                if (!LookupAccountSid(
            NULL, pSid, name, &nameSize, domain, &domainSize, &sidType)) 
            retVal = GetLastError();
        

                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.

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) 

                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;

                                    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.

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++) 
            
            DWORD nameSize = sizeof(name);
            DWORD domainSize = sizeof(domain);

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

                        if (!LookupAccountSid(
                NULL, pSid, name, &nameSize, domain, &domainSize, &sidType)) 
                retVal = GetLastError();
                break;
            

                        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

05SEP2022 First version