Home > Windows Development > Creating and Using Managed Elevated COM objects

Creating and Using Managed Elevated COM objects

Why elevated COM you might ask.  Answer, because elevated COM is the ‘best’ way to allow an application that does not normally need administrator privileges to gain them for the unusual task.  Why managed you ask.  Because managed code in C# or VB allows the developer to deploy only one application to both x64 and x86 platforms.  As soon as you touch C++ you must compile the code for the specific mode you wish to run.  Further driving my quest for the use of managed code was the fact that I arrived here with minimal experience with COM.  In fact, I had never written any COM objects and did not relish the thought of learning ATL, or worse, at this point in my career.  Armed with the ‘knowledge’ that managed COM was easy, I set out on the development path.

There are several articles available that discuss some part of creating or access a managed elevated COM object.  I could not find one that filled in all the pieces.  I found Phil Wilson’s article, Build and Deploy a .NET COM Assembly, to be particularly helpful the uninitiated.  Christoph Wille has published three articles on the subject of managed elevated COM objects.  The last one, UAC Elevation in Managed Code: Guidance for Implementing COM Elevation, is the best resource and provides a good framework and it was my starting point.

I ran into several difficulties along my way to actually being able to access a managed COM object.  Perhaps the most insidious is the 64 bit versus 32 bit issue.  A COM object, managed or unmanaged, must be registered as 32 bit or 64 bit according to how it is going to be used.  The reality of this statement is that the object must be registered as both 32 bit and 64 bit when used on a 64 bit system.  Managed COM can be handled fairly easily by running both versions of REGASM.  (A 64 bit system has a second copy of REGASM in the Framework64 directory tree under the Microsoft.NET directory.)  Since either of these versions can be run from a 32 bit or 64 bit (elevated) command prompt, I created two bat files, REGASM32 and REGASM64, that call the correct REGASM and then I created a REGASM bat file that invokes both of these bat files.  That way I can conveniently invoke either or both versions of REGASM as needed.  (Check out the post at http://differentpla.net/content/2008/10/things-i-learnt-week-regsvr32exe-windows-x64 for information about registering 32 and 64 bit unmanaged COM objects.)  The other registration issue that I ran into is that the TYPELIB must be registered or activation will fail.  That may be written down somewhere or COM gurus may know this, but it was news to me.  The way that I have discovered that works and is easily done is to simply add the /tlb option to REGASM.  The third registration issue has to do with where the DLL actually is.  There are some people that believe that the DLL must be in the GAC to be registered.  That is not true.  If you add the /codebase option to REGASM, it will add the path to the registry and the COM object can be activated from any other path on the system.

In his third article Christoph discusses automatically adding the elevation information to the registry as the object is registered by using the ComRegFunction and ComUnregisterFunction attributes.  Well, again I ran into issues.  After much trial and error, I discovered that these attributes must be fully specified as [ComRegisterFunctionAttribute()] and [ComUnegisterFunctionAttribute()].  If you leave of the () or the word Attribute, you will not get a compile error, but the methods will not be called.  As a note, all the examples I found showed these two static functions to be public.  In reality, it is best practice that they be private.  Following’s Christoph’s lead I have reduced these two functions to a minimal and produced a helper class that does all the actual work.

[ComRegisterFunctionAttribute()]
private static void ComRegister (Type typeToRegister)
{
  ElevatedComRegistration.Register(typeToRegister.Assembly.Location,
    ClassGuid, AppIdGuid, 100);
}

[ComUnregisterFunctionAttribute()]
private static void ComUnregister (Type typeToUnRegister)
{
  ElevatedComRegistration.UnRegister(typeToUnRegister.Assembly.Location,
    ClassGuid, AppIdGuid);
}

ClassGuid and AppIdGuid are constants in the class to be registered.  Again I followed Christoph’s example, but made a few cosmetic changes.

#region Interface ISCUtility
[Guid(SCUtility.InterfaceGuid),
InterfaceType(ComInterfaceType.InterfaceIsDual),
ComVisible(true)]
public interface ISCUtility: IDisposable
{
  // Add COM accessible methods and properties here
}
#endregion

#region Class SCUtility
[GuidAttribute(SCUtility.ClassGuid),
ClassInterface(ClassInterfaceType.None),
ComVisible(true)]
public class SCUtility: ISCUtility, IDisposable
{
  public const string    ClassGuid = “DE177E01-C449-4987-A1E0-C98567EB3047”;
  public const string    InterfaceGuid = “5C10AA8E-4A8F-464f-8882-2B95DA4A993E”;
  private const string   AppIdGuid = “AD1C0896-F21B-42be-80E3-A31D0F031CEE”;

}

As a note, by adding the ComVisible(true) to the interface and class definitions, you do not have to set ComVisible(true) for the entire assembly.  Of course, it should go without saying that you should not reuse my GUIDs in your projects.

A note.  As you read further it may appear that I am disparaging Christoph’s code.  That is far from my intent.  Without his groundbreaking work, I would not have completed this project and I would have turned in another direction.  I hope that I have simply built upon a predecessor’s work in a constructive manner.

The code in the helper functions is similar to Christoph’s but with some simplification and additions.  In particular, his UnRegister function does not clean up enough information and leaves registry entries for items that have been unregistered.

public static void Register (string assemblyLocation, string classToElevate,
  string appId, int localizedStringId)
{
  RegistryKey    class_key;
  RegistryKey    elevation_key;
  RegistryKey    hkcr_app_id;
  RegistryKey    key;

/*
    Need a good check here for a system that requires UAC elevation
*/

  class_key = Registry.ClassesRoot.OpenSubKey(@”CLSID\{” + classToElevate + “}”, true);
/*
*    Add the appid and the prompt string to the class’s entry
* */
  class_key.SetValue(“AppId”, “{” + appId + “}”, RegistryValueKind.String);
  class_key.SetValue(“LocalizedString”, “@” + assemblyLocation + “,-” +
    localizedStringId.ToString(),RegistryValueKind.String);
/*
*    Enable elevation
* */
  elevation_key = class_key.CreateSubKey(“Elevation”);
  elevation_key.SetValue(“Enabled”, 1, RegistryValueKind.DWord);
  elevation_key.Close();
  class_key.Close();

  hkcr_app_id = Registry.ClassesRoot.OpenSubKey(“AppID”, true);
/*
*    Add the AppId key and tell it to use DLLSurrogate
* */
  key = hkcr_app_id.CreateSubKey(“{” + appId + “}”);
  key.SetValue(null, Path.GetFileNameWithoutExtension(assemblyLocation));
  key.SetValue(“DllSurrogate”, “”, RegistryValueKind.String);
  key.Close();
/*
*    Add the AppId guid
* */
  key = hkcr_app_id.CreateSubKey(Path.GetFileName(assemblyLocation));
  key.SetValue(“AppID”, “{” + appId + “}”, RegistryValueKind.String);
  key.Close();

  hkcr_app_id.Close();
}

public static void UnRegister (string assemblyLocation, string classToElevate, string appId)
{
  RegistryKey    class_key;
  RegistryKey    hkcr_app_id;

  class_key = Registry.ClassesRoot.OpenSubKey(@”CLSID\{” + classToElevate + “}”, true);
  if (class_key != null)
  {
    try
    {
      class_key.DeleteSubKeyTree(“Elevation”);
    }
    catch
    {
    }
    class_key.DeleteValue(“AppId”,false);
    class_key.DeleteValue(“LocalizedString”,false);
  }
  hkcr_app_id = Registry.ClassesRoot.OpenSubKey(“AppID”, true);
  if (hkcr_app_id != null)
  {
    hkcr_app_id.DeleteSubKey(“{” + appId + “}”,false);
    hkcr_app_id.DeleteSubKey(Path.GetFileName(assemblyLocation),false);
  }
  hkcr_app_id.Close();
}

One last item of difficulty was the inclusion of the resource string required for registration.  This string is displayed to the user in the UAC prompt.  I found a method that works, but I would not propose it a seriously good method.  Maybe someone can give me a better method.

I took a .RC file from a CPP+ project and removed everything but four lines.   The result was

STRINGTABLE
BEGIN
    100 “Put the prompt message here
END

The 100 is the 100 that is last parameter in the call to ElevatedComRegistration.Register.  I then added a Post Build event of rc $(ProjectDir)app.rc and then set the resource file on the Application tab to the full path of the resulting RES file.  Of course this does not handle alternate languages correctly, but it is a start.

Advertisements
Categories: Windows Development Tags: , ,
  1. Sunit
    2009/09/22 at 20:25

    Nice and informative. Thanks for taking time to write this.

  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: