http://www.codeguru.com/cpp/com-tech/activex/security/
COM Security Primer, Part I
Next to threading and apartments, security is the part of COM that seems to cause developers the most pain. Nearly everyone's first experience with DCOM is to attempt to launch a remote COM server, only to be met with an E_ACCEESSDENIED error. E_ACCESSDENIED may be the most hated HRESULT in all of COM, because it tells you that the security subsystem prevented you from doing what you wanted to do, but it doesn't tell you why you were prevented from doing it.
I get regular e-mail messages from developers who have written COM applications that work on one machine, but fail miserably when they're deployed to work across machines. In almost every case, the problem is security. Security isn't a big deal when COM clients and COM servers reside on the same machine, but it's a huge deal when activation requests and method calls start flying between machines. COM can't simply allow someone to walk up to a machine, log in, and begin launching remote server processes on other machines; to do so would constitute a hole in security. If you want a COM client to launch a COM server on another machine, you must configure the security subsystem to allow it. And that's not all. There's also access control, remote server process identity, and authentication to think about. If you get a distributed COM application to work without factoring all these aspects of the COM security model into your design, you got lucky. And even though your application runs fine right now, something as simple as a user logging out from a remote server might cause it to fail.
The story of COM security is a story that needs to be told. In this article, the first in a two-part series, I'll discuss two important aspects of the COM security model: activation security and access security. Two articles can't hope to convey everything there is to know about COM security, but they can help you build the fundamental understanding necessary to have the security subsystem work for you instead of against you.
Activation Security
Let's say Bob walks up to a machine running Windows NT or Windows 2000, logs in, and runs an application that attempts to create a COM object on another machine on the network. By default, that attempt will fail. If Bob is to launch remote server processes on other machines, Bob-and anyone else who wishes to launch a server process remotely-must be granted permission to do so. COM refers to such permissions as launch permissions.
Both types of launch permissions are recorded by writing access control lists (ACLs) to the registry of the machine on which the COM server is installed. An ACL is a standard NT security construct used to identify users and groups of users. Each entry in a launch ACL specifically grants or denies launch permission to a user or group of users. Server-specific launch permissons are applied by adding a value named LaunchPermission to the registry's HKCR\AppID\{} key and assigning to that value a binary ACL, as shown in Figure 1. (HKCR stands for HKEY_CLASSES_ROOT; {}is the COM server's AppID.) Default launch permissions are applied by adding a DefaultLaunchPermission value to the registry's HKLM\Software\Microsoft\Ole key and assigning it an ACL, as shown in Figure 2. (HKLM is short for HKEY_LOCAL_MACHINE.) Because it's no fun to encode ACLs by hand, launch permissions are normally added to the registry by running DCOMCNFG, a configuration tool that comes with COM.
Figure 1: Server-specific launch and access permissions
Figure 2: Default launch and access permissions
The upshot of all this is that if Bob runs a COM client on machine A that launches a COM server on machine B, Bob must be granted launch permission on machine B. that permission can come from a LaunchPermission entry or a DefaultLaunchPermission entry, but it must come from one of the two. Bob must also be authenticatable on machine B. If Bob logged in using a domain account and both machine A and machine B belong to that domain, then Bob can be authenticated on both machines. However, if Bob logged onto machine A using a local account, then machine B must have an identical local account (same user name and password), or else the activation request will fail.
Note that you can effectively disable activation security by granting launch permission to Everyone. Granting launch permission to Everyone is the only way to allow anonymous (unauthenticated) users to launch remote COM servers.
As an aside, I often receive e-mail from developers asking whether it's possible for a COM client to launch a remote COM server using an alternate identity. For example, suppose Alice has permission to launch a particular COM server, but Bob doesn't. Can Bob launch a remote COM server as if he were Alice? The answer is yes-if Bob knows Alice's user name and password. The trick is to initialize a COAUTHIDENTITY structure with Alice's credentials, put the address of the COAUTHIDENTITY structure in a COAUTHINFO structure, put the address of the COAUTHINFO structure in a COSERVERINFO structure, and pass the address of the COSERVERINFO structure to a COM activation function such as CoCreateInstanceEx or CoGetClassObject. If you do this, be sure to specify an impersonation level equal to or higher than RPC_C_IMP_LEVEL_IMPERSONATE in the COAUTHINFO structure's dwImpersonationLevel field. Otherwise, the activation request will fail.
Power COM Programming Tip |
Ever been frustrated by CoCreateInstance or CoCreateInstanceEx calls that take a minute or more to succeed? The delay is probably due to the fact that an unauthenticatable caller is attempting to launch a remote server process, which is perfectly legal if you've granted launch permission to Everyone. The problem is that the local COM Service Control Manager (SCM) tries first to establish an authenticated connection to its counterpart on the remote machine, and only falls back to an unauthenticated connection after its earlier attempts fail-hence the delay. The solution is to pass CoCreateInstanceEx a COSERVERINFO structure containing the address of a COAUTHINFO structure whose dwAuthnLevel field is set to RPC_C_AUTHN_LEVEL_NONE. This tells the local SCM to go directly to an unauthenticated connection and bypasses the time-consuming attempt to create an authenticated connection to a remote SCM. |
Access Security
Activation security allows a system administrator-or anyone who has permission to edit the registry-to control who can launch remote server processes. Access security governs who can call into remote server processes once they're launched. Generally speaking, the same users and groups of users that enjoy launch permission should be granted access permission, too. There are exceptions, however. Occasionally it's useful to allow someone to call into a running server (or client) process but not grant them permission to launch the process to begin with.Like launch permissions, access permissions can be applied to individual COM servers and to entire machines. ACLs used for server-specific access permissions are stored in the registry at HKCR\AppID\{}\AccessPermission, where once more {}is the COM server's AppID (see Figure 1). Default access permissions, which define who can make method calls to a remote server process that isn't accompanied by server-specific access permissions, are stored at HKLM\Software\Microsoft\Ole\DefaultAccessPermission (see Figure 2). The values assigned to AccessPermission and DefaultAccessPermission are serialized ACLs whose entries explicitly grant or deny access to individual users and groups. As with launch permissions, access permissions recorded in the registry are typically put there with DCOMCNFG.
That's one way to apply access permissions: write them to the registry. But there's another way. CoInitializeSecurity is a COM API function that a process can use to create its own programmatic security blanket. It can only be called successfully once per process, so if you call it, you must call it early in the process's lifetime (before COM beats you to the punch). The first parameter to CoInitializeSecurity defines the process's access permissions. It can be set to any of the following values:
- NULL
- A pointer to a GUID that corresponds to a registered AppId
- A pointer to an IAccessControl interface
- A pointer to a security descriptor containing an ACL
Which type of value the first parameter holds is indicated by a flag passed in CoInitializeSecurity's eighth parameter (dwCapabilities). Setting the first parameter to NULL prevents access checks from being performed, effectively turning access control off for this process. Passing a pointer to an AppID causes COM to set access permissions according to the AccessPermission ACL found in the registry under that AppID. Passing a pointer to a security descriptor assigns access permissions using the ACL in the descriptor. Finally, passing a pointer to an IAccessControl interface permits a COM server to take control of access checking and to succeed or fail individual calls as they arrive based on the caller's identity-in effect, to implement its own security policy. By implementing IAccessControl, a COM server could, for example, accept calls from users whose user names begin with A through K and reject all others.
Many programmers believe that setting access permissions with CoInitializeSecurity is superior to relying on ACLs in the registry. Why? Because CoInitializeSecurity ensures that your carefully applied access permissions can't be destroyed by someone with the freedom to edit the registry. It's unfortunate that launch permissions can't be applied with CoInitializeSecurity, too, but they can't, because a process has to be launched before it can call CoInitializeSecurity, and once it's launched, it's too late for any API function to prevent the launch from occurring.
Whether you choose to apply access permissions declaratively (with DCOMCNFG) or programmatically (with CoInitializeSecurity), there's one detail you mustn't forget. Be sure to give the SYSTEM account-the built-in account under which most NT services run-access permission to your COM servers if you want clients to be able to activate them remotely. During one critical phase of the remote activation process, a part of COM that runs in an NT service must call into the freshly launched server process. If you've denied the SYSTEM account access permission, the call will fail and the client's activation request will fail, too. Due to a bug in Windows NT 4.0, the HRESULT returned to the client might be E_OUTOFMEMORY instead of E_ACCESSDENIED.
What it all means is that if you want Bob to be able to launch a remote server process and then make method calls into that process, you should grant Bob access permission as well as launch permission. Failure to do both will result in the infamous E_ACCESSDENIED errors that COM programmers have come to know all too well.
Power COM Programming Tip |
One of the errors that newbies often experience when they first begin tinkering with DCOM occurs when they fail to give a remote server process permission to perform callbacks to a client. Suppose a client on machine A launches a COM server on machine B and receives an interface pointer in return. Then that client passes an interface pointer of its own to the server so the server can perform callbacks. What's wrong with this picture? Nothing, except for the fact that callbacks will only be permitted if the server process is granted access permission to the client process. If the server process is assigned the identity Mister Server, then Mister Server must be granted access permission to the client process. One way to grant that access permission is to have the client process call CoInitializeSecurity. Another way is to include Mister Server (or Everyone) in the client machine's DefaultAccessPermission ACL. What makes this error especially difficult to diagnose is that if connection points are involved, the failure typically doesn't occur when the server attempts its first callback; it occurs when the client passes its interface pointer to the server using IConnectionPoint::Advise. Most implementations of Advise, including ATL's, call QueryInterface on the client. But if the server process lacks access permissions in the client process, QueryInterface will fail. When Advise sees QueryInterface fail, Advise will fail, too. The moral: If you're using connection points to facilitate callbacks from remote servers and IConnectionPoint::Advise returns E_ACCESSDENIED or E_OUTOFMEMORY, check the access permissions on the client. Chances are the security principal whose identity the server process has been assigned does not have permission to call into the client process. |
Next Month...Identity and Authentication
Learning about activation security and access security is a good first step on the road to understanding COM security. But there's more-much more, as a matter of fact. In my next column , I'll talk about two more aspects of the COM security model: remote server process identity and authentication. If you found this month's discussion interesting, I think you'll find next month's to be even more so. Stay tuned...
COM Security Primer, Part II
In part 2, I'll continue last month's discussion by explaining two more fundamental tenets of COM security-namely, remote server process identity and authentication.
Identity
When a person logs onto a machine running Windows NT or Windows 2000, that person is authenticated against a database of valid user names and passwords. If the logon is approved, a new logon session is created and an access token is issued that identifies the person who performed the logon and the groups that he or she belongs to. Processes subsequently started by that user are tagged with the access token. Before NT or 2000 permits a process to access a system resource such as a file, it uses the access token and information encoded in the resource's security descriptor, if present, to determine whether or not to allow the access to occur. The operating system fails attempts to access the resource if the process performing the access lacks the necessary permissions. For example, if Bob is forbidden to read a certain file, then any process tagged with Bob's access token will utterly fail in its efforts to read that file.Clearly, the fact that every running process is tagged with an access token is an essential element of operating system security. But when a remote server process is launched by a COM client, a fundamental question arises: whose identity should the server process be assigned? On the surface, it might seem to make sense to assign the process the identity of the launching user-in other words, that of the client process that lodged the activation request. And while that is indeed one of the options available to you, it turns out to be the worst option for most distributed applications. I'll explain why momentarily.
- Interactive user
- Launching user
- A specific user ("this user")
If you choose interactive user, you're commanding COM to assign the server process the identity of the the person logged in at the server console-the "interactive user"-when a launch occurs. If Alice is logged in when an activation request arrives from another machine, then the server process is assigned Alice's identity. It's as if the process had been launched by Alice herself, and it doesn't matter who lodged the remote activation request as long as that person has launch permission. Interactive user is a good choice for testing and debugging a distributed application because 1) it's the only option that lets you start the server process in a debugger, and 2) it's the only option that permits a remote server process to display message boxes and other GUI application elements on the screen. The drawback to interactive user is that the process can't be launched if there's no one logged in on the server-that is, if there is no interactive user. Therefore, interactive user is a poor choice for server identity unless you can ensure that someone is logged in on the server, and that the logged-on user has sufficient security permissions to do whatever it is the COM server is designed to do.
The second option is launching user. This option is sometimes referred to in COM literature as the "as activator" option. It also happens to be the default, which is unfortunate because launching user has crippled more than its fair share of distributed applications. Launching user simply tells COM to assign the server process the identity of the remote client process that did the launching. If Bob runs a COM client on machine A and that client launches a server process on machine B, then the server process is assigned Bob's identity. The good news is that this enables the process to launch even if no interactive user is logged in. The bad news is that every new security principal (Bob, Alice, and so on) who launches a server process this way will cause a unique instance of the server process to be launched, and in a unique winstation, no less. Among other things, this makes it impossible for two different clients to use CoCreateInstance[Ex] to connect to a singleton object in a remote server process unless both clients are logged in under the same account. Unless you truly do want COM to satisfy activation requests with different server processes, stay away from launching user.
The proper way to deploy most remote COM servers is to set up a special user account for each server to run under and to designate that account with the "this user" option. If a COM server is configured to run as Mister Object, and if Mister Object is a valid account on the server, then when launched, the remote server process is assigned the identity of Mister Object. It matters not who's logged in on the server (or if anyone is logged in at all) or who launched the process-all that matters is that Mister Object is a valid account on the machine that hosts the COM server. By assigning the remote server process a fixed and known identity, you can control the process's rights and priveleges by exercising normal administrative control over the account. That's a win for everyone involved.
Remote server process identity is normally assigned by running DCOMCNFG on the server (see Figure 1). Choosing interactive user writes a RunAs entry to the server's registry-based AppID and assigns that entry the string "Interactive User," as shown in Figure 2. If you choose this user instead, the account name is written to the RunAs entry in place of "Interactive User." If you choose launching user in DCOMCNFG, no RunAs entry is created because launching user is the default. In other words, the absence of a RunAs entry means the server has been assigned the identity of the launching user. In most cases, that's bad.
Figure 1: Using DCOMCNFG to assign an identity to a COM server
Figure 2: RunAs entry added to the registry by DCOMCNFG
Assigning an identity to a remote server process is easy with DCOMCNFG. But what if you want to write an installation program to do it for you? You can use the Reg functions in the Win32 API to write a new RunAs entry to the registry, but that's only half the job. You also have to tell COM what the account's password is. For security reasons, the password isn't stored in the registry; it's stored in a private location managed by the Local Security Authority, or LSA. Registering the password that goes with a RunAs account is accomplished with the LsaStorePrivateData API function. For sample code demonstrating how, see the DCOMPERM sample in the Windows 2000 Platform SDK. Pay particular attention to the function named SetRunAsPassword, because it's that function that registers a password with the LSA.
Another issue to be aware of when writing custom installation programs for DCOM servers has to do with logon rights. An account used to provide an identity for a remote COM server-that is, an account referenced by a RunAs entry in the registry-must have the right to log on as a batch job. DCOMCNFG enables this right automatically when you use its "this user" option, but if you write a setup program that registers a RunAs account, you must enable this right programmatically. The same DCOMPERM sample that demonstrates how to register a RunAs password also shows how to enable an account's "logon as a batch job" right. See the DCOMPERM function named SetAccountRights for details.
Power COM Programming Tip |
Not too long ago, a gentleman at a large computer company called me requesting help diagnosing a problem he was experiencing with a DCOM application. His app called for multiple clients, all running on different machines, to use CoCreateInstanceEx to connect to a singleton object on another machine. In testing, QA personnel found that the clients were able to connect just fine in one test scenario, but using a slightly different test script, each call to CoCreateInstanceEx caused a new server process to be launched, effectively negating the object's singleton behavior. Paradoxically, the code was identical in both cases. My friend's question to me: what could possibly cause this to happen? The problem, as it turned out, was simple. Because the remote COM server had not been assigned an identity, it was defaulting to launching user. In the test scenario that worked, all test personnel were logged in under the same account-a special account established just for QA employees. In the scenario that didn't work, the testers were logged in using their normal domain user accounts. Get the picture? If ten users log in as Bob and attempt to launch a remote server process configured to run as the launching user, then the system will happily allow them to connect to one server process. But if Bob logs in as Bob and Alice logs in as Alice and each calls CoCreateInstance[Ex], COM will launch two server processes-even if the object they're attempting to connect to is a singleton. The moral of this story? Beware the launching user-especially if you want multiple clients out on the network to be able to "launch" into the same server process. |
Authentication
Authentication is the answer to the question "How sure do I want to be that my callers are who they say there are? And how secure should my data be when COM method calls pass over the wire?" COM answers these questions by assigning an authentication level to all calls that travel between machines. You decide what the authentication level should be. Your choices, in order of ascending security, are listed below: Authentication Level | Meaning |
None | Do not authenticate callers |
Connect | Authenticate callers on their first call into the server process |
Call | Authenticate callers on every call into the server process |
Packet | Authenticate callers on every packet of every method call |
Packet Integity | All of the above, plus verify that calls havent been altered en route |
Packet Privacy | All of the above, plus encrypt all data passed over the wire |
You choose an authentication level based on the level of security you desire. Authentication=None means you're not concerned about security and want to allow anonymous access to your server. (If you go this route, remember to grant launch and access permissions to Everyone; otherwise, no one will be able to use your COM server.) Authentication=Packet is a good middle ground if you want to prevent "spoofing"-callers gaining access to your server by posing as someone else. If you intend to transmit sensitive information such as credit card numbers and passwords in your method calls, then you might opt for Packet Privacy, which fully encrypts every bit of every packet comprising a COM method call. Packet Privacy provides the highest degree of security, but it's bad for performance. Don't use it unless you really need it.
Before you choose an authentication level, be aware that a server can't authenticate callers if the callers' credentials aren't valid on the server's machine. For example, if you specify that Bob has access permission to your COM server and pick an authentication level equal to, say, Connect, then Bob will only be able to call into the server process if the user name and password that Bob logged in with are valid on the server, too. The easiest way to achieve this is to have both machines be members of the same domain. If Bob is a valid account in the domain, and if the client that Bob's logged in to and the server he's making calls to belong to that domain, then you can use any authentication level you desire.But if for any reason Bob can't be authenticated on the server-if, for example, the two PCs belong to different domains that don't enjoy a trust relationship, or if one or both of them don't belong to a domain-then the only good way to authenticate calls from Bob is to set up identical Bob accounts (same user name, same password) on both machines. (例如:两台机器属于同一个工作组)That's a pain from an administrative standpoint, but it can be done.
So how do you set the authentication level? One way to do it is to use DCOMCNFG. You can pick a default authentication level for processes on the host machine from the Default Authentication Level list on DCOMCNFG's Default Properties page (Figure 3). Doing so writes the authentication level to the registry at HKEY_LOCAL_MACHINE\Software\Microsoft\Ole\LegacyAuthenticationLevel. In Windows 2000 and in NT 4.0 Service Pack 4 and higher, you can assign authentication levels to individual COM servers (Figure 4). Server-specific authentication levels, if used, override machine-wide default authentication levels, and are written to the registry under the COM server's AppID.
Figure 3: Setting a machine's default authentication level
Figure 4: Setting an individual COM server's authentication level
The second way to set the authentication level is to call CoInitializeSecurity. Passing an authentication value to CoInitializeSecurity sets the authentication level for the calling process and overrides authentication levels applied with DCOMCNFG. If desired, the authentication level can even be specified on a per-interface pointer basis using COM's IClientSecurity interface. That, however, is a topic for another day.
One point to be aware of concerning authentication levels is that you should set it at both ends of the wire-that is, on both the client and server PCs. When a client acquires an interface pointer from a remote COM server, COM examines the authentication levels in the two processes and encodes the higher of the two in the channel object that pairs the interface proxy and stub. The practical effect of this is that setting the authentication level to, say, Connect on the server doesn't necessarily mean that calls will be transmitted with an authentication level equal to Connect. If the client process has a higher authentication level-say, Packet-then calls will be performed with a Packet level of authentication. If you really want the server's authentication level to drive the authentication level of incoming calls, set the client's authentication level to None. Then, and only then, can you rest assured that the server's authentication level will be the one that's used.
Putting It All Together
Needless to say, there's much more that could be said about COM security. But hopefully these basics are enough to get you started and to help you overcome the security-related problems that bedevil many COM developers. Now, for example, if you encounter E_ACCESSDENIED errors attempting remote activations, you'll know to check the launch permissions on the server. If those are set correctly, then the next place you'd look is at the remote server process identity. If the server is configured to run as the interactive user but there is no interactive user, then bingo!-you've found the problem. To take this example a step further, suppose launch permissions are set to allow Bob to launch, that it's Bob who's attempting the launch, that Bob is a valid account on the server, that the server is set to run as launching user, and still you get E_ACCESSDENIED errors. The likely culprit? Check that the authentication level isn't equal to None. A server process can only be launched under the identity of the launching user if COM knows who the launching user is. But with authentication disabled, COM can't identify the launching user.If you'd like to learn more about COM security, I highly recommend that you read Keith Brown's book Programming Windows Security. Chapter 9 especially contains valuable information that every COM developer should know, and by itself can justify the book's cost.