So far I have discussed what you can find out from a token, as well as what adjustments you can make to a token. But tokens don't really get interesting until you begin executing code under a token other than your process token. There are two ways to do this:
First I will discuss how to create a new process under a different user context than your own, because it is the less sophisticated way of executing code on behalf of a user other than yourself. Then I will discuss impersonation at length.
To create a process using a token, you should call CreateProcessAsUser:
BOOL CreateProcessAsUser( HANDLE hToken, PCTSTR pszApplicationName, PTSTR pszCommandLine, PSECURITY_ATTRIBUTES psaProcessAttributes, PSECURITY_ATTRIBUTES psaThreadAttributes, BOOL fInheritHandles, DWORD dwCreationFlags, PVOID pEnvironment, PCTSTR pszCurrentDirectory, PSTARTUPINFO pStartupInfo, PPROCESS_INFORMATION pProcessInformation);
CreateProcessAsUser is parameter-for-parameter identical to CreateProcess except for the first parameter, hToken, which accepts a primary token for the user context under which the new process will run. (Please refer to Programming Applications for Microsoft Windows, Fourth Edition, for an extensive discussion of CreateProcess.)
Now let's look at its differences. CreateProcessAsUser returns TRUE if the new process was created and FALSE if the function failed. In three common cases CreateProcessAsUser might fail, where CreateProcess would not.
In the first case, the calling process might not have access to the executable file or directory of the executable file. A service running in the LocalSystem account is unlikely to run into this problem, although it is possible. To get around this problem, you must temporarily impersonate the user for whom the new process is being created by using the same token that is being passed to CreateProcessAsUser. This temporary impersonation must be done before making the call to CreateProcessAsUser. Naturally, if the new user does not have access to the executable or directory, the function fails.
In the second case, CreateProcessAsUser requires the calling process to have the SE_ASSIGNPRIMARYTOKEN_NAME and SE_INCREASE_QUOTA_NAME privileges granted to its token. The exception to this rule is the case in which the token being passed to CreateProcessAsUser is a restricted token created with the calling process's token. In this situation, the privilege SE_ASSIGNPRIMARYTOKEN_NAME is not required. (Using restricted tokens is a very powerful technique that is discussed in some detail later in this chapter.) The rationale here is that the restricted token will have less access to the system than the current process.
In the third scenario, CreateProcessAsUser fails when the token passed as the hToken parameter is not a primary token. If you hold a handle to an impersonation token, you can convert it to a primary token with a call to DuplicateTokenEx:
BOOL DuplicateTokenEx( HANDLE hExistingToken, DWORD dwDesiredAccess, PSECURITY_ATTRIBUTES pTokenAttributes, SECURITY_IMPERSONATION_LEVEL ImpersonationLevel, TOKEN_TYPE TokenType, PHANDLE phNewToken);
The hExistingToken parameter is the token that you wish to duplicate. This token can be either a primary token or an impersonation token. The dwDesiredAccess parameter indicates the access that you want for the new token. You should ask only for the rights needed to accomplish the task at hand. See Table 11-2 for a list of access rights.
The pTokenAttributes parameter indicates the security descriptor and inheritance attributes of the new token. Do not confuse this parameter with the token's default DACL—this parameter sets the access control for the new object. (Security descriptors and access control are discussed at length in Chapter 10. The topic of token default DACLs is discussed earlier in this chapter as well as in Chapter 10.)
The ImpersonationLevel parameter indicates the new token's impersonation level and can be any of the values in Table 11-3.
Table 11-3. Members of the SECURITY_IMPERSONATION_LEVEL enumerated type
|SecurityAnonymous||A token created with the SecurityAnonymous impersonation level cannot be used to create a process with impersonation.|
|SecurityIdentification||A token created with this impersonation level can be used only as a means of identification. The token's user and groups can be queried, but the token cannot be used with impersonation or in calls to CreateProcessAsUser.|
|SecurityImpersonation||This impersonation level creates a fully functional token that can be queried as well as used to execute code through impersonation. A token must be an impersonation token before a thread can impersonate that token.|
|SecurityDelegation||A token with the SecurityDelegation impersonation level can be used to access network resources. This is called delegation and allows a server to become a client on behalf of its client in a connection to a second server. Windows NT 4.0 and earlier versions did not support delegation. Windows 2000 does, but it requires that delegation be allowed on the server attempting to create a token of delegation type.|
The TokenType can be either TokenPrimary or TokenImpersonation, both members of the TOKEN_TYPE enumeration. And the final parameter, phNewToken, receives the handle to the new token. Remember to call CloseHandle on the handle when you are finished with the object.
If DuplicateTokenEx returns FALSE, the function has failed. It usually fails because of insufficient access rights on the original token. (See Chapter 10 for a complete discussion of access control and access rights.)
In previous versions of Windows, tokens created on a server machine for the purpose of acting on behalf of a client were called network tokens and typically had the SecurityImpersonation impersonation level. These tokens did not contain a copy of the trustee's credentials and could not be used to access resources outside of the machine that created the tokens.
However, with Windows 2000 the SecurityDelegation impersonation level is supported using the Kerberos security protocol (discussed in Chapter 12). Now it is possible to have a network token that does not limit access to the local machine.
You now know how to obtain a token from a process and how to create a process using an arbitrary token. However, we have not discussed how to retrieve a handle to a token when you have only a username and a password. You do this by calling LogonUser:
BOOL LogonUser( PTSTR pszUsername, PTSTR pszDomain, PTSTR pszPassword, DWORD dwLogonType, DWORD dwLogonProvider, PHANDLE phToken);
The pszUsername and pszDomain parameters indicate the username and the domain of the user for which you wish to receive a token. You can pass the period character (".") for the pszDomain parameter, which searches only the local system's account database. If you pass NULL for the pszDomain parameter, the local system is searched for the user followed by the domains trusted by the system. The third parameter, pszPassword, is the password for this account.
The dwLogonType parameter of LogonUser describes to the system how the token will be used and what type of token you want to receive. Table 11-4 lists the values you can pass as the dwLogonType parameter.
Table 11-4. Values you can pass for LogonUser's dwLogonType parameter
|LOGON32_LOGON_INTERACTIVE||Passing this value to LogonUser causes the system to check for the existence of the SE_INTERACTIVE_LOGON_NAME privilege in the account of the user for whom a token is being requested. LogonUser will fail if the user does not have this privilege assigned. Additionally, tokens received with this logon will be cached with the system. This means that the local system can lose connection with the authenticating machine but still make future successful calls to LogonUser using cached credentials. LogonUser will return a primary token.|
|LOGON32_LOGON_BATCH||Passing this value to LogonUser causes the system to check for the existence of the SE_BATCH_LOGON_NAME privilege in the account of the user for whom a token is being requested. LogonUser will fail if the user does not have this privilege assigned. Tokens received with this logon type are not cached, increasing the performance of LogonUser and making the logon type appropriate for high-performance servers. LogonUser will return a primary token.|
|LOGON32_LOGON_NETWORK||Passing this value to LogonUser causes the system to check for the existence of the SE_NETWORK_LOGON_NAME privilege in the account of the user for whom a token is being requested. LogonUser will fail if the user does not have this privilege assigned. Tokens received with this logon type are not cached. In addition, this token will be an impersonation token and a "network token."|
|LOGON32_LOGON_NETWORK_CLEARTEXT||Like the LOGON32_LOGON_NETWORK logon type, this logon type checks the account in question for the SE_NETWORK_LOGON_NAME account right. However, unlike LOGON32_LOGON_NETWORK, this logon type returns an impersonation token while preserving a copy of the trustee's credentials so that network access is possible using the resulting token. The token has to be duplicated to a primary token before it can be used in calls to CreateProcessAsUser.|
|LOGON32_LOGON_NEW_CREDENTIALS||This intriguing logon type is new in Windows 2000. It requires that you use LOGON32_PROVIDER_WINNT50 as the value for dwLogonProvider in calls to LogonUser. This logon type makes a copy of the calling thread's process token and adds a second identity to the token. This second identity will be the token's identity for all network access, where the token's identity for the local machine will remain the same as that of the original token. This makes the LOGON32_LOGON_NEW_CREDENTIALS unique in that it uses an existing token to build a new token with extra credentials. For an example of this logon type, see the RunAs.exe utility provided with Windows 2000. The "/NetOnly" switch uses the LOGON32_LOGON_NEW_CREDENTIALS logon type to create a token for the new process. The resulting token is a primary token.|
|LOGON32_LOGON_SERVICE||Passing this value to LogonUser causes the system to check for the existence of the SE_SERVICE_LOGON_NAME privilege in the account of the user for whom a token is being requested. LogonUser will fail if the user does not have this privilege assigned. |
This token will be cached for future calls to LogonUser if the machine loses connection to the authenticating agent. LogonUser returns a primary token.
|LOGON32_LOGON_UNLOCK||This logon type is used by GINA to handle the unlocking of a workstation. If auditing is turned on for the system, calling LogonUser with this logon type creates an entry in the event log. The resulting token is a primary token.|
As a developer or a network administrator, you can tighten your security and make systems more secure against misuse by creating user accounts that can be used only with the LOGON32_LOGON_SERVICE or LOGON32_LOGON_INTERACTIVE type. For example, you could grant only the SE_SERVICE_LOGON_NAME or the SE_INTERACTIVE_LOGON_NAME privilege, respectively.
Although logon types such as LOGON32_LOGON_SERVICE and LOGON32_LOGON_INTERACTIVE require different privileges, they do not actually produce drastically different tokens. (The major difference among the tokens is the existence of a SID in the token groups, which indicates what type of logon was used. The logon source will also differ.) Requiring different privileges allows the system to provide an extra level of authentication without complicating the architecture of a token.
A token created with the LOGON32_LOGON_BATCH or LOGON32_LOGON_SERVICE can still run a process that interacts with the user, creating windows and other GUI objects.
However, when the system creates a process to run as a service, the token it uses is logged on using the LOGON32_LOGON_SERVICE logon type. And when the system logs a user on interactively, it uses the LOGON32_LOGON_INTERACTIVE logon type.
The value you pass for dwLogonProvider decides which method is used to authenticate the credentials passed to LogonUser. You should pass LOGON32_PROVIDER_DEFAULT for all uses of LogonUser, unless you are using the LOGON32_LOGON_NEW_CREDENTIALS logon type. In this case you should pass the value of LOGON32_PROVIDER_WINNT50 as the dwLogonProvider parameter.
You receive a handle to your new token via the phToken parameter of LogonUser.
The LogonUser function will return TRUE if the system has succeeded in authenticating the credentials passed, and FALSE if it has failed. A call to GetLastError will return the reason for failure.Three common errors that result when LogonUser fails are ERROR_LOGON_FAILURE, ERROR_LOGON_TYPE_NOT_GRANTED, and ERROR_PRIVILEGE_NOT_HELD. When GetLastError returns ERROR_LOGON_FAILURE, the credentials passed to LogonUser are not recognized. If GetLastError returns ERROR_LOGON_TYPE_NOT_GRANTED, the requested account does not have the proper account right for the requested logon type. The error code ERROR_PRIVILEGE_NOT_HELD suggests that the process calling LogonUser does not have the SE_TCB_NAME privilege granted. By default the LocalSystem account has this privilege granted. No other trustee in the system has this privilege by default.
When you are finished with the token, call CloseHandle on the object.
When you call LogonUser, you are asking the system to build a token for you. You can use this token in calls to CreateProcessAsUser and in impersonation, which is the subject of the next section.
When you are writing server software in Windows, you have the option of creating a process, on behalf of your client, to do the bidding of your client. However, creating a process for each client simply isn't a scalable technique. Impersonation is the solution to this problem, because it lets a single thread act under the security context of your client for an arbitrary length of time and revert back to the process's security context when it has finished. Impersonation can be done in a manner that is exceptionally efficient and scalable.
I love impersonation—the system handles the details for you, and it is implemented simply by making a call to ImpersonateLoggedOnUser. I think this is really cool because it ensures the thread is acting under the client's security context. What better way to ensure that you don't accidentally allow a client to cause a service to misuse the awesome power granted to it via the LocalSystem account? Here is ImpersonateLoggedOnUser:
BOOL ImpersonateLoggedOnUser( HANDLE hToken);
If the token passed as the hToken parameter is of type TokenPrimary, ImpersonateLoggedOnUser creates a duplicate token of type TokenImpersonation and assigns it to the calling thread. If the original token is already an impersonation token, the token is directly assigned to the thread. In the first case, the calling thread must have TOKEN_QUERY and TOKEN_DUPLICATE access to the token. In the second case, only TOKEN_QUERY is required.
If ImpersonateLoggedOnUser succeeds, it returns TRUE. Otherwise your service should call GetLastError to find the reason for failure.
The token passed to ImpersonateLoggedOnUser can be a token received via a call to LogonUser, DuplicateTokenEx, OpenThreadToken, or any of the other similar functions we have discussed so far. It can also be a token retrieved by some other means (of which I will discuss a few shortly). Upon success, most securable functions called by the impersonating thread will recognize the new security represented by the token.
If a thread currently in the impersonation state calls CreateThread to create another thread, the new thread will not be impersonating. Said another way, all threads created using CreateThread use the process's token for securable activity, unless the created thread explicitly impersonates a token.
This can cause some pretty tough-to-find bugs, because your code can impersonate a user and then call a function that creates a new thread to perform work. This new thread will not represent the creating thread's security context. In the case of service development, this more commonly grants more access, not less access, to the thread than was intended and can create a security "hole" in your service.
When your thread has finished acting on behalf of your client, the thread can return to using the process s token by making a call to RevertToSelf:
To improve performance when impersonating, it is preferable to avoid obtaining a token more often than necessary regardless of whether you are calling LogonUser, OpenProcessToken, or some other function to get the token you are using for impersonation. Typically your service retrieves or builds a handle once, saving the handle to the token in the state data for the connection. The service can then call ImpersonateLoggedOnUser and RevertToSelf as needed using the stored handle, closing the handle only when the connection has been terminated and the token is no longer necessary.
Now your service can act on behalf of clients connecting via any communication mechanism, so long as the client is able to pass its credentials to the service. Alternatively, your service could store a set of preconfigured credentials that it uses for its various client accounts. Depending on the needs of your service, this approach can be very effective. In many cases, however, a more seamless approach is desirable.
Windows offers a truly seamless form of impersonation that does not require your service to acquire a set of credentials. This form of impersonation is connection-oriented, but it is otherwise similar to the impersonation technique that we already discussed. If a trusted authority has authenticated the client and the communication medium is one that supports impersonation, your service can impersonate the client automatically!
Table 11-5 lists connection methods supported by impersonation as well as the functions used to initiate the impersonation and revert to the process's token.
To discuss each of these forms of impersonation, I would need to digress to the topic of network communication, which is worthy of an entire book. However, the RoboService sample application in the previous chapter fully implements impersonation using named pipes, and the SSPIChat sample application (in Chapter 12) implements impersonation using the Security Support Provider Interface (SSPI). (SSPI is also discussed in Chapter 12.)
Two of the functions mentioned in Table 115 warrant further mention. They are ImpersonateSelf and SetThreadToken:
BOOL ImpersonateSelf( SECURITY_IMPERSONATION_LEVEL ImpersonationLevel);
The ImpersonateSelf function duplicates your process's token, creating a token of type TokenImpersonation, and assigns the token to the calling thread.
In practice you won't commonly use ImpersonateSelf to adjust the impersonation level of the token. Rather, you'll use it to create an impersonation so that adjustments to the thread, such as enabling and disabling privileges or disabling and enabling groups in the token, affect only a single thread rather than every thread in the process. The RevertToSelf function ends the impersonation.
Table 11-5. Connection methods supported by impersonation
|Connection Method||Impersonating Functions|
BOOL ImpersonateLoggedOnUser( HANDLE hToken);
BOOL ImpersonateSelf( SECURITY_IMPERSONATION_LEVEL ImpersonationLevel);
BOOL SetThreadToken( PHANDLE Thread, HANDLE Token);
|Named pipes|| |
BOOL ImpersonateNamedPipeClient( HANDLE hNamedPipe);
|Dynamic data exchange (DDE)|| |
BOOL DdeImpersonateClient( HCONV hConv);
BOOL ImpersonateDdeClientWindow( HWND hWndClient, HWND hWndServer);
|Remote procedure calls (RPC)|| |
RPC_STATUS RPC_ENTRY RpcImpersonateClient( RPC_BINDING_HANDLE BindingHandle);
RPC_STATUS RPC_ENTRY RpcRevertToSelfEx( RPC_BINDING_HANDLE BindingHandle);
|Sockets or any other transport mechanism (via SSPI)—covered in Chapter 12|| |
SECURITY_STATUS ImpersonateSecurityContext( PCtxtHandle phContext );
SECURITY_STATUS RevertSecurityContext( PCtxtHandle phContext);
The SetThreadToken function, shown next, allows you to arbitrarily choose an impersonation token for any thread by using the thread's handle. The pThread parameter is a pointer to a handle to the thread that you will adjust, where passing NULL indicates the current thread. The hToken parameter indicates the token to be used for impersonation, where a NULL value will cause the thread to revert to the process-level token.
BOOL SetThreadToken( PHANDLE pThread, HANDLE hToken);
At first glance, the SetThreadToken function might not seem necessary. Why not just use the impersonation function appropriate for your communication mechanism? The answer is the one missing feature with impersonation—the notion of an impersonation stack. Let me explain.
If you were to call ImpersonateLoggedOnUser with token A, and then call a function that calls ImpersonateLoggedOnUser with token B, when the function called RevertToSelf, the system would revert the thread's token all the way back to the process token. The system has no memory of the thread's association with token A. Normally you have control of your code and can avoid situations like this. But here are two noteworthy exceptions:
To fake an impersonation stack, your function would have to retrieve a handle to its current token via a call to OpenThreadToken (which we have discussed), store it (most likely on the stack), and then call the appropriate impersonation function. Rather than calling the complementary "revert" function, your code would use SetThreadToken to restore the stored token. This way your function gives the thread back to the calling code the way it found it.
In a high-performance server environment, the overhead of communicating impersonation information over the network is undesirable. In many cases, you can achieve the best performance by calling the appropriate impersonation function upon connection and then making a call to OpenThreadToken to retrieve and store a handle to the impersonating thread token. Thereafter you can use SetThreadToken or ImpersonateLoggedOnUser to impersonate the stored token, as threads in your service fulfill further requests for that client.