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.


November 1998

Microsoft Systems Journal Homepage

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 developer’s 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
      The process of determining the caller's identity is known as authentication, and understanding COM authentication is probably the most important step to mastering COM security. The simplest way to implement an authentication scheme would be to have a caller (Alice) send her user name and password with the first call so that the object (Bob) can verify that Alice is who she says she is. This will work at most once. (Anyone who uses a network sniffer such as NetMon will now know Alice's password.)
      Windows NT® 4.0 uses a more secure mechanism known as NTLM authentication. NTLM is a simple challenge/response protocol that avoids sending Alice's password across the wire. In a nutshell, Alice contacts Bob and indicates that she wants to be authenticated via NTLM. Bob sends a random 8-byte number (the challenge) back to Alice, effectively asking her to prove her identity. Alice uses this challenge and her password to create a 24-byte response (via various cryptographic functions), and sends this back to Bob along with her name (principal) and the name of her domain controller (authority). Bob presents all this information to the Local Security Authority (LSA) on his machine and asks for proof of Alice's identity in the form of a security token. The LSA routes the information to the correct domain controller via trust relationships. The domain controller eventually performs the same calculation as Alice (it's the only other entity that knows Alice's password) and either verifies Alice's identity or tells Bob that the caller is a fraud.
      Besides proving Alice's identity to Bob, NTLM (and most other authentication protocols) also presents both Alice and Bob with a random string of bits that nobody else knows about. Alice can use this shared secret to sign portions of her messages to continue assuring Bob that it's really still her, or maybe even to hide the contents of the message from eavesdroppers by encrypting it.
      COM security hides all these details behind a set of simple authentication levels (see Figure 1) that you can consider a measure of authentication protection. These constants are purposely arranged in ascending order so that higher numbers represent higher levels of protection (and correspondingly a bigger hit in terms of CPU cycles). The first level, NONE, is a special setting that turns off all authentication. You don't pay for the overhead, but all your callers are anonymous (you can't tell them apart). This might be an appropriate choice if your callers are widely dispersed across the Internet and there is typically no way to authenticate them anyway (certificate-based authentication is designed to make this better in Windows NT 5.0). Most COM applications deployed on an intranet, however, should usually take advantage of the authentication services that COM provides. Whether you choose to use authentication or not, understanding how to control the authentication level is critical.
      Standard COM proxies expose an interface known as IClientSecurity (see Figure 2) that among other things allows the caller to set the authentication level via the SetBlanket function. (The pun on security blanket was intentional, I'm sure.) So ultimately the caller (Alice) is in charge of determining which level of authentication COM will use for a given call.
      COM also provides the IServerSecurity interface (see Figure 3) that Bob can use to find out what level of authentication Alice chose. This allows Bob to reject calls that do not conform to his requirements. During an incoming call, Bob can use CoGetCallContext to reach up into thread local storage, grab the COM-provided implementation of IServerSecurity, and find out exactly what settings the current caller selected. If Bob simply wants to verify that all incoming calls are authenticated, for example

 dwAuthnLevel >= RPC_C_AUTHN_LEVEL_CONNECT
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 Alice—how 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:
 CoInitializeEx(0, COINIT_MULTITHREADED);
 CoInitializeSecurity( 0, -1, 0, 0,
                       RPC_C_AUTHN_LEVEL_CONNECT,
                       RPC_C_IMP_LEVEL_IDENTIFY,
                       0, 0, 0 );
      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
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:
 HKCR\AppID\{AppID}
     AuthenticationLevel = n
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:
 CoInitializeSecurity( 0, -1, 0, 0,
                       RPC_C_AUTHN_LEVEL_CONNECT,
                       RPC_C_IMP_LEVEL_IDENTIFY,
                       0, 0, 0 );
      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 parameter—you control security settings on each interface proxy (and IUnknown) separately.
Figure 6  Inside the Proxy
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:
 typedef struct _COAUTHINFO {
     DWORD               dwAuthnSvc;
     DWORD               dwAuthzSvc;
     LPWSTR              pwszServerPrincName;
     DWORD               dwAuthnLevel;
     DWORD               dwImpersonationLevel;
     COAUTHIDENTITY *    pAuthIdentityData;
     DWORD               dwCapabilities;
 } COAUTHINFO;
      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
      That covers the basics of authentication. Once you've identified your caller, you can implement an access control policy. COM provides a couple of simple mechanisms to help you do this, although they are rather coarse-grained: access permissions and launch permissions. You choose your access permission policy via the first parameter to CoInitializeSecurity (see Figure 4), and this allows COM to screen callers automatically on your behalf. If Bob's access control policy denies Alice, then he will never see any incoming calls from her, as COM will reject them at the door with E_ACCESSDENIED.
      There are four ways of specifying this policy, three of which are generally useful:

  • Pass NULL
  • Pass a pointer to a security descriptor
  • Pass a pointer to an implementation of IAccessControl
  • Pass a pointer to an AppID (used by surrogate processes, but not generally useful otherwise)
The first option, passing NULL, turns off access control checking. This is the only legal setting if you use an authentication level of NONE. (Remember, if all your callers are anonymous, access control is meaningless.)
      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:
 void* AllocSystemSID() {
     SID_IDENTIFIER_AUTHORITY ntauth =
         SECURITY_NT_AUTHORITY;
     void* psid = 0;
     AllocateAndInitializeSid(
         &ntauth, 1, SECURITY_LOCAL_SYSTEM_RID,
         0, 0, 0, 0, 0, 0, 0, &psid );
     return psid; // call FreeSid to clean up
 }
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):
 HKCR\AppID\{AppID}
     LaunchPermission= 01 00 04 80...
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
      Now that I've covered COM's basic access control mechanisms, let's take one more look at the issue of identity. You saw that authentication was the mechanism used to determine a client's identity, and that without knowing who the client is, access control is meaningless. Well, it turns out that as the client, it helps to understand how COM represents your identity when you make an outgoing call.
      By default, all outgoing calls use the process token, regardless of whether you are impersonating or not. Unlike most Win32 APIs you know and love, COM ignores impersonation tokens. So if Alice makes a call to Bob while she is impersonating Steve, the call goes out as Alice (Bob sees Alice as the caller). Windows NT 5.0 is expected to provide a cloaking feature that allows you to explicitly capture the thread token for an outgoing COM call. Until then, COM ignores impersonation tokens completely.
      Here's an interesting identity question: what token should the SCM choose when it launches a COM server in response to an activation request? All processes that run on Windows NT must have an associated security token that identifies the user account on whose behalf the code executes. Well, here are the options the SCM can choose from:

  • The token of the person who happens to be logged on interactively on the server box (Interactive User in DCOMCNFG)
  • The token of the client who made the activation request (Launching User in DCOMCNFG)
  • A token for some other user (This User in DCOMCNFG)

      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:
 HKCR/AppID/{AppID}
     RunAs="Domain\User"
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 exception—you 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:
 HKCR\AppID\{AppID}
     RunAs="Interactive User"
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
      COM security is a very detailed and often subtle topic. I explored most of the major difficulties that you'll experience at one time or another while developing a COM-based project. I'll be back with more discussion of other cool Windows NT security features in future columns. In the meantime, check out my COM Security FAQ Ex2 and the Anonymous Delegator at http://www.develop.com/kbrown.

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.