November 1998
Download Nov98Sec.exe (4KB)
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 developers guide to distributed security. Reach Keith at http://www.develop. com/kbrown.
Q
I am having difficulty getting my COM-based distributed application to work. A coworker told me that many of the problems I'm encountering are probably related to security settings. How can I get a handle on COM security?
Carol Martinez
Port Angeles, WA A Even if security isn't your top priority, you need to deal with it if you plan on developing COM-based distributed applications. Security in COM is on by default, and if you don't have a reasonable grasp of the basic concepts, you will have a difficult time getting your application to behave in a predictable way. The first step to understanding COM security is to break it down into its fundamental parts. Like most secure systems, COM security is based on identity and access control. When you think of a secure application, the first thing that comes to mind is probably access control. However, ask yourself this question: how can you possibly decide whether to grant or deny access to a caller if you are not sure who the caller really is?
Authentication Basics
he has to remember to write code at the top of each and every method call to check the authentication level and return E_ACCESSDENIED if the level is unacceptable. This is not only tedious and error prone for Bob, but it also presents a problem for Alicehow can she possibly determine Bob's minimum level of authentication? She is left guessing, and if she guesses wrong, Bob will fail the call with E_ACCESSDENIED.
To solve these problems, COM provides a simple mechanism that allows a process to specify an applicationwide minimum authentication level. COM automatically rejects all calls that come in below this low water mark and returns E_ACCESSDENIED to the caller. Bob simply needs to call CoInitializeSecurity to control this minimum level. This function is prototyped in Figure 4. CoInitializeSecurity is one of the most subtle and misunderstood functions in COM. It can only be called once per process, and the thread that makes the call must already have joined an apartment by calling CoInitialize(Ex). Here's the code Bob might use to set a minimum authentication level of CONNECT:
There are three parameters in this call that relate to authentication: cAuthSvc, asAuthSvc, and dwAuthnLevel. The first two parameters specify the security support providers (the authentication services). Passing 1 and 0 for these is almost always the right thing to do, as it allows COM to choose the most appropriate provider. The current provider is NTLM, but in Windows NT 5.0 the default is scheduled to be the security negotiation (Snego) protocol, which will allow both parties to automatically take advantage of the best authentication protocol that they have in common. dwAuthn-Level is the processwide low water mark I discussed earlier.
So far, I've only solved Bob's problem. Alice still needs some way to discover Bob's minimum authentication level so she can correctly configure the proxy via SetBlanket. COM provides a simple solution here as well. It turns out that when Alice imports Bob's interface pointer, during a process known as OXID resolution COM obtains Bob's low water mark. (The OXID resolver is part of the internal implementation of the COM infrastructure. See the DCOM wire protocol specification at http://www.microsoft.com/com for more details.) So the proxy that Alice receives is configured automatically with an authentication level that is at least as high as Bob's low water mark. This works the same way if Bob first exports the pointer to Susan, who sends it to Joe, who then sends it to Alice. No matter what path the pointer takes, the exporter's low water mark is discovered during the OXID resolution request, which is always sent to the original exporter (Bob). Alice doesn't need to call SetBlanket at all because the proxy starts with a default authentication level high enough to satisfy Bob. The proxy doesn't necessarily start exactly at Bob's required setting, however. This is because the dwAuthnLevel parameter to CoInitializeSecurity is overloaded and has two meanings, one for exporters, and one for importers. This means that Alice can also call CoInitializeSecurity and specify dwAuthnLevel. For importers, this represents the minimum level of authentication for all newly imported proxies. The net effect is that the default authentication level on a proxy is the highest of the two dwAuthnLevel settings between the exporter and the importer. |
Figure 5 Negotiating Authentication |
Most of the confusion about setting COM authentication levels comes from this overload. Figure 5 shows how proxies negotiate the default authentication level. Processes C and D have called CoInitializeSecurity to specify their low water mark, and have exported several interface pointers. Processes A and B have imported these interface pointers, but since they also called CoInitializeSecurity, the proxies they import will default to authentication levels that are the highest of the two settings. This seems straightforward, but there are some difficulties with the dwAuthnLevel overload. Consider a case where you want to turn off authentication completely for an Internet-based COM application. The server (Bob) should call CoInitializeSecurity and specify a low water mark of NONE, since otherwise all anonymous calls would be blocked at the door. However, if the client (Alice) calls CoInitializeSecurity and specifies CONNECT or higher, the proxy will take the highest of the two (CONNECT) and will attempt to authenticate with Bob. When authentication fails, the call will be rejected with E_ACCESSDENIED. Ouch! This error message may seem odd, since Bob explicitly stated his willingness to accept anonymous (unauthenticated) calls. However, authentication often protects the client as well. Most authentication protocols (such as Kerberos) support mutual authentication, which guarantees to the client that the server is authentic. Later, I'll show you how Alice can work around this problem to make calls to Bob successfully. But first, I'll demonstrate that there are many ways this rather ugly situation can crop up. What happens if you don't bother to call CoInitializeSecurity explicitly? COM will automatically call it on your behalf as soon as you do something interesting (like import or export an interface pointer). What settings will COM use? It depends on the service pack of Windows NT 4.0. SP3 and earlier uses a machinewide default, while SP4 will use an application-specific setting:
These registry settings are configurable (meaning easily hosed) by any member of the local administrators group who knows how to run DCOMCNFG.EXE. The moral of the story is to call CoInitializeSecurity explicitly to wrest back control of your security environment.
I mentioned earlier that you are allowed to call CoInitializeSecurity only once per process. If you attempt to call it a second time, you'll get the dreaded RPC_E_TOO_LATE error. If you think about it, this implies that inproc servers cannot call CoInitializeSecurity reliably. In fact, what if a process (say, a browser) doesn't even bother to call CoInitializeSecurity? Well, the first interesting COM-related thing the browser does will cause COM to call this function implicitly using registry settings. An ActiveX® control downloaded and instantiated on a client's machine lives in this environment. Harsh! What if that control wants to use COM to gather data from some remote server across the Internet? What will the default authentication level on the proxy be? Well, the act of importing the proxy to the remote object in the first place is considered interesting to COM, which will make the following call on behalf of the browser:
I am making an educated guess for the authentication level because this is the default registry setting, and you can almost guarantee that most of your Internet clients have not used DCOMCNFG to lower it to NONE for you. Since the default authentication level on the proxy is the highest of the two settings, you should assume that your Internet controls will live in a process with CONNECT-level authentication. If you make calls directly on this proxy, your call will generally fail with E_ACCESSDENIED because COM cannot authenticate your client.
To turn off authentication, use IClientSecurity::SetBlan-ket (or the shortcut CoSetProxyBlanket) explicitly to drop the authentication level on the proxy to NONE. Look closely at the interface proxies, as well as the IUnknown implementation on the proxy manager itself in Figure 6. They actually maintain security settings individually. This is why QueryBlanket and SetBlanket take an interface pointer as their first parameteryou control security settings on each interface proxy (and IUnknown) separately. |
Figure 6 Inside the Proxy |
I developed a COM object called the Anonymous Delegator that automatically drops the authentication level for every interface (including IUnknown) on a proxy. If you're not using the delegator or something similar, you'll need to call SetBlanket manually on each interface you plan to use (see Figure 7). One final note about controlling authentication levels: when you make an activation request in COM (via CoCreateInstanceEx or CoGetClassObject), you are indirectly (through the SCM) making calls into a COM server, yet you don't have a proxy on which to call SetBlanket to control the authentication level for this request. If you need to turn off authentication for these calls (or otherwise adjust the authentication settings), you'll have to specify a COSERVERINFO that points to a COAUTHINFO structure containing these settings. Note the similarity of COAUTHINFO to the parameters in SetBlanket:
Your COAUTHINFO settings will only control the authentication settings used for the activation request (the call to CoCreateInstanceEx or CoGetClassObject). The proxy you get back will have default settings that are negotiated as described previously, so you will most likely still need to call SetBlanket as well before you make any outgoing calls through the proxy. It is important to note that the authentication level that an activator (Alice) specifies via CoInitializeSecurity has absolutely no effect on the activation calls she makes. Recall that for importers, dwAuthnLevel sets the default authentication level for proxies; this has nothing to do with activation calls.
If you do not explicitly specify a COAUTHINFO for a remote activation call, the SCM will attempt to establish at least CONNECT-level authentication over any installed network transport supported by COM, stopping only when authentication succeeds or all network transports have been exhausted. At that point, COM will retry without authentication. While this is a reasonable default that works well if authentication succeeds, it can be incredibly expensive if the client cannot be authenticated. (Depending on the network transports installed, you might find yourself waiting minutes for all these requests to either fail or timeout.) The moral of the story is that if Alice and Bob do not require authentication, then for each remote activation request, Alice should explicitly specify a COAUTHINFO that turns off authentication by setting dwAuthnLevel to RPC_C_AUTHN_ LEVEL_NONE.
Access Control
Passing a pointer to a security descriptor is very straightforward except for a couple of gotchas. You simply create a security descriptor whose DACL contains the access control policy. However, CoInitializeSecurity is very fragile with regard to this security descriptor. It must be in absolute format, and the Owner and Primary Group SIDs must be set to some legal value. (Yes, these requirements are inconsistent with the majority of the Win32® API.) If you don't form the descriptor properly, you'll get a somewhat less than helpful E_INVALIDARG result. Passing a pointer to an implementation of IAccessControl is just a fancier way of passing a security descriptor. IAccessControl is a COM interface that represents a discretionary access-control policy. Today its primary use is for platforms not based on Windows NT (such as Windows® 9x and Unix). Whatever mechanism you choose, the permission you need to either grant or deny is COM_RIGHTS_EXECUTE (0x00000001). Do not attempt to use generic rights (such as GENERIC_EXECUTE and GENERIC_ALL) with COM security, as there is no mapping for them and COM ignores them completely. Also, you need to be absolutely certain to grant the SYSTEM account access permissions. Failing to do this will result in severe punishment, including random errors like E_OUTOFMEMORY. The SYSTEM account requires access because the COM SCM and OXID resolver run under this account and need access to your process to perform bookkeeping functions like lazy-use-protseq, reference rundown from unreachable client processes, and so on. Here's some sample code that programmatically forms the well-known SID for the SYSTEM account, so you won't get nailed by localization issues:
Use this well-known SID in the security descriptor you pass to CoInitializeSecurity.
While access permissions specify who is allowed to make COM calls into a process, launch permissions tell the SCM which clients are allowed to start COM servers via activation requests. (The SYSTEM account does not need to be granted launch permissions.) If your server expects anonymous clients, you may grant them launch permissions via the World SID ("Everyone"). The SCM checks for this special case explicitly. Since your server is not yet running when the SCM evaluates launch permissions, this setting must be specified in the registry (versus via CoInitializeSecurity):
Setting this named value is as easy as dumping a self-relative security descriptor to the registry. If you do not explicitly specify a LaunchPermission-named value for your COM application, the SCM will use a machinewide default setting.
Identity
The first option has some serious problems. You will never be able to predict whether your server runs with administrative privileges or whether it runs with guest privileges. It depends on who happens to be logged on. What if no one is logged on? In this case, the activation request would fail. The second option is even worse. For every distinct client principal, you'll need a new copy of the server process, regardless of whether you register your class objects with REGCLS_MULTIPLEUSE. Yikes! While it turns out that this option is the default and is pure, unbridled evil for distributed applications, it's the most secure setting for legacy servers. Option three is almost always the best choice for distributed applications. Pick some user account you'd like your server to run as. (Ideally your install program would create this account programmatically via NetUserAdd and friends, or via ADSI.) This is known as the RunAs setting for your server. When the SCM evaluates this setting, your process is not yet running, so you must set this value in the registry:
You also need to set the password for this account in the registry, although it is stored as a secret in the local security policy, accessible only by administrators via a little-known interface known as the LSA API. In case you're interested, this API is documented in a WinHelp file that is buried in the LSASAMP Platform SDK sample. Figure 8 contains some sample code that sets this password.
Here's a tip: whatever user account you specify for your RunAs principal, it must be granted the "Logon as a Batch Job" privilege on the machine where your COM server runs (with one exceptionyou must set this on the primary domain controller if your server runs on a backup domain controller). If you forget this step, your server will fail to start in response to activation requests. When you specify the RunAs principal via DCOMCNFG, it does its best to grant this privilege automatically, but (at least in SP3) it has trouble on backup domain controllers. Figure 8 shows you how to set this up programmatically so you don't have to rely on DCOMCNFG. Using the token of the interactive user is actually quite useful when debugging, and you can choose it via the following RunAs setting:
If your server puts up diagnostic message boxes in your debug build (remember what ASSERT does when it fires?), you will definitely want to switch to this setting and maintain an interactive logon when running your debug build. Otherwise, your server (run as "This User" or "Launching User") will probably be in a noninteractive window station where message boxes appear, but no user is there to notice and dismiss them. Consequently, your server will appear to hang rather than displaying that helpful ASSERT box where you can see it. Most developers find this behavior rather disconcerting. DCOMCNFG is a great way to switch your server's RunAs setting during development and testing.
Conclusion
Have a question about security? Send it to Keith Brown at http://www.develop.com/kbrown. |
From the November 1998 issue of Microsoft Systems Journal.