When looking at how access controls work, it's useful to think of a lock and key. The lock is the ACL, which we examined in detail in a previous column, "Setting Security," and the key is the information within the process token, which defines the security information for a thread or process. The combination of a process token and an object's ACL determines whether the OS will grant the process the requested access to the object.
This week, I'll introduce you to process tokens and show you how to get at a process token's contents. I'll use this discussion to lay the groundwork for future articles where you'll need to have an understanding of what process tokens are and how they work.
Understanding what information is in a process token can help you debug your code and is particularly useful when dealing with Web-based applications and Distributed COM (DCOM) objects. Both Web-based applications and DCOM objects can run under more than one user context, or could be impersonating the client. Determining why a particular operation failed can be confusing, and knowing the details about the user trying to perform the operation can help. Systems administrators can also use process token information to determine why users can access a resource they shouldn’t have access to or can’t get to a resource users should have access to.
Listing 1 shows the source code for a DumpTokenInfo application that displays the process token contents to the console. DumpTokenInfo is also a useful diagnostic tool. At approximately 500 lines, this piece of code is unusually long and complex for this column, and it contains several helper functions. To download a compiled version of the code, click Download the code from the Article Information box at the top right corner of this page. Let’s look at main() at line 322 in dumptokeninfo.cpp.
Although this version of the code doesn't include any argument parsing or user input, a good addition to this code might be to enable the right to debug processes and open other processes to display the contents of their tokens. Because we’re dumping the token for our own process, we can easily get a handle by calling GetCurrentProcess(). Once we've opened the process, we can use OpenProcessToken() to obtain a handle to the process token, which we’ll need to obtain the token information.
Many Win32 API calls return variable-length structures, which is the case for most of the information that GetTokenInformation() returns. To address these structures, you can make the call several times, determine how much memory you need, double that amount of memory, and hope for the best. This code will work most of the time, but it will also likely have intermittent bugs. A better approach is to trap the error, reallocate the buffer to the appropriate size, and try again. Memory allocation is generally an expensive operation, so try to start with a buffer that will meet your needs most of the time. To help make main() more readable and in an effort to write reusable code, I've added IncreaseBuffer() at line 14 as a helper function to initially allocate the buffer and increase the size if needed. I start with a 512-byte buffer because it's large enough, but is still an efficient size for the heap manager.
Now on to the meat of the program. A for() loop lets us work our way through the various information classes that GetTokenInformation() supports, and because a TOKEN_INFORMATION_CLASS is an enum, we have to play some casting tricks to keep the stricter C++ compiler from complaining—see the comments in the code above the for() loop for details. Note that Windows 2000 includes another information class: TokenRestrictedSids. Restricted tokens are a very cool new security feature that I’ll cover in detail in a future column.
After we make the GetTokenInformation() call, we have to handle any errors that might occur. The most likely error is the result of the buffer not being large enough. We also have to handle an error thrown when trying to obtain TokenImpersonationLevel information from a token that isn’t an impersonation token. The GetTokenInformation call provides the information we requested, as Listing 2 shows, and we can print it out and examine each portion in detail.
Process Token Information
The first piece of information that the output provides is the token user information. To make the code more readable, individual functions are used to print the more complicated structures. For example, PrintTokenUser() leads to PrintSid(), which is a wrapper around LookupAccountSid(). Together, these two calls get the name of the user or group and print the type of account we’re dealing with. Note that the code treats one SID specially, Logon ID SID, but we’ll get to that in a moment. When you run the application, PrintTokenUser() will print the account you’re currently using.
Second, the output provides information about the groups within the token. Most objects with access controls don’t have explicit access for individual users; rather, they enable access based on groups. This simple fact makes groups in a process token important. Let’s look at some of the output:
Enabled by default
This line of output tells me that I’m running the application locally, as opposed to across the network. Within a process token, groups have various attribute bits that you can set to enable or disable groups, deny access if an access-denied access control entry (ACE) is present on an object, or for logon IDs. You can call AdjustTokenGroups() to change which of these flags you want to set for a process or thread token; however, this protection is weak because the process can make the same function call to reset these flags to their original state.
Although most of the groups shown probably won’t surprise you, I found one—Logon ID SID—where I couldn’t resolve the SID to a name using LookupAccountSid(). I honestly don't know what this group does, and I only found a few clues in the winnt.h header file. If I learn more about this particular group, I’ll discuss it in a future column.
The third piece of information that the output provides is user privileges. This information is an important part of a token because it tells us what user rights might be available to a process. Interestingly, the system doesn’t enable most user privileges by default, even though it grants the user that privilege. The only privilege that was enabled in my process token was SeChangeNotifyPrivilege, otherwise known as the right to bypass traverse checking. This right means that if I am currently operating in c:\lemon and I’d like to access objects in c:\lime, the system won’t check to see whether I have access rights to c:\ because I’m just passing through. All other rights you see are actually granted to your user account. If you need to use these rights, you must use AdjustTokenPrivileges() to enable them.
Fourth, a process token contains information about who can access your process. A process is an object, and like other objects, it has an owner and a default group. The code dumps these pieces of information next in the output, along with the ACL for your process. On some occasions, you might have the rare need to change the discretionary access control list (DACL) on your process to grant other users' processes certain access. Take a look in PrintProcessAccessMask() to see what sort of access you can use. The code also contains an important fine point that I use to dump the access mask—if you’re looking for the presence of one bit, it is sufficient to do a binary AND operation and check the result. If you’re looking for the presence of a combination of bits (e.g., PROCESS_ALL_ACCESS), you have to do the AND operation first and then see whether all the bits you need are still there, like so:
(PROCESS_ALL_ACCESS & access ) == PROCESS_ALL_ACCESS
The fifth piece of information that the output provides is the source of the token. Although I’m not sure why you would need to know this information, it points out an interesting programming issue: An 8-byte character buffer contains the name of the token source, and the buffer isn’t null-terminated. I don’t know of a way to pass this buffer to printf() and not end up with unexpected results. The moral of the story is to be careful when dealing with static buffers that the OS passes back to you because the buffers might not be terminated. I've inserted a bit of code at line 450 to handle this problem. Note that if I’d used strlen-strcpy to handle this buffer, the application would probably have thrown exceptions; however, strncpy() works nicely.
Finally, we print the type of the token. If a process or thread is impersonating a user from another process or system, we’ll get an impersonation token; otherwise, the token will be a primary token and some information about the level of impersonation won’t be accessible. We can also print out the TokenStatistics information, but most of it isn’t very interesting or is information we’ve already obtained from other calls.
We’ve covered everything you'd want to know about the information in a process token. Next time, we'll begin to look at ways you can manipulate this information.