Windows 2000 presents some interesting new features to control the access levels that a process uses. For example, the new RunAs command lets you start a process under a different user context so that users can temporarily increase their user level when performing an administrative task. Another tool, CreateRestrictedToken(), currently available only at the API level, lets you execute a process as yourself, but drop selected privileges and groups. Unlike simply disabling a group or impersonating another user, once you use a restricted token to create a process, you have no way to regain those privileges.
CreateRestrictedToken() presents some exciting possibilities. For example, a user who typically logs on as a local administrator or Power User might want to run the email client with only ordinary user account rights. The RestrictProcess application is complex, and the concepts we'll discuss in this article depend on understanding what a process token is and the information a process token contains. If you haven't read Understanding Process Tokens, you might want to do so now.
At first glance, I thought writing the application shown in Listing 1 would be easy—simply get the process token from the process, create a list of groups and privileges to drop, and then call CreateProcessAsUser(). However, as we’ll see, writing the code wasn’t as easy as I’d first thought. If you'd like to follow along, you'll need to download the code by clicking Download the code from the Article Information box at the top right corner of this page. Open RestrictProcess.cpp in another window. Start with wmain() at the bottom of the file. First, notice that I'm using two separate arguments to ask for the application path and the arguments to the application. Why didn't I just write it all as one argument? If we look at the arguments to CreateProcess(), the lpApplicationName parameter can be NULL, in which case the API assumes that the first whitespace-delimited token is the executable name. What if we're trying to run C:\program files\<something>? We might end up trying to run C:\program.exe, which isn’t what we want. If we explicitly specify the application name, we can avoid this ambiguity. However, lpApplicationName expects a full and exact path to the application. One solution to this path problem is to call SearchPath(). (If you’ve always missed having the UNIX which command-line utility on your Windows systems, you can use the code surrounding SearchPath() to provide this functionality on Win32 systems. If you don’t like the default search order that SearchPath() uses, you can use the FindFile() functions to implement your own.)
The CreateProcess family of APIs is full of interesting features. If we run this program on a command-line application that just prints its own arguments and we don’t prefix the command line with the executable name, we find that what should be argv\[1\] ends up being argv\[0\]. To prevent shifting all our arguments down by one, the next bit of code inserts the application name into the beginning of the CmdLine argument.
Now that we have the full path to the program that we want to execute, let’s look at the first function that the application calls—RestrictProcessToken()—a very complicated function. We must first obtain a process token. Next, we need to get the groups from the token into a buffer and decide which SIDs to disable. The argument that CreateRestrictedToken() takes is an array of SID_AND_ATTRIBUTES structures, which doesn’t contain actual SIDs, but pointers to SIDs. For this reason, we need to allocate the token group information.
Following the code into the GetTokenGroups() function, we use a loop to address the possibility of having an insufficient initial buffer. We allocate the buffer at the top of the loop, and if our attempt to call GetTokenInformation() fails because the buffer is too small, bufsize is reset and the code returns to the top of the loop. Note that checking to see whether the value returned as needed is greater than the buffer size isn't sufficient. If GetTokenInformation() fails for some other reason, the needed argument will be undetermined. Now that we have the groups from our token, let’s return to RestrictProcessToken().
We need to allocate another buffer that can fill up with groups that we want to disable. Conveniently, the TOKEN_GROUPS structure contains a count of the number of groups. In a worst-case scenario, we’d want to disable all the groups, which leads into the GetSidsToDisable() function. We need to pay careful attention to the return of this function because there are three situations that can occur: an error, no groups to disable, or at least one group to disable. Although we'd like CreateRestrictedToken() to input the list of groups we want to allow, the process works the other way. In most process tokens, several benign groups exist that we typically need. In the sample code, I decided to allow the following groups:
- Everyone—This group occurs in too many ACLs to drop.
- Local—As far as I know, this one doesn’t hurt anything.
- Authenticated Users—This group also occurs in many ACLs.
- Interactive—I’m not sure what impact dropping this group might have on application compatibility. You might want to try dropping Interactive to see what happens with your applications.
- Users—If this group is enabled, leave it.
- Logon ID SID—When I discussed this group in Understanding Process Tokens, I wasn’t sure what Logon ID SID did. It turns out that this group determines desktop access. If two different processes are running and share the same user, but not the same desktop, the two processes can’t collide. As I noted in the code, I’m not sure if leaving this SID enabled is a security problem. Try disabling Logon ID SID to see what happens with your applications.
Once you finally get to the end of the loop, the current group must be a group you want to disable. Be aware that if you’re running this application as a domain user, this approach strips out your right to access anything on the network that isn’t explicitly allowed by your account.
Now that we've taken care of the groups, let’s look at privileges we need to delete in GetPrivilegesToDelete(). One privilege that can cause application compatibility problems if we drop it is the right to bypass traverse checking or SE_CHANGE_NOTIFY_NAME. First, we need to obtain the token privileges and address the buffer size problem in the same way that we did with the group information. Then we allocate an output buffer and prepare to copy over the Locally Unique Identifier (LUID) associated with the privilege. The documentation misleadingly tells us that the LUID is really a LARGE_INTEGER, but that's not quite accurate. A LARGE_INTEGER is a union between a structure consisting of two DWORDs (high part and low part) and a 64-bit data type known as a LONGLONG (QuadPart). If the LUID really were a LARGE_INTEGER, the comparison would be simple—we’d just compare the two QuadPart members. Unfortunately, the LUID is only the structure consisting of two DWORDs, and we have to compare both members to determine equality. Correctly dereferencing the pointer for the output buffer turns out to be a little tricky—look carefully at the use of parentheses at the end of this function. On return, we need to use a switch statement to correctly handle the three possible return situations.
The last step is to call CreateRestrictedToken(). It seems like this call should be at the end of all this complicated code. Instead, this program led me into one of the more interesting debugging problems I’ve ever faced. If you're running the application as an administrator, you find that CreateProcessAsUser() returns success, but the application fails to initialize and throws a 0xC0000022 exception. (I’m not sure where this exception is documented, so trying to look it up wasn’t much help.) Calling CreateProcess() instead of CreateProcessAsUser() correctly creates the process, so the problem wasn’t in the command line or the STARTUPINFO structure. Opening a command shell as an ordinary user with runas.exe correctly creates any process we want, so the problem happens only when an administrator runs the code. Step by step, I determined that if I dropped the privileges but not the groups, the problem didn’t occur. Dropping each group in turn revealed that the bug happened only if I removed the administrators group. Because restricted tokens aren't terribly useful if administrators can't drop their group membership, I searched for a solution in the Microsoft Knowledge Base, but there were no articles on this API. However, running dumptokeninfo.exe gave me something to think about—an administrator owns a token created by an administrator, not the user who created the token. The discretionary access control list (DACL) on the token also allows only system and administrator access. As it turned out, you can’t create a process with a token that doesn’t have access to itself.
The solution to the bug leads to the TweakToken() function. In this function, we first obtain the SID of the token user, then replace the token owner with the SID of the token user. We also need to replace the token owner because the owner of the process owns the objects created by a process. Many ALCs grant access based on Creator-Owner, and if the administrators own the process, then you could create objects that you don’t have the right to modify. We first have to create a new DACL for the token and apply it. (If you need further explanation of creating DACL code, please consult Setting Security and Setting Security, Part 2.) Finally, we return from CreateRestrictedToken() and call StartProcess(). The only notable thing about StartProcess() is that we always need to remember to close the handles to the child process if the parent process is going to exit first.
I found it interesting to create command shells using this program and then run dumptokeninfo.exe. We can now see the changes in the groups. (Note that the groups are all still present but are marked for use in deny ACLs only.) You shouldn’t be able to evade access to denied access control entries (ACEs) by using this function to drop group membership, which can lead to some interesting issues (e.g., you can’t delete files you previously created as an administrator). I haven’t had time to test RestrictProcess with many applications, so please write and let me know how it works for you.