Another way to expose a COM/.NET DLL assembly as a secure DCOM service

In my last post I mentioned a simple way to expose a .NET class under a secure DCOM connection. That particular method works best if the class that you securely exposed through DCOM lives in your own EXE service (which is written in C# for example).

But, among the various feedback comments, there is an interesting question from John H. Bergman which adds a twist to the original problem:

>>> Can you tell me how you mark a .NET assembly DLL to run out of proc? I was hoping I could do something like running inside DLLHost, but I cannot seem to get it working.

Well, to be fair this is a different task since we have now a .NET DLL assembly as opposed to an EXE. First, we can't just expose this DLL as a separate DCOM process since DLLs must live in a certain process. Now, John already mentions in his email DLLHOST.EXE which seems a reasonable path. DLLHOST.EXE exposes a DCOM server as a regular process (which happens to run under whatever credential mechanism were specified through the AppID key). Still, this is not yet a Windows service. So what's the solution?

Surprisingly, starting with Windows XP (and continuing in Windows Server 2003) it is actually possible to run a Windows Service under DLLHOST.EXE hosting! 

For a quick start, open the COM+ graphical management interface (the old DCOMCNFG.EXE now revamped as a MMC snap-in). After registering your application, go to the Properties dialog and you will see in the Identity tab a new setting - run the application under the system account (which can be Local Service/Network Service or Local System).

But there is a problem with this approach: no one wants to play with the DCOMCNFG UI. When you have to manage automatically 300 servers, you would like a script instead of a UI to register your app. Fortunately, COM+ has a scriptable administrative API. Below, I presented a small script that would register any COM DLL under a separate process running as a service.

The script is very simple to use. First, if you run the script with no arguments, hopefully under cscript hosting, it will show the usage. Now, to register the DLL, just use the “-register“ option:

CScript.exe register_app.vbs -register “Your Application” YourApp.DLL “Description of your app“

Finally, to unregister the DLL, run the script with the “-unregister“ command.

CScript.exe register_app.vbs -unregister “Your Application“

Here is the script:

 '****************************************************************************** ' Registers/unregisters a COM/.NET DLL as a COM+ app running as a service '****************************************************************************** Option Explicit '****************************************************************************** ' Main Routine '****************************************************************************** Dim Args Set Args = Wscript.Arguments If Args.Count < 1 Then PrintsUsage End If Dim ApplicationName, ApplicationDLL, ApplicationDescription If Args.Item(0) = "-register" Then If Not Args.Count = 4 Then PrintsUsage ApplicationName = Args.Item(1) ApplicationDLL = Args.Item(2) ApplicationDescription = Args.Item(3) UninstallApplication InstallApplication Wscript.Quit 0 End If If Args.Item(0) = "-unregister" Then If Not Args.Count = 2 Then PrintsUsage ApplicationName = Args.Item(1) UninstallApplication Wscript.Quit 0 End If ' Wrong options? PrintsUsage Wscript.Quit 0 '****************************************************************************** ' Prints the usage '****************************************************************************** Sub PrintsUsage Wscript.Echo "" Wscript.Echo "Usage:" Wscript.Echo "" Wscript.Echo " 1) Registering a DLL as a Windows Service (as a COM+ Application):" Wscript.Echo " CScript.exe " & Wscript.ScriptName & " -register   " Wscript.Echo "" Wscript.Echo " 2) Unregistering a COM+ application:" Wscript.Echo " CScript.exe " & Wscript.ScriptName & " -unregister " Wscript.Echo "" Wscript.Quit 1 End Sub '****************************************************************************** ' Installs the Application '****************************************************************************** Sub InstallApplication Wscript.Echo "Creating a new COM+ application:" Wscript.Echo "- Creating the catalog object " Dim cat Set cat = CreateObject("COMAdmin.COMAdminCatalog") wscript.echo "- Get the Applications collection" Dim collApps Set collApps = cat.GetCollection("Applications") Wscript.Echo "- Populate..." collApps.Populate Wscript.Echo "- Add new application object" Dim app Set app = collApps.Add Wscript.Echo "- Set app name = " & ApplicationName & " " app.Value("Name") = ApplicationName Wscript.Echo "- Set app description = " & ApplicationDescription & " " app.Value("Description") = ApplicationDescription ' Only roles added below are allowed to call in. Wscript.Echo "- Set app access check = true " app.Value("ApplicationAccessChecksEnabled") = 1 ' Encrypting communication Wscript.Echo "- Set encrypted COM communication = true " app.Value("Authentication") = 6 ' Secure references Wscript.Echo "- Set secure references = true " app.Value("AuthenticationCapability") = 2 ' Do not allow impersonation Wscript.Echo "- Set impersonation = false " app.Value("ImpersonationLevel") = 2 Wscript.Echo "- Save changes..." collApps.SaveChanges wscript.echo "- Create Windows service running as Local System" cat.CreateServiceForApplication ApplicationName, ApplicationName , "SERVICE_AUTO_START", "SERVICE_ERROR_NORMAL", "", ".\localsystem", "", 0 wscript.echo "- Add the DLL component" cat.InstallComponent ApplicationName, ApplicationDLL , "", "" ' ' Add the new role for the Local SYSTEM account ' wscript.echo "Secure the COM+ application:" wscript.echo "- Get roles collection" Dim collRoles Set collRoles = collApps.GetCollection("Roles", app.Key) wscript.echo "- Populate..." collRoles.Populate wscript.echo "- Add new role" Dim role Set role = collRoles.Add wscript.echo "- Set name = Administrators " role.Value("Name") = "Administrators" wscript.echo "- Set description = Administrators group " role.Value("Description") = "Administrators group" wscript.echo "- Save changes ..." collRoles.SaveChanges ' ' Add users into role ' wscript.echo "Granting user permissions:" Dim collUsersInRole Set collUsersInRole = collRoles.GetCollection("UsersInRole", role.Key) wscript.echo "- Populate..." collUsersInRole.Populate wscript.echo "- Add new user" Dim user Set user = collUsersInRole.Add wscript.echo "- Set user name = .\Administrators" user.Value("User") = ".\Administrators" wscript.echo "- Add new user" Set user = collUsersInRole.Add wscript.echo "- Set user name = Local SYSTEM " user.Value("User") = "SYSTEM" wscript.echo "- Save changes..." collUsersInRole.SaveChanges Wscript.Echo "Done." End Sub '****************************************************************************** ' Uninstalls the Application '****************************************************************************** Sub UninstallApplication Wscript.Echo "Unregistering the existing application..." wscript.echo "- Create the catalog object" Dim cat Set cat = CreateObject("COMAdmin.COMAdminCatalog") wscript.echo "- Get the Applications collection" Dim collApps Set collApps = cat.GetCollection("Applications") wscript.echo "- Populate..." collApps.Populate wscript.echo "- Search for " & ApplicationName & " application..." Dim numApps numApps = collApps.Count Dim i For i = numApps - 1 To 0 Step -1 If collApps.Item(i).Value("Name") = ApplicationName Then collApps.Remove(i) WScript.echo "- Application " & ApplicationName & " removed!" End If Next wscript.echo "- Saving changes..." collApps.SaveChanges Wscript.Echo "Done." End Sub 

Let's go step by step on the COM+ registration API (if you are interested of course). First, we will create an instance of the COM Admin Catalog and add an application there. The Application in COM+ terms is the equivalent of an AppID registry key in classic COM terms. You can see below that I am using encrypted COM communication enforced between the client and the server process (which surprisingly doesn't seem to consume a lot of CPU). Also, I use some standard COM security features, like secure COM references, prevent impersonation for outgoing COM calls, etc. The numeric constants are the classic CoInitializeSecurity parameters that are documented on MSDN:

Set cat = CreateObject("COMAdmin.COMAdminCatalog")
Set collApps = cat.GetCollection("Applications")
collApps.Populate
Set app = collApps.Add
app.Value("Name") = ApplicationName
app.Value("Description") = ApplicationDescription
app.Value("ApplicationAccessChecksEnabled") = 1 ' Enforce COM authorization
app.Value("Authentication") = 6 ' RPC_C_AUTHN_LEVEL_PKT_PRIVACY
app.Value("AuthenticationCapability") = 2 ' EOAC_SECURE_REFS
app.Value("ImpersonationLevel") = 2              ' RPC_C_IMP_LEVEL_IDENTIFY
collApps.SaveChanges

Now I call a magic function on the catalog object that converts this app to a Windows Service. The parameters are self-explanatory:

cat.CreateServiceForApplication ApplicationName, ApplicationName , "SERVICE_AUTO_START", "SERVICE_ERROR_NORMAL", "", ".\localsystem", "", 0

Next, I add our DLL to the application:

cat.InstallComponent ApplicationName, ApplicationDLL , "", ""

And finally, I am configuring the COM+ roles for the newly created application. Just to bring you up to speed, a COM+ role is an abstracted identity that can be a set of users/groups. I will define one role containing the list of users that can call into the service (the other users will get E_ACCESSDENIED and won't even get hands on a class instance implemented in your DLL). This is what I would call a truly secure COM server :-)

Set collRoles = collApps.GetCollection("Roles", app.Key)
collRoles.Populate
Set role = collRoles.Add
role.Value("Name") = "Administrators"
role.Value("Description") = "Administrators group"
collRoles.SaveChanges
Set collUsersInRole = collRoles.GetCollection("UsersInRole", role.Key)
collUsersInRole.Populate
Set user = collUsersInRole.Add
user.Value("User") = ".\Administrators"
Set user = collUsersInRole.Add
user.Value("User") = "SYSTEM" collUsersInRole.SaveChanges

You can see that in the text above I used the predefined account “.\Administrators”. Small warning though - this account might not be named in this way on all systems. If you want a more general script, then you will have to retrieve the name of the generic Built-in administrators SID (S-1-5-32-544) using the Win32_Account WMI class, for example.

So, in the end we have a simple, self-contained VBS script that will register any COM DLL as a Windows service. Obviously, you can use the same script to register a .NET DLL assembly.