Share via


This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.

MIND

Exploring Handle Security in Windows
Keith Brown

Code for this article:Mar00SecurityBriefs.exe (265KB)

A

while ago I did some research trying to figure out how security works with handles in the face of interprocess communication, impersonation, handle inheritance, and the powerful DuplicateHandle API. This month I'll present my findings along with a program that you can use to explore the issues further.
      I used to speculate about how security works with built-in operating system objects (processes, semaphores, registry keys, files, window stations, and so on). But only after I wrote a program to really explore how security works with these objects was I able to develop a solid conceptual model that I felt comfortable with.
      For instance, why is it that you can create a named mutex with a DACL that denies everyone all access (an empty DACL is the easiest one to use), and yet the handle you get back from CreateMutex is completely functional? Anyoneâ€"even yourselfâ€"who attempts to open this mutex by name for SYNCHRONIZE access will fail with ERROR_ACCESS_DENIED (see Figure 1). If you've ever wondered exactly how security works on handles, you'll enjoy this month's brief.

The Conceptual Model


      From what seemed to be several isolated cases, I discovered a reasonable security model that can be expressed by a few simple rules. To help you understand what a handle really represents as far as security is concerned, I refer to handles as sessions here. Opening a session means calling CreateMutex, CreateFile, RegOpenKeyEx, and so on; using a session means calling WaitForSingleObject, WriteFile, RegQueryValueEx, and so on; and closing a session means calling CloseHandle, RegCloseKey, and so on. Here are the rules.
      First, each handle represents a process-relative session to an object that can be used by any thread in the process. Before the system creates a session, it performs an access check using the security descriptor of the object, the token of the thread or process, and the intentions of the creator (specified by a requested access mask, typically a parameter named dwDesiredAccess). If the thread has no token, the process token is used. If the requested access permissions are granted, the system creates the new session, making a note of the creator's specified intentions. Later, when the session is used, the system performs no further access checks other than enforcing that the session is used for the intended purpose.
      For example, if you call CreateFile requesting FILE_WRITE_ DATA access, the system will check to see if you have this access, returning a valid handle if you do. Calls to WriteFile through this handle will always succeed, while calls to ReadFile will always fail. No further access checks are performed when you use this handle.
      Second, new sessions can be created from existing sessions via handle inheritance. The system never performs an access check in this case; the parent's session is duplicated for the child with the same permissions.
      Third, new sessions can be created from existing sessions via DuplicateHandle. The system will perform an access check against the object's security descriptor only if one or more new permissions (not already granted in the original session) are being requested. (Some objects such as files and registry keys don't even bother performing this access check at all; they simply fail the DuplicateHandle call if new permissions are requested.) The system works this way regardless of whether the new handle is being duplicated into the current process or a different process.
      Finally, success audits are generally logged only when new sessions are openedâ€"not when they are duplicated, and not when they are used. Failure audits are generally logged when a session open request or duplicate request fails due to access restrictions.
      The goal of session-based security is efficiency and ease of use. Once you've opened a session, access control and auditing are finished and you can use that session with virtually no security-related overhead. You won't get any security-related surprises while you're in the middle of using a session, like when you thought you had a file open for read access, but then ReadFile fails with ERROR_ACCESS_DENIED. However, it's imperative that you understand the session-oriented nature of security in Windows® so that you don't end up fighting against itâ€"or even worse, opening security holes in your application because you weren't aware of its behavior.
      Given the basic four rules I just outlined, the following corollaries provide some practical insight into how the system works at runtime. First, changes to the security descriptor of an object have no effect whatsoever on existing sessions to that object. Second, changes to the token of a thread or process have no effect whatsoever on existing sessions. Third, somewhat following from the second corollary, once open a session can be used (or duplicated) without regard to the principal using the session.
      Here's a concrete example of the third corollary. A thread impersonating Alice can open a file for read access, giving the resulting handle to a thread impersonating Bob. That second thread can use that handle to read the file, regardless of whether Bob has read access to the file. Bob would simply be using Alice's session. Similarly, a thread in a process running as Alice can open a file for write access, duplicating the handle into a process running as Bob, which can then use the handle to write to the file regardless of whether Bob had access. This is just a slightly more sophisticated case where Bob is using Alice's session. The audit log in both of these cases will show that Alice opened the file. Bob's reads and writes won't get audited at all because audits generally aren't performed during steady-state session usage.
      This last corollary is important for systems programmers to understand. Sharing objects via handle inheritance or via DuplicateHandle is a very powerful technique that can lead to security holes if you're not careful. If you care about security, you won't give away your sessions willy-nilly by calling DuplicateHandle.
      If you want to run a process 24 hours a day, seven days a week, and you want it to dynamically change its identity on the fly via impersonation (in other words, you want to let users log in and out of the process without shutting it down or logging out of Windows), be aware that you'll have to close any handles (sessions) to objects that were opened by the previous user. This happens naturally when a process closes; all sessions opened by that process are torn down by the system. Letting the operating system do this for you is almost always a better solution than trying to do it yourself.
      Before I discovered these rules, I used to wonder why bInheritHandle was a member of the SECURITY_ATTRIBUTES structure. It certainly didn't seem like it had anything to do with security. However, with the insight that child processes end up getting a copy of a preestablished session without any further access checks, the factoring becomes quite clear and feels very reasonable.
      Now that I've presented the executive summary of the model, I'd like to drill down and share some details about the rather sophisticated test harness I developed to help discover and verify my conclusionsâ€"partly to share and encourage feedback on the methodology, and partly because the code for the test harness demonstrates several security and COM-related programming techniques that you might find useful.

Configuring the Test Harness


      Prior to writing this test harness, I had performed various (somewhat random) tests to discover how handles really worked using handle inheritance, named objects, and the DuplicateHandle API. I wanted this test harness to cover all the bases. So I created the simple little dialog-based application shown in Figure 2. (This really took me backâ€"what fun!) From this dialog, you can choose the class of object to test or indicate that all classes should be tested, one by one. At the time of this writing, I hadn't added support for every single type of object that can appear in your process's handle table, but most objects are represented here and the results are very consistent.
Figure 2 The Security Test Harness App
Figure 2The Security Test Harness App

      You can completely control the environment in which the test is performed. The first setting you can choose is whether the test runs in a single process or between two separate processes. In the single-process case, all handles being tested are opened within the client process. In the cross-process case, the client process spawns a second process, thus testing the security model where a single object is shared between two processes (via handle inheritance, named objects, and so on).
      In the cross-process case, you've got the option of running the secondary process in the same window station as the client (WinSta0), or in a separate window station (via the Cross Window Station option). I didn't think this would make any differenceâ€"and it turns out that it doesn'tâ€"but I didn't want to leave any stones unturned.
      Similarly, you can control the logon session that hosts the second process (via the Alternate Credentials option). The secondary process normally shares the client's logon session, but if you provide alternate credentials, the client process will call LogonUser/CreateProcessAsUser to start the secondary process in an alternate logon session. You'll need to run the client from a highly privileged logon session if you want to use this feature, as LogonUser can only be called from either the SYSTEM logon session or from a process running with SeTcbPrivilege (act as part of the operating system). Being an administrator is not enough, and the client program will alert you if you don't have the correct privileges. Consider using my cmdasuser utility, which I discussed in the February 2000 issue of MSJ (https://www.microsoft.com/msj/0200/logon/logon.asp), to run the test harness from the SYSTEM logon session.
      Regardless of whether you run the test harness across two processes or within a single process, you ultimately need to decide how to transmit the handle from the creator to the test. Here are the available choices:

Direct

The same physical handle obtained from the creation function (CreateMutex, CreateThread, and so on) should be used directly by the test. This makes sense only in the single-process case.

Named Object

The handle should be obtained by opening a named object, thus the creator will communicate a name (rather than a handle) to the test harness. This works in both the in-process and cross-process cases.

DuplicateHandle

The object should be created, the handle retrieved from the creation function should be duplicated (via DuplicateHandle), and the duplicate given to the test harness. This works in both the in-process and cross-process cases.

Inheritance

At creation time, the appropriate flag should be specified that causes the creation function to return an inheritable handle (most often this is tucked away in the SECURITY_ATTRIBUTES structure), and the secondary process should pick up this handle via inheritance, using the inherited handle in the test harness. This only makes sense in the cross-process case.

Inheritance of Duplicated Handle

At creation time, the object should not be created as inheritable; rather, the resulting handle should be passed to DuplicateHandle, specifying bInheritHandle as TRUE to indicate that the duplicate handle should be inheritable. This duplicated handle should then be passed to the secondary process via inheritance, and the test harness should use this duplicated handle. This only makes sense in the cross-process case.

All

Iteratively use all of the previous transmission options, trying one after another.

The Test Harness at Work


      The client simply creates an instance of the specified class (mutex, file, registry key, and so on), asking for all possible permissions. It then transmits a reference to the COM object in charge of executing the tests by calling either IHandleTest::SetHandle or IHandleTest::SetName, depending on the handle transmission mechanism in use. Next, the client runs the test by calling IHandleTest::RunTest, specifying two backpointers to COM objects implemented by the client. The first object allows the tester to request dynamic DACL changes from the creator of the object; the second is simply a pipe (IStream) that the tester uses to send back the results of each test, including any errors that occur. This second object is a simple implementation of IStream that dumps the data sent to it into the output edit box shown on the right of the client (see Figure 2).
      The client always creates the object being tested with a NULL DACL. This basically turns off access checks to the object to begin with. Creating an object with a NULL DACL is trivial:

// here we use a SECURITY_DESCRIPTOR to say
// that we don't want *any DACL at all*
SECURITY_DESCRIPTOR sd;
InitializeSecurityDescriptor(&sd,
    SECURITY_DESCRIPTOR_REVISION);
SetSecurityDescriptorDacl(&sd, TRUE, 0, FALSE);

SECURITY_ATTRIBUTES sa = { sizeof sa, &sd, FALSE };
CreateMutex(&sa, FALSE, "MyMutex");

If the transmission options require duplicating the resulting handle, the handle is duplicated for all possible permissions for the class of object in question. The idea is to start the test harness off by giving it full access to the object. If the object was passed by name, the tester attempts to open a handle for all possible permissions, otherwise the handle passed by the client is used directly.
      The tester now determines exactly which permissions it can obtain to the object passed by the client. The tester figures this out by attempting to duplicate the handle for all possible permissions. (Only the permissions appropriate to the class of the object under scrutiny are tested.) This proceeds, one permission at a time, and the tester obtains a list of which permissions were granted and which were denied. Clearly the first time this test runs, all permissions should be granted. At this point, the first round of testing is finished, so the tester closes any temporary handles that it has opened to the object. It's important to note that any handle transmitted from the client remains open, though.
      The tester then sends a message to the client asking it to place an empty DACL on the object, thus denying all access to everyone. (Recall that the creator will still have READ_CONTROL and WRITE_DAC permissions due to ownership.) Once again, the tester gathers a list of which permissions it was granted and denied. Note that if the test was passed a session from the client with all permissions, this change in the DACL should have no effect on the outstanding session.
      Here's some sample code that applies an empty DACL to an existing object:


// construct an empty DACL
ACL acl;
InitializeAcl(&acl, sizeof acl, ACL_REVISION);

// apply the DACL to the object
SetSecurityInfo(hMutex, SE_KERNEL_OBJECT,
                DACL_SECURITY_INFORMATION,
                0, 0, &acl, 0);

Finally, the tester sends a message to the client asking it to restore the NULL DACL, and tries again.
      If you check the Detailed Output checkbox on the client, the tester will send back the details of every step it is performing. This is useful if you're not familiar with the test harness. If you don't select Detailed Output, the tester will simply list the test it is performing and will not output any other information. It will instead verify programmatically that the test results confirm the model described in this column. If any unexpected results occur, they will be noted in the output, regardless of the detail level. The goal of this test harness is to be able to quickly verify the model on each new Service Pack for Windows NT or Windows 2000 without requiring much human intervention.
      If you like, you can check the Auditing checkbox. This causes a very simple SACL to be applied to each object right after it is created. The SACL says that all possible permissions on the object should be audited for both success and failure. To validate that most objects don't audit accesses to preexisting sessions, I also added a piece of code to the test that uses the object (it writes to a file, reads from a registry key, or synchronizes on a kernel object). You'll need to enable auditing of object access to see the results.
      To test how DuplicateHandle works in the face of escalating permissions, I actually run the test twice in most cases except when I know permission escalation is not supported, such as with files and registry keys. I end up starting the test with a handle that doesn't have all permissions and demonstrate that in the vast majority of cases calling DuplicateHandle will dynamically expand permissions by performing an access check if you request an escalation of permissions. Turn on Detailed Output to see exactly what's going on during these two test runs.

Implementing the Test Harness


      Since I wanted to explore the way handles worked in both a single process and across multiple processes (potentially running in different logon sessions or window stations), it was clear that my test harness needed an interprocess communication mechanism. I designed a pair of mating COM interfaces that would be used to maintain a bidirectional communication link between the test client and the test server so that the client and server code could either share the same process or run in different processes without changing any of the code.
      However, since I needed to be able to send handles to the server process via inheritance, I couldn't use the COM built-in activation service (CoCreateInstance and friends). Instead, I packaged the test object in a DLL and designed a simple surrogate-like program that expects to be launched directly by the client, inheriting a handle to a shared section used to establish a shared memory block between the two processes (see Figure 3). The server explicitly marshals an interface pointer into this block, and the client unmarshals a proxy, setting up a communication link from the client to the server process (see Figure 4). Setting up the reverse link is easy; I just pass an interface pointer via a method call. This is a very useful technique that I've used in other applications as well.
Figure 3 Establishing a Shared Memory Block
Figure 3Establishing a Shared Memory Block

      The steps for establishing the communication link as shown in Figure 3 are as follows:

  1. The test client creates and maps a section object (backed by the swap file) by calling CreateFileMapping, specifying that the handle should be inheritable. The client then creates an event object (with an inheritable handle) to synchronize the exchange and writes this new handle into the shared memory block. The client blocks on the event, waiting until the server process (which has not been started yet) indicates that it is ready to execute the test.
  2. The client launches the server process (via CreateProcess or CreateProcessAsUser), specifying TRUE for the bInheritHandles argument, and passing the value of the section handle as a command-line argument.
  3. The newly created server process parses its command line, discovers the handle value, and maps a view of the section object using this inherited handle, obtaining a pointer to the client-created shared memory block.
  4. The server creates a COM object that implements IHandleTest, and marshals this interface pointer into the shared memory block via CoMarshalInterface. It then signals the event to indicate to the client that a marshaled interface pointer is ready and waiting in the shared memory block.
  5. The client's thread wakes up and unmarshals the interface pointer that was written by the server, thus retrieving a proxy.
  6. Finally, the client initiates the test by calling IHandleTest::RunTest, passing a pointer to a client-provided COM object that implements IHandleTestSite. This backpointer allows the server to communicate back to the client during the test.
      Rather than using this hand-rolled scheme, I considered (for a brief moment) using the running object table (ROT), but discarded this idea because the ROT's security policy makes it somewhat difficult to use across window station boundaries when the security principals of the processes involved are not configured in the registry beforehand.
      Two things are noteworthy about the surrogate program. First, since I am not using the COM activation mechanism to launch the server, I can't really classify this program as a custom surrogate. However, I actually did end up implementing ISurrogate and calling CoRegisterSurrogate simply because it's a convenient way to receive notification (via ISurrogate::FreeSurrogate) when all stubs in the process have shut down. Unless you're doing something rather esoteric, this generally can be taken as an indication that you have no more external references and that it's safe to shut down the process. This is important since the COM object implementing the test is packaged in a separate DLL so that it can also be activated in-process and, therefore, the surrogate process would have no other way of knowing when it was safe to shut down.
      Note also that the surrogate program immediately calls CoInitializeSecurity to turn off COM security by specifying an authentication level of RPC_C_AUTHN_LEVEL_NONE. I'm only using COM as a convenient communication mechanism, and I don't need or want COM security getting in the way. Of course, the client program also calls CoInitializeSecurity in this fashion. (Remember that both the client and server need to agree to turn off authentication as the default level is negotiated for each new proxy.) Since I am not using the COM activation mechanism, I don't need to bother choosing a RunAs principal or setting up launch permissions; I'm completely in control of the process by simply calling CoInitializeSecurity.

Summary


      Understanding the session-oriented nature of security in Windows is paramount to designing secure, robust systems. Logon sessions and handles are the two major forms of these sessions. In this column I've discussed the rules governing handles.
      Go to the link at the top of this article to get the handle security test harness, or visit my security samples site at https://www.develop.com/kbrown. That's where I'll incorporate any suggestions or bug reports. Enjoy!

Keith Brown works at DevelopMentor, developing the COM and Windows NT Security curriculum. He is coauthor of* Effective COM *(Addison-Wesley, 1999), and is writing a developer's guide to distributed security. You can reach Keith at https://www.develop.com/kbrown.

From the March 2000 issue of MSDN Magazine.