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.
Exploring Handle Security in Windows | ||||
Keith Brown | ||||
Code for this article:Mar00SecurityBriefs.exe (265KB) |
||||
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. The Conceptual ModelFrom 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 HarnessPrior 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.
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). DirectThe 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 ObjectThe 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.DuplicateHandleThe 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.InheritanceAt 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 HandleAt 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.AllIteratively use all of the previous transmission options, trying one after another.The Test Harness at WorkThe 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. Implementing the Test HarnessSince 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.
The steps for establishing the communication link as shown in Figure 3 are as follows:
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. SummaryUnderstanding 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.