COM Security Primer, Part II

In my
previous column
, I began a 2-part discussion on COM security by introducing launch permissions and access permissions. COM uses launch permissions to decide whether to allow or disallow remote activation requests, and it uses access permissions to determine who’s allowed to make method calls to remote server processes. Failure to take launch and access permissions into account frequently leads to the E_ACCESSDENIED errors that COM developers so despise.

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.

When you configure a COM server to allow remote activation, one of the key choices you make is what identity to assign the server process when it’s launched at the behest of a remote client. In the general case-that is, assuming the COM server is a regular EXE and not an NT service-your options are threefold:

  • 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.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read