Code for this article: May99SecurityBriefs.exe (7KB)
Keith Brown works at DevelopMentor, developing the COM and Windows NT Security curriculum. He is coauthor of Effective COM (Addison-Wesley, 1998), and is writing a developer's guide to distributed security. Reach Keith at http://www.develop.com/kbrown.
I thought I'd talk about classic Windows NT® security, with a twist. Most folks think of access control when they think of security, and although it's not rocket science, some features added to Windows NT recently make it much easier to deploy applications that extend the access control model of Windows NT into their own problem domain. After reading this column, you'll have been exposed to some of the basic tenets of access control in Windows NT, and some interesting dirt on window stations and desktops. Most importantly, you'll see a cool new feature of Windows® 2000 that you can use today if you install an add-on component that ships with Windows NT 4.0 Service Pack 4 (SP4).
Basic Access Control in Windows NT
Windows NT strives to provide a security model that is robust and efficient, which means that access control checks, while required, must not be performed willy-nilly. In fact, there is a perfectly logical place to perform access checks: upon opening an object.® is that the programmer creates a new object (or connects to an existing one) and receives some sort of handle. At this time, the programmer declares her intentions and any access checks are performed. As an example, imagine acquiring a handle to a file on a secure disk partition (say, NTFS). In this case, you'd call CreateFile, which requires a DWORD parameter, dwDesiredAccess. This parameter allows you to specify exactly how you intend to use the resulting handle. Let's assume you only plan to read from the file, so you pass GENERIC_READ. The operating system now has all the information it needs to either grant or deny your request. It knows who you are (based on your token), it knows what you want (the name of the file you asked to open), and it knows what you plan to do (read from the file).
The normal metaphor for getting work done in Win32
The system now obtains your token by opening the token for the current thread (if you are impersonating) or for the current process, and obtains the security descriptor from NTFS for the file you requested. It passes the token, the security descriptor, and the permissions you requested (GENERIC_READ) to a kernel mode component known as the Security Reference Monitor (SRM), which decides whether to grant or deny your request. If the SRM says go, the file system hands back a handle that is annotated with the permissions you requested (GENERIC_READ), and no further discussion with the SRM is required since its decision has been cached in the handle.
When you later call ReadFile or WriteFile, each of these functions simply checks the annotation in the handle and either succeeds or fails accordingly. While this mechanism is efficient, it also means that changes to the file's security descriptor have no effect on outstanding file handles. Understanding Windows NT security caching strategies such as this is critical to being able to design systems that are secure and correct.
A security descriptor is simply a variable-length data structure that encapsulates security settings on a per-object basis. If you peer into a security descriptor, you'll see three interesting things: an owner and two lists. The first list is known as the discretionary access control list (DACL), and this is one element of Windows NT security with which most people are familiar. The DACL is a list of entries, each of which either adds or subtracts a set of permissions for a particular principal or group.
Figure 1 shows an example of a DACL that grants the Friends group read and write permissions and denies Bob those same permissions. ("rw" in this case is a human-readable representation of a 32-bit access mask; masks aren't actually stored as strings.) The semantics of DACLs require that all principals that are not listed in the DACL be implicitly denied access, so assuming that Eve is not a member of the Friends group, she is implicitly denied access to the object protected by this DACL.
Also, negative entries (in other words, denied entries) always take precedence over positive entries. This means that even if Bob is a member of the Friends group, Bob will be denied access. When you're editing a DACL interactively, don't let the ordering of entries fool you. Negative entries are always physically ordered first and thus take precedence over positive entries. The various interactive DACL editors provided by Windows NT guarantee that the DACLs they produce will be ordered correctly (even though the order may not be clear from the user interface), and any DACLs you produce must also follow this rule.
Either a principal or group is the logical owner of the object. This setting augments the DACL by implicitly granting the owner the ability to read and write the
DACL at any time, even if the DACL explicitly denies these permissions. This
simply avoids stupid problems like users accidentally locking themselves (and perhaps everyone else) out of secure objects such as huge files stored on an already overloaded server.
In the case where the owner is kidnapped by aliens or suffers some other ghastly fate, this allows an administrator or anyone else who has the TakeOwnership privilege to take ownership of an object from the dearly departed in order to adjust the DACL. This allows the object to be either recovered or deleted, but leaves an audit trail in case the original owner returns from his abduction. (Once you take ownership, you can't give it back.) Understanding what it means to be an owner makes the discretionary in DACL more obvious; access permissions for an object are ultimately granted at the owner's discretion because the owner always has the right to revoke those permissions.
The second list in the security descriptor is known as the system access control list (SACL), and controls auditing for the object. For instance, if auditing is enabled for files, the file system driver instructs the SRM to not only check the DACL, but also to add an audit entry as appropriate if any entry in the SACL matches the principal or groups in the requestor's token. SACL entries can also be positive or negative, but in this case they refer to whether the SRM should generate an audit if a particular permission was granted (positive) or denied (negative).
Windows NT automatically performs all this magic on your behalf when you access system-provided objects such as NTFS files, registry keys, or executive objects (often referred to as kernel objects) such as processes, threads, and so on. However, when you are developing a custom application, there may not be a convenient mapping from logical application objects (such as bank accounts) to individual files or registry keys. In this case, you face a dilemma. You must decide whether to roll your own access control mechanism or to build an access control framework that extends the Windows NT model. Choosing the former implies (among other things) reinventing the security descriptor and losing the ability to inject auditing information into the Windows NT security audit log. Choosing the latter means learning to understand the subtleties of the Windows NT access control API. It also means writing a DACL editor, since the one present in Windows NT 4.0 is not documented. (And, thank heaven, because according to sources at Microsoft, this DACL editor is one that only a mother could love.)
The Access Control Editor
I always preach to my students that,
assuming they had already embraced Win32, they would see more and more benefit over the years by embracing the Windows NT security architecture as well. Lo and behold, along with Windows 2000 comes a brand-spanking-new user interface component that edits entire security descriptors, not just individual DACLsand the DACL editing component is light-years ahead of the classic Windows NT 4.0 editor in terms of usability and functionality.
The new editor exposes all the richness of the DACL, including support for negative entries and inheritable entries, both of which received little or no exposure in the old editor. For complex applications, this provides maximum flexibility. For simpler applications (for instance, where auditing or inheritable DACL entries are not needed), you can turn off these features and avoid complicating the user interface. And to top it off, the main interface to the editor is a COM interface, which provides a consistent mechanism for resource management and will be extensible in the future via QueryInterface.
The good news is that this component can be used with Windows NT 4.0 SP4 if you install an add-on component called the security configuration editor (look in the MSSCE subdirectory of the SP4 CD). If you run Windows 2000 or Windows NT 4.0 with the add-on, you'll notice a new look for the Security tab when you ask for properties on a file stored on an NTFS partition (see Figure 2). This is the Access Control Editor's property page, which you can add to your own property sheets by simply calling CreateSecurityPage and passing a pointer to a COM callback interface, ISecurityInformation. By implementing this interface, you can control the complexity of the page. For instance, is the Advanced button available? Is the Auditing page available?
Figure 2 Access Control Editor Properties
You can also control the exact strings to be used when describing objects and their permissions. Since most folks developing Win32-based applications care about internationalization, the strings may be exposed directly as indexes into a stringtable resource that exists either in your module (the default) or in a separate resource DLL. Besides providing hints for the user interface, ISecurityInformation logically represents a way to get and set the owner, DACL, and SACL for the object in question.
Simultaneous Interactive Logon Sessions
Instead of simply regurgitating the documentation for ISecurityInformation, which is available in the Windows 2000 beta Platform SDK docs, I figured an applied example might be a bit more interesting. With all the poking around I do from day to day, researching the dark corners of Windows NT security, I really needed a tool that would make it fast and easy to simulate different principals logging in to my workstation and making outgoing calls to remote servers using different credentials. I simply got sick of logging in as Alice, running some client code, logging out, logging back in again as Mallory, running the same code, and so on.
I built a simple little application that, given a set of credentials (authority, principal, and password), temporarily installs itself as a service, calls LogonUser, and then calls CreateProcessAsUser to spawn a command prompt from which I can type arbitrary commands running under a specified user's logon session. Once the command prompt is launched, the service removes itself (by calling DeleteService) and shuts down. I named this tool cmdasuser, and you can download it along with the source code from the link at the top of this article, or from my security sample gallery at http://www.develop.com/kbrown.
The cmdasuser service illustrates an approach to ease the use of calling certain privileged Win32 API calls for developers. The method used to install and remove the service dynamically would not be an appropriate solution for deployed code, which needs to make privileged API calls. An alternative, deployable solution is to install a permanent service that exposes Remote Procedure Call (RPC) interfaces to the privileged operations. This service could implement its own access-checking methods to regulate access to the RPC interfaces.
Cmdasuser installs itself as a service as a convenience; to call LogonUser and CreateProcessAsUser, you must have special privileges that most developers don't have. But to install a service, you only have to be a member of the local administrators group, which fits the bill for most developers I know. Services running as LocalSystem have the somewhat esoteric privileges required to call these functions.
Being a savvy Windows NT security guy, I knew that
I couldn't simply launch a process with arbitrary
credentials and direct it onto the interactive desktop because it wouldn't be granted access to the interactive window station and desktop. I had to do
something special to make it work. By the end of
this column, I think you'll agree that the solution wasn't obvious.
Interlude: Window Stations and Desktops
For those who are not familiar with these terms, a window station is simply a secure USER object that is associated with a process and contains a clipboard, an atom table, and a set of desktops. The interactive window station is named winsta0, and is the only station that can receive mouse and keyboard input from the interactive user. In other words, this is where all GUI-based applications need to reside if they want any sort of interaction with the user sitting at the console. There are also noninteractive window stations where daemon processes such as Windows NT services and COM servers typically live, but for this column I'm only concerned with winsta0.
A desktop is associated with a thread and is a place where windows can be created and managed. There are three desktops that you normally work with on a day-to-day basis. The desktop named Default is the one you are looking at if you are doing any normal work on Windows NT. If you press Control-Alt-Delete, you will be switched to a desktop named Winlogon. Whenever your screen saver activates, you are automatically switched to yet another desktop named Screen-saver, thus protecting your work-in-progress on the default desktop from unwanted snoopers who wander into your cubicle. Note though, for Windows 2000 unsecured screen savers will run on the winsta0\default desktop.
Each window station and desktop has a DACL associated with it. This allows you to place user interface components, which normally have no worries about security, under an umbrella that protects them from daemon processes on the machine. For instance, you might feel uncomfortable running a COM server on a machine built by your MIS department if you realized that it might be silently capturing your screen every few minutes, sending a picture of your current Freecell game to the CEO, who might fire you because you missed an obvious move.
Figure 3 Trying to Load an App Without Permissions
You could run the COM server under a separate account (see my November 1998 column on COM security for details on how to do this), which would not have access permission to the interactive window station and desktop. In this case, COM creates the server in its own noninteractive window station, and even if that server attempted to start a screen-capturing process in winsta0, it would suffer the same fate as my command shell (see Figure 3). Many of you have seen this dialog and perhaps wondered what it meant. This occurs when USER32.DLL is loaded into an application whose corresponding logon session has not been granted the correct permissions to the window station in which the application was created, or the desktop on which the primary thread was created.|
Using the Access Control Editor
I'll show you how to fix that problem later, but first I'll present the tool I built to explore the issue in the first place. This application simply pops up a property sheet with a couple of instances of the Access Control Editor's property pages, displaying the DACLs for winsta0 and the default desktop. To make this happen, I first had to provide a table that described each physical permission (the access mask) along with the stringtable index of its description. For example, I mapped the DESKTOP_CREATEMENU permission to a stringtable entry "Create Menus". The table is simply a contiguous array of SI_ACCESS structures. (The SI prefix indicates that this structure is associated with the ISecurityInformation interface. See Figure 4 for the definition of SI_ACCESS and ISecurityInformation.) documentation for PropertySheetProc if you want to see how this fits into the big picture. Next in line is the GetAccessRights method, which allows you to provide a pointer to the table of access permissions and description stringtable indexes. This function also allows you to specify a default mask so when a new principal or group is added to the DACL (or SACL), a positive entry with the default mask will be added.
Since I wanted property pages for two classes of objects (window stations and desktops), I actually created two of these tables. While I'd love to show these tables in this column, they are quite elegant and understandable when viewed on a 120-column display. They would be somewhat less elegant and understandable if scrunched onto this page, so please download the sample source code and have a look. I've shown excerpts of my ISecurityInformation implementation in Figure 5 for reference. When you download the code, you'll notice that I factored most of it into a base class, since the window station and desktop implementation have a lot in common. The ISecurityInformation interface makes this easy to do since COM interfaces are polymorphic.
When you implement ISecurityInformation, the basic sequence of events during property page initialization is pretty much what you'd expect. The first call is to GetObjectInformation, where you need to specify what features you'd like on the property page (for example, what parts of the security descriptor you want to expose). You may also customize the strings for the name of the object whose security settings are being edited, as well as the title of the property page itself.
The next call is to the PropertySheetPageCallback method, which is simply a pass-through from the property sheet common control that you will generally ignore. Check out the MSDN
Finally, in preparation for displaying the DACL (which is the information displayed directly on the property page), your GetSecurity and MapGeneric methods are invoked. The first method expects you to provide a security descriptor populated with just the information requested by the property sheet. (This is indicated by the RequestedInformation parameter.) In this case, you'll only be asked for the DACL, since this is the only information shown on the property page when it first appears. The MapGeneric function allows you to specify how generic permissions are mapped for the class of object being edited. I had to generate two mappings, one for window stations and one for desktops. Since these mappings are not explicitly documented, I used common sense to construct reasonable approximations.
Once the property page has been displayed, a couple of interesting things might happen. The user may press the Advanced button (if you've exposed it), or the user may make changes to the DACL and apply those changes. In the first case, you'll see more traffic through your GetAccessRights method, which will indicate that the Advanced page is about to be displayed. Generally, you can ignore this flag and always return the same SI_ACCESS table for the DACL because each entry in the table has a flag indicating whether the permission should be displayed in the normal view or only in the advanced view. This is a cool feature because you can declaratively provide two levels of detail for users: basic permissions (which can include multiple permissions lumped into one entry), and advanced permissions (where the gloves come off and you provide maximum flexibility for your more adventuresome users).
If you specified the SI_CONTAINER flag to denote your object as a container, your GetInheritTypes method will also be invoked at this time, giving you a chance to provide the string mappings for the dropdown listbox shown in the advanced Access Control Editor. You'll often see window station DACLs that contain inheritable entries. Providing this string mapping makes it easier to discern which entries in the DACL are purely for inheritance.
When the user selects the Auditing page (if you've exposed it), GetAccessRights will indicate (via the dwFlags parameter) that it needs your table of SI_ACCESS entries for audit mappings, not access control mappings, so watch out for this case and be sure to provide a separate table. My example explicitly hides the Auditing page, so you won't see any code checking for this case in my implementation.
If the user makes changes, you won't be notified until the user presses the Apply or OK button on the property sheet. At this point, the property page will ask you to update the security descriptor with whatever information has changed. You will be provided a security descriptor containing just the parts that changed (typically just the DACL), along with a bitfield that indicates which parts changed. My code simply pulls out the various pieces and calls SetSecurityInfo to apply them directly to winsta0 or the default desktop, as the case may be.
Once you've implemented ISecurityInformation and are ready to drop it into a property sheet, simply call CreateSecurityPage, passing your ISecurityInformation interface pointer as input, and you'll get back an HPROPSHEETPAGE that you can pass to the PropertySheet function. The PropertySheet function simply wraps a frame around the property sheet and displays it in a modal dialog. If all you want is a property sheet around a single Access Control Editor property page, you can call EditSecurity, which calls PropertySheet for you. See Figure 6 for sample code.
In order to use the Access Control Editor, you'll be relying on ACLUI.DLL (and most folks will also link to ACLUI.LIB, the associated import library, which ships with the Windows 2000 beta Platform SDK). This DLL is normally installed only when the security configuration editor add-on that ships with SP4 is also installed.
Memory Management Gotchas
ISecurityInformation strays significantly from the COM memory management model, where [out] parameters are allocated using the COM heap (via CoTaskMemAlloc and friends). Since this is a [local] interfacetechnically, it wasn't even written in IDLyou'll never see it on a proxy. So this doesn't break the interface, but you must follow the interface's documented rules carefully for memory management to get it right. It is actually quite efficient and reasonable, if not terribly consistent. For one thing, most of the pointers (to strings, the SI_ACCESS table, and so on) that you return to the property page via [out] parameters are never freed (or otherwise modified) by the property page. Generally these are pointers to static data anyway, which makes the programming model extremely simple.
If you're doing anything more complex, you'll need to manage memory on behalf of the property page, and wait to free those pointers until the property page releases your ISecurityInformation implementation. When the property page calls your GetSecurity function, you'll need to make sure you allocate a self-relative security descriptor using the local heap allocator (LocalAlloc), or using a function that allocates it for you in the same way. My example simply calls GetSecurityInfo to reach into winsta0 (or the default desktop) and get a security descriptor filled with the necessary information. GetSecurityInfo explicitly uses LocalAlloc, so I don't have to jump through any memory management hoops.
Window Station and Desktop DACLs
After building and running the tool, you should be able to explore what happens when you deny yourself various permissions on winsta0 and the default desktop. For instance, try denying yourself the "Access the Clipboard" permission on winsta0, and then run an instance of Notepad. You won't be able to copy or paste from that instance of Notepad, because the handle it has to winsta0 excludes the permission needed to call OpenClipboard. However, note that any other applications that were running before you made the change are totally unaffected. Recall the caching mechanism I discussed at the start of this column; this is just another example of that kind of caching.
Try denying yourself the "Create Menus" permission on the default desktop, and run another instance of Notepad. Gee, this is fun! Be careful if you launch any other (potentially more harmful) applications while you've been tweaking your DACLs because you could very well pay the price; most developers don't expect basic functions like CreateMenu to fail, and it may do dangerous things like format your hard drive or reset your Freecell high scores. In any case, you can now use this tool to grant access to other principals, and I can happily party on with my cmdasuser tool, at least theoretically.
An Interesting Twist
If you download and test these tools in concert, perhaps by creating a local account called FooBargranting it all permissions to winsta0 and the default desktop using the code I explored in this column, and then running cmdasuser to spawn an interactive command shell running as FooBaryou may have gotten a nasty surprise (see Figure 3). For some reason, the window station and desktop access permissions you specified for FooBar didn't seem to take effect, and the command shell (cmd.exe) running as FooBar was denied access to winsta0 and the default desktop (thus the dialog in Figure 3). You can even try granting all access to winsta0 and the default desktop to Everyone, and you'll still run into the same problem. What's going on?
It turns out that except for some very specific special cases, window stations and desktops don't perform access checks using the principal SID and all the group SIDs in your token. Instead, they perform access checks to discover what permissions have been granted to the logon session for which the token was created. If this sounds strange, read on.
When you log on to Windows NT interactively, you get a special SID in your token (stored with the other group SIDs) known as a Logon SID. This SID is generated dynamically based on a unique identifier associated with your logon session (the session ID), and allows permissions to be granted specifically to your current logon session (it's like granting access to a specific instance of you, rather than you as a whole). As an example, if you call LogonUser for FooBar twice in a row, you'll get two distinct logon sessions, each with a different session ID. Granting access to the logon session SID for a single logon session is much more fine-grained than granting access to FooBar as a principal.
Understanding this model is critical to solving the problem with the cmdasuser tool (in other words, working around the error message shown in Figure 3). To solve the problem, you need to explicitly grant access permissions to the logon session, whose ID you won't know about until you call LogonUser to create the session in the first place. So with some extra typing you can get this to work reliably: call LogonUser to get a token for FooBar, then find the Logon SID by calling GetTokenInformation and enumerating
all the groups until you find the logon session SID (it's easy to find as it is marked with the SE_GROUP_LOGON_
ID flag). Finally, add entries to the DACLs of winsta0 and the default desktop, and grant this SID all access permissions. Now the command shell will launch successfully and any commands you type will execute in the security context of FooBar. The sample code in cmdasuser demonstrates this technique.
Interestingly enough, if you examine a token for LocalSystem, you'll notice that it does not have a logon session SID, which seems to imply that you cannot run a service (with a GUI) as LocalSystem in winsta0. However, this is clearly not the case, as evidenced by the checkbox in the Services control panel applet, which allows services running as LocalSystem to run in winsta0 on the default desktop. Well, there is a funky backdoor that makes it possible to grant window station/desktop access to LocalSystem (or any other member of the local Administrators group). Window stations and desktops make special arrangements for members of the local Administrators group, and don't limit their access checks in this case to the logon session, which means that you can grant permissions directly to the Administrators group (or the LocalSystem account). In fact, LocalSystem is granted all permissions to winsta0 and the default desktop by default. (You can see this by running the sample code for this column.)
Finally, any negative entries in a window station or desktop DACL are always applied, regardless of whether they apply to the logon session, the principal, or one of the groups in which the principal is a member. So you can safely deny access to any principal or group, but when granting permissions, you must grant access to specific logon sessions if you want them to party on your window station.
I hope you find these tools (winstadacl.exe and cmdasuser.exe) useful in exploring Windows NT security. I'd like to send a warm thanks to Steve Rodgers for staying up late in his hotel room in Caversham helping me test hypotheses about window station security, and to Brent Rector for giving me some great feedback before this column went
For those of you who read my last column on NULL sessions and the Guest account, be sure to check out my Web site (http://www.develop.com/kbrown), as it discusses an important change that occurred in SP4.
Have a question about security? Send it to Keith Brown at http://www.develop.com/kbrown.