We will be learning a number of powerful and interesting techniques that can be performed by manipulating tokens in Windows 2000. The first step down the path to all the fun is to acquire a handle to a token. So let's begin our journey by obtaining a handle to a process's token:
BOOL OpenProcessToken( HANDLE hProcessHandle, DWORD dwDesiredAccess, PHANDLE phTokenHandle); |
The OpenProcessToken function retrieves a handle to a process's token. The hProcessHandle parameter is the handle to the process for which we wish to open the token. Unfortunately the system does not allow you to pass NULL to indicate your desire to retrieve a handle to your own process's token. But a quick call to GetCurrentProcess returns a pseudo-handle that can be passed as the hProcessHandle parameter. The dwDesiredAccess parameter indicates how the token is to be used. You should request only the rights that your code needs. Table 11-2 describes the available token access rights. Many of the functions listed in the table will be discussed later in this chapter.
The variable whose address you pass as phTokenHandle receives a handle to the requested token. If the return value of OpenProcessToken is TRUE, the function succeeded; otherwise the function failed, in which case you should call GetLastError for more information.
Table 11-2. Token access rights
Value | Description |
---|---|
TOKEN_ADJUST_DEFAULT | Required when calling SetTokenInformtion (discussed shortly) to change features of the token, such as the default owner, primary group, or default DACL. |
TOKEN_ADJUST_GROUPS | Required in a call to AdjustTokenGroups. |
TOKEN_ADJUST_PRIVILEGES | Required in a call to AdjustTokenPrivileges. |
TOKEN_ADJUST_SESSIONID | Required to adjust the session ID of token and requires the SE_TCB_NAME privilege. |
TOKEN_ASSIGN_PRIMARY | Required when using the token in calls to CreateProcessAsUser. |
TOKEN_DUPLICATE | Required to duplicate the token. |
TOKEN_EXECUTE | Equals STANDARD_RIGHTS_EXECUTE. See Chapter 10 for further discussion of standard rights. |
TOKEN_IMPERSONATE | Required to use this token with ImpersonateLoggedOnUser. |
TOKEN_QUERY | Required to read any token information other than its source using GetTokenInformation. |
TOKEN_QUERY_SOURCE | Required to read the token's source using GetTokenInformation. |
TOKEN_READ | Combines STANDARD_RIGHTS_READ and TOKEN_QUERY. See Chapter 10 for further discussion of standard rights. |
TOKEN_WRITE | Combines STANDARD_RIGHTS_WRITE, TOKEN_ADJUST_PRIVILEGES, TOKEN_ADJUST_GROUPS, and TOKEN_ADJUST_DEFAULT. See Chapter 10 for further discussion of standard rights. |
TOKEN_ALL_ACCESS | Complete access to the token, combining all rights. |
The most likely reason for OpenProcessToken to fail is insufficient access rights; however, if your service is running in the LocalSystem user context, it will probably have sufficient access to any process's token. (For more information on access control, see Chapter 10.)
Token objects are kernel objects, and as with most kernel objects, you should pass the token object's handle to CloseHandle when you are finished using the resource.
NOTE
The handle you receive when calling OpenProcessToken is the handle of the token, which affects the user context of that process. Anything you do to adjust the token can have dramatic and immediate effects on the secure behavior of the process from which you retrieved the token. I will be discussing what you can do to a token shortly. For information on how to obtain the handle to a process (other than the current process), see the discussion of OpenProcess in Chapter 22 of Programming Applications for Microsoft Windows, Fourth Edition (Jeffrey Richter, Microsoft Press, 1999).
If you guessed that there must also be a way to obtain a thread's token (assuming the thread is impersonating at the time), you are correct. You can do this using OpenThreadToken:
BOOL OpenThreadToken( HANDLE hThreadHandle, DWORD dwDesiredAccess, BOOL fOpenAsSelf, PHANDLE phTokenHandle); |
Notice that OpenThreadToken is very similar to OpenProcessToken except that the first parameter is a handle to a thread rather than a process, and it has the additional parameter fOpenAsSelf. This parameter indicates to the system who you want to open the token as. Let me explain.
Remember that when you call OpenThreadToken you are requesting a token, which is a secured object in Windows. This means that you might or might not have access to the object. Remember also that you are running in a thread that could be impersonating a security context other than your process's security context. Because it is very common for a thread to retrieve a handle to its own token when it is impersonating, the system allows you to indicate that you want all access checks that are necessary to open a thread's token to be performed against your process's token. You should pass TRUE for the fOpenAsSelf parameter of OpenThreadToken if you want access checks to be performed against your process's token. You should pass FALSE if you want access checks to be performed against your thread's impersonation token. This parameter has no meaning if your thread is not currently impersonating. (I will discuss impersonation in more detail later in this chapter.)
Like OpenProcessToken, the phTokenHandle parameter of OpenThreadToken returns a handle to the requested token. If the return value of OpenThreadToken is TRUE, the function succeeded; otherwise the function failed, in which case you should call GetLastError for more information.
If the thread is not impersonating, the OpenThreadToken function will fail and GetLastError will return ERROR_NO_TOKEN.
So now you have a handle to a token. What can you do with it? you ask. Read on.
Most of the token information listed in Table 11-1 can also be read from a token, assuming that the calling code has TOKEN_QUERY (or TOKEN_QUERY_SOURCE) access to the object. You should use the GetTokenInformation function to find the contents of a token:
BOOL GetTokenInformation( HANDLE hTokenHandle, TOKEN_INFORMATION_CLASS tokenInformationClass, PVOID pTokenInformation, DWORD dwTokenInformationLength, PDWORD pdwReturnLength); |
You should pass a handle to a token as the hTokenHandle parameter. The tokenInformationClass parameter indicates what information you wish to obtain from the token, and the pTokenInformation parameter is a pointer to a buffer that the system fills with the requested information. The dwTokenInformationLength parameter indicates the length of the buffer you passed, and the pdwReturnLength parameter points to a variable that receives the size of the buffer necessary to retrieve the information.
The buffer pointed to by the pTokenInformation parameter varies in type, depending on the tokenInformationClass parameter. The following list describes the information that you can retrieve from a token: the information class value and the data type used. (The TOKEN_INFORMATION_CLASS is also used with the SetTokenInformation function, discussed later in this chapter.)
typedef struct _TOKEN_USER { SID_AND_ATTRIBUTES User; } TOKEN_USER; |
typedef struct _TOKEN_DEFAULT_DACL { PACL DefaultDacl; } TOKEN_DEFAULT_DACL; |
typedef struct _TOKEN_OWNER { PSID Owner; } TOKEN_OWNER; |
typedef struct _TOKEN_PRIMARY_GROUP { PSID PrimaryGroup; } TOKEN_PRIMARY_GROUP; |
typedef struct _TOKEN_GROUPS { DWORD GroupCount; SID_AND_ATTRIBUTES Groups[ANYSIZE_ARRAY]; } TOKEN_GROUPS; |
typedef struct _TOKEN_PRIVILEGES { DWORD PrivilegeCount; LUID_AND_ATTRIBUTES Privileges[ANYSIZE_ARRAY]; } TOKEN_PRIVILEGES; |
typedef struct _TOKEN_SOURCE { CHAR SourceName[8]; LUID SourceIdentifier; } TOKEN_SOURCE; |
typedef enum _TOKEN_TYPE { TokenPrimary = 1, TokenImpersonation } TOKEN_TYPE; |
typedef enum _SECURITY_IMPERSONATION_LEVEL { SecurityAnonymous, SecurityIdentification, SecurityImpersonation, SecurityDelegation } SECURITY_IMPERSONATION_LEVEL; |
typedef struct _TOKEN_STATISTICS { LUID TokenId; LUID AuthenticationId; LARGE_INTEGER ExpirationTime; TOKEN_TYPE TokenType; SECURITY_IMPERSONATION_LEVEL ImpersonationLevel; DWORD DynamicCharged; DWORD DynamicAvailable; DWORD GroupCount; DWORD PrivilegeCount; LUID ModifiedId; } TOKEN_STATISTICS; |
typedef struct _TOKEN_GROUPS { DWORD GroupCount; SID_AND_ATTRIBUTES Groups[ANYSIZE_ARRAY]; } TOKEN_GROUPS; |
DWORD |
As the previous list shows, you can read a great deal of information from a token. Here are the most common items to retrieve from a token:
When retrieving information from a token, you will typically have to call GetTokenInformation to find out the required length of the buffer, and then call it again to actually retrieve the information. To create truly fault-tolerant code, you should be prepared to call GetTokenInformation repeatedly until you have successfully retrieved the desired information. This is because the size of token information (such as the default DACL) can change between the time you call GetTokenInformation to retrieve the buffer size and the time you call GetTokenInformation to retrieve the actual data. This condition in multithreaded programming is known as a race condition and can cause those hard-to-find bugs that appear only once every several months or so.
The following function shows how to call GetTokenInformation properly to receive information about a token. It also returns the token information in a buffer allocated using LocalAlloc. If you use this function in your code, you should free the returned buffer using LocalFree when you are finished with the buffer.
LPVOID AllocateTokenInformation(HANDLE hToken, TOKEN_INFORMATION_CLASS tokenClass ){ PVOID pvBuffer = NULL; __try{ BOOL fSuccess; // Initial buffer size ULONG lSize = 0 ; do { // Do we have a size yet? if (lSize != 0) { // Do we already have a buffer? if (pvBuffer != NULL) LocalFree(pvBuffer);// Then free it // Allocate a new buffer pvBuffer = LocalAlloc(LPTR, lSize) ; if(pvBuffer == NULL) __leave; } // Try again fSuccess = GetTokenInformation( hToken, tokenClass, pvBuffer, lSize, &lSize ) ; // Still not enough buffer? }while( !fSuccess && (GetLastError() == ERROR_INSUFFICIENT_BUFFER)) ; // If we failed for some other reason, back out if(!fSuccess) { if(pvBuffer) LocalFree(pvBuffer) ; pvBuffer = NULL; } }__finally{} // Return locally allocated buffer return (pvBuffer) ; } |
The following code fragment shows the use of this function to retrieve the token's user SID and default DACL for the current process.
HANDLE hToken; if(!OpenProcessToken( GetCurrentProcess(), TOKEN_QUERY, &hToken)){ // Error } TOKEN_USER* ptUser = (TOKEN_USER*)AllocateTokenInformation(hToken, TokenUser); if ( ptUser != NULL){ // Do something with the SID pointed to by ptUser->User } TOKEN_DEFAULT_DACL* ptDACL = (TOKEN_DEFAULT_DACL*)AllocateTokenInformation(hToken, TokenDefaultDacl); if ( ptDACL != NULL){ // Do something with the DACL pointed to by ptDACL->DefaultDacl } |
Notice that a number of the structures returned by GetTokenInformation use the SID_AND_ATTRIBUTES structure, which is defined as follows:
typedef struct _SID_AND_ATTRIBUTES { PSID Sid; DWORD Attributes; } SID_AND_ATTRIBUTES ; |
This structure includes a SID for a trustee account and an Attributes member, which includes information about the SID. See the Platform SDK documentation for the meaning of the Attributes member for specific structures that include a SID_AND_ATTRIBUTES structure.
The following function, AllocateTokenLogonSID, uses the AllocateTokenInformation sample function to retrieve the token's group SIDs, and then iterates through the SIDs to find the logon SID discussed earlier in this section. The function finds the logon SID by checking the Attributes member for the SE_GROUP_LOGON_ID flag. The returned PSID should be freed using LocalFree.
PSID AllocateTokenLogonSID(HANDLE hToken){ PSID psidLogon = NULL; TOKEN_GROUPS* ptGroups = NULL; __try{ // Get the token groups ptGroups = (TOKEN_GROUPS*) AllocateTokenInformation(hToken, TokenGroups); if (ptGroups == NULL) __leave; // Find the logon SID int nCount = ptGroups->GroupCount; while (nCount--){ if ((ptGroups->Groups[nCount].Attributes & SE_GROUP_LOGON_ID) != 0) break; } if (nCount == -1) __leave; // No logon SID found // Get memory for the returned SID ULONG lLen = GetLengthSid(ptGroups->Groups[nCount].Sid); psidLogon = (PSID)LocalAlloc(LPTR, lLen); if (psidLogon == NULL) __leave; // Copy the logon SID if(!CopySid(lLen, psidLogon, ptGroups->Groups[nCount].Sid)){ LocalFree(psidLogon); psidLogon = NULL; __leave; } }__finally{ if (ptGroups != NULL) LocalFree(ptGroups); } return (psidLogon); } |
The TokenMaster sample application ("11 TokenMaster.exe") demonstrates the use of the token-related system functions, including GetTokenInformation and SetTokenInformation. The source code and resource files for the sample application are in the 11-TokenMaster directory on the companion CD. The program allows you to acquire a token from one of four sources: a process, a thread, a user's credentials, or via duplication. The program also allows you to view and modify token information. When the user executes TokenMaster, the dialog box in Figure 11-1 appears.
Figure 11-1. User interface for the TokenMaster sample application
The source code for this sample will help you understand how to apply the concepts discussed in this chapter. As a user of the TokenMaster sample application, you can explore the many capabilities of user context in Windows 2000. I will describe TokenMaster before I discuss some of the coding techniques so that you can use the sample to test some of the concepts in the chapter.
When TokenMaster is executed, its first task is to check its own user identity. If it is running under any account other than the LocalSystem account, it does the following in an attempt to upgrade itself:
The features of TokenMaster are far more educational for you if you run the sample application under the LocalSystem account, but special privileges are required to take the first three steps. If you are interested in executing TokenMaster to its full potential, you must do the following:
If you are successful, when you execute TokenMaster, the Status window in the application should contain the following message: "Token Master, Status - Token Master running as SYSTEM". Otherwise, the window will report that TokenMaster is running as the account you used to launch the application.
The primary function of the application is to view information about a token and modify the information that can be changed. The following are ways to acquire a token:
After you have acquired a token, you will see a listing of information about the token in the Token Information window. You can view all retrievable information for a token by using the GetTokenInformation function.
Using TokenMaster, you can modify and adjust the token in a variety of ways:
I strongly suggest that you spend some time using the TokenMaster sample application to become familiar with the user context-related features of Windows.
You will also find that TokenMaster can be used to ease the process of debugging. For example, I often launch the application and "steal" a token from the LocalSystem process. I then use this token to execute the Microsoft Visual Studio development environment, with which I can compile and test code from the LocalSystem user account. In terms of security, using the LocalSystem token when testing approximates the behavior of code running in a service.
The source code for this sample application demonstrates a number of useful programming tactics. First, it calls nearly every system function covered in this chapter. It also offers some tips that might be useful to you if you are new to security programming for Windows.
Windows security functions often require the allocation and reallocation of small buffers. This requirement can often be viewed as troublesome by developers, including myself. As a result, it is often tempting to hard-code buffer sizes or make other concessions that can lead to code that isn't airtight. The TokenMaster sample application addresses this requirement by making use of a simple template class known as CAutoBuf. You will see that this class greatly simplifies code where variable-sized buffers are required for system functions.