# Tuesday, September 25, 2007
COM Elevation

The code at the bottom of this blog post provides a Function called CreateObjectElevated which is provided as a compliment to Visual FoxPro's CreateObject, CreateObjectEx, and NewObject functions. What it does that those other 3 functions can't do is provide for COM elevation. Basically COM elevation provides the ability for an application running under a limited user account (LUA) to instantiate a COM class (OLEPublic) that requires a higher level of permissions in order to do whatever it is that it does (depends on the component and what it was designed to do). The code in CreateObjectElevated is a little bit on the advanced side, but I think you'll agree that what it does is pretty simple - instantiates a class for us.

Simple Process Elevation

Allow me to digress for a moment, because I want to explain what COM Elevation is and is not. COM Elevation is instantiating a COM class, it is not starting a separate process as elevated. If you simply want to start another process (application) elevated, this can be facilitated by the ShellExecute API call as in the following code:

m.lnOwnerHWND = 0
m.lcCommand = "runas" && this does the trick!
m.lcTarget = "C:\Windows\Notepad.exe"
m.lcParams = ""
m.lcDirectory = ""
ShellExecute(m.lnOwnerHWND, m.lcCommand, m.lcTarget, m.lcParams, m.lcDirectory, SW_SHOWNORMAL)

The "runas" verb in the preceding code causes permission elevation to occur regardless of what the target's manifest may have in its trustInfo node. So, for starting another process as elevated, it's pretty simple and straight-forward.

COM Elevation Moniker

To facilitate the ability for processes running under LUA to instantiate and use COM classes that require elevated permissions Microsoft decided to use a moniker. If you want the nitty-gritty on all of this you can read what Microsoft has to say about it out on the MSDN. Needless to say, the approach while quite easy in VC++, leaves a bit to be desired for those of us that are trying to develop at times in something other than VC++. However, thankfully for us VFP contains a little known or used SYS function - SYS(3096). What this function does is take an IDispatch pointer and turns it into an object. So, since the CoGetObject API call has an out parameter that will return to us an IDispatch pointer, we are in business! We can now instantiate COM objects using Elevation:Administrator!new:{guid} or Elevation:Highest!new{guid}. For those of you that have been reading through this whole series, you will note that the name of those monikers look a lot like the two elevated values for the level attribute of the requestedExecutionLevel node (part of the application manifest).

Properly Registering COM Components for Elevation

It is important to note many key additions to the registration of COM components on Vista so that they can be instantiated in an elevated manner by CoGetObject. In addition to the usual registry keys that you will have after registering a COM server on Vista, the following additional keys and values are needed (clsid guid is the clsid for the COM component and AppId guid is simply a guid you can generate yourself). Later on we'll have to see about creating a class that will handle all of this additional registration for us... or we could override the DllRegister and DllUnregister functions in our VFP COM servers, or maybe even replace that portion of the VFP PE files completely with new code that would handle all of this, but those are topics and code for another day. For now pay special attention to the following registry entries because that absolutely need to be part of the registration of our COM servers in order for our VFP application to use the COM Elevation Moniker on them.

Add the following data values to the "HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{guid}" key (where path is to your com server and the -5 refers to a string id in the string table of the file - more on that string id in a moment, for now set it to -16. As for the AppId guid, just create a guid for that.
  •   Name:            LocalizedString
      Type:            REG_EXPAND_SZ
      Data:            @D:\MyComServer.dll,-16
  •   Name:            AppId
      Type:            REG_SZ
      Data:            {guid}

Add the following Elevation key containing an Enabled DWORD value to the "HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{guid}" key as well.
  • HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{guid}\Elevation
      Name:            Enabled
      Type:            REG_DWORD
      Data:            0x1
Add two new keys containing values to the "HKEY_LOCAL_MACHINE\SOFTWARE\Classes\AppID" key as follows
  •   Key: HKEY_LOCAL_MACHINE\SOFTWARE\Classes\AppID\MyComServer.dll
  •   Name:            AppId
      Type:            REG_SZ
      Data:            {same guid as you put up above for the AppId value in clsid}

  • Key:HKEY_LOCAL_MACHINE\SOFTWARE\Classes\AppID\{same guid as you put up above for the AppId value in clsid}
  • Name:            DllSurrogate
      Type:            REG_SZ
  • Name:            AuthenticationLevel
      Type:            REG_DWORD
      Data:            0x1

Sorting the Code

In the CreateObjectElevated function I have provided in the code at the end of this blog entry, you will note a number of defines, including IID_IDispatch which is the guid we use to tell CoGetObject what Interface within the COM component we want a pointer to. After some local variable declarations, a couple of API declarations, we come to the m.lcBindOpts3Struct which I am using to hold the BIND_OPTS3 structure that CoGetObject requires. The BIND_OPTS3 structure is derived from the BIND_OPTS2 structure and simply adds a HWND to it.

While there are a number of members to this structure, I am only setting two of them. The first one which holds the structure size (note that I am setting that to 34 which corresponds to the total bytes that the structure takes up), and the class context which is defined as follows:


The class context, taken from the CLSCTX enumeration, that is to be used for instantiating the object. Monikers typically pass this value to the dwClsContext parameter of CoCreateInstance."

I have set the class context using the CLSCTX_INPROC_SERVER constant. For those of you that are wholly unfamiliar with creating structs in VFP, just know that a struct is nothing more than a string of bytes holding binary information. So, even though Visual FoxPro doesn't support structs natively we can simply string together the binary data our struct would contain and send it to an API that requires it.

m.lcBindOpts3Struct = BINTOC(34,"4rs") && structure size
m.lcBindOpts3Struct = m.lcBindOpts3Struct + REPLICATE(CHR(0), 16)
m.lcBindOpts3Struct = m.lcBindOpts3Struct + BINTOC(CLSCTX_LOCAL_SERVER,"4rs")
m.lcBindOpts3Struct = m.lcBindOpts3Struct + REPLICATE(CHR(0), 10)

Since I'm allowing the developer to just send in the ProgId, as opposed to the Clsid, I next use the CLSIDFromProgIDEx function that internally uses the CLSIDFromProgID and StringFromGUID2 API calls to generate the guid for the moniker. I then concatenate that guid on to the end of the ADMINELEVATION constant to complete the moniker that is going to be used in our call to CoGetObject.

    m.lcClsID = CLSIDFromProgIDEx(m.tcProgID)
    m.lcMoniker = STRCONV(ADMINELEVATION + m.lcClsID + CHR(0),5)

Lastly, before call CoGetObject I instantiate a variable, named lnIDispatchPointer to hold the out parameter value that CoGetObject will generate upon a successful call to it. Then I make the actual call. Assuming that the call succeeded, the user will be prompted with a UAC dialog on Vista asking for permission to elevate and if they approve it then lnIDispatchPointer will have a pointer to the IDispatch of MyComServer.MyOlePublicClass. Then it is just a matter of call SYS(3096) in order to transform that Interface pointer into a useable object (an instance of MyOLEPublicClass) for our Visual FoxPro application.

A Note Regarding Strings in a String Table

Every Visual FoxPro application or COM server has a string table in it. It is just another resource that is included when Visual FoxPro compiles the application. By default VFP puts the following string table in when it compiles...

2,     "VFP9.EXE"
3,     "VFP9R.DLL"
4,     "VFP9T.DLL"
5,     "Microsoft Visual FoxPro"
6,     "Cannot locate the Microsoft Visual FoxPro support library."
7,     "The %s file is invalid or damaged."
8,     "Cannot run the file %s.\n\nError code 0xlX."

Now, looks are a little deceiving because the string table is zero-based and there are a bunch of empty strings (null) in this string  table. So, while you can't see them there is a string 0, 1, 9, 10, 11, 12, 13, 14, and 15 that aren't showing because they're null. That's why I said to put -16 into that registry key, because I am going to provide you with code that you can use to not only replace string that are already there, but add new strings to the string table as well. And yes, even though you are seeing the string IDs in the above table as positive numbers, they are referenced in the registry as negative numbers.

What the UAC uses that -16 resource string for is showing on the UAC approve or deny dialog box that Vista displays when the CoGetObject API call is made successfully. Here's a look at a similar UAC dialog in case you haven't seen it before...

Right where you are seeing the "Unidentified Publisher" is where our string from the string table will show, and the localized string is required so we need to set it to something. So, the next thing we need to do is figure out how to manipulate our compiled VFP COM servers so we can add or replace strings in the string table, so take a look back at the class I provided in Part 2 of this series and note the AddStringToStringTable method of the ModuleResourceEditor class. See it? A call to it looks like the following:

?m.loModuleResourceEditor.AddStringToStringTable("RegFreeDll Adder",-1)

What that does is manipulate the string table of any file that is in PE format, which would of course include Visual FoxPro EXEs and DLLs. If you want to append a string on the end of the string table then send in the string you want appended as parameter 1 and a -1 as the second parameter. So, that line of code above will append a string to the 0-15 strings that already exist in the VFP dll's string table and thus I will have a string id of 16 (-16) for the string, which is exactly what we put in the registry earlier. So, when the UAC dialog comes up asking for elevated permission approval the user will see "RegFreeDll Adder" as the string on the dialog.

If you want to replace any of the strings in a string table simply send in the replacement string as parameter one, and the string position within the string table of the string to be replaced as parameter 2. If you were to send in something like 101 as the second parameter for a default VFP EXE or DLL then the code would add 84 null strings to the string table and then the string specified as parameter one would become string id 101 (-101).

Enough for Now

My fingers are sore and I need more coffee, so this will have to do for now. We'll go through some more exceedingly cool stuff in Part 5... stay tuned.

COM Elevation Sample Code

LOCAL loElevatedInstance
m.loElevatedInstance = CreateObjectElevated("MyComServer.MyOLEPublicClass")

FUNCTION CreateObjectElevated(tcProgID, tnLevel)
    #DEFINE IID_IDispatch "{00020400-0000-0000-C000-000000000046}"
    #DEFINE ADMINELEVATION 'Elevation:Administrator!new:'
    #DEFINE HIGHESTELEVATION 'Elevation:Highest!new:'

    LOCAL lnIDispatchPointer, lcClsID, lnReturn, lcMoniker, lcBindOpts3Struct, lcIDispatch

    m.lcBindOpts3Struct = BINTOC(34,"4rs") && structure size
    m.lcBindOpts3Struct = m.lcBindOpts3Struct + REPLICATE(CHR(0), 16)
    m.lcBindOpts3Struct = m.lcBindOpts3Struct + BINTOC(CLSCTX_LOCAL_SERVER,"4rs")
    m.lcBindOpts3Struct = m.lcBindOpts3Struct + REPLICATE(CHR(0), 10)
    m.lcClsID = CLSIDFromProgIDEx(m.tcProgID)
    m.lcMoniker = STRCONV(ADMINELEVATION + m.lcClsID + CHR(0),5)
    CLSIDFromString(STRCONV(IID_IDispatch + CHR(0),5), @m.lcIDispatch)
    m.lnIDispatchPointer = 0

    m.lnReturn = CoGetObject(m.lcMoniker, @m.lcBindOpts3Struct, m.lcIDispatch, @m.lnIDispatchPointer)
    IF m.lnReturn = 0
        RETURN SYS(3096, INT(m.lnIDispatchPointer)) && int casts the pointer to what Sys(3096) expects
        RETURN GetLastErrorMessage(m.lnReturn)

FUNCTION GetLastErrorMessage(tnError)
    LOCAL lcBuffer
    DECLARE INTEGER FormatMessage IN kernel32.DLL ;
        INTEGER dwFlags, ;
        STRING @lpSource, ;
        INTEGER dwMessageId, ;
        INTEGER dwLanguageId, ;
        STRING @lpBuffer, ;
        INTEGER nSize, ;
        INTEGER Arguments
    m.lcBuffer = SPACE(128)
    =FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, 'WINERROR.H', m.tnError, 0, @m.lcBuffer, 128, 0)
    RETURN m.lcBuffer

    LOCAL lcGuid, lnResult, lnLen, lcReturn
    m.lcReturn = SPACE(82)
    m.lcGuid = SPACE(16)
    m.tcProgID = STRCONV(m.tcProgID + CHR(0), 5)
    m.lnResult = CLSIDFromProgID(m.tcProgID, @m.lcGuid)
    IF m.lnResult = 0
        DECLARE INTEGER StringFromGUID2 IN ole32.DLL STRING pGuid, STRING @ pString, INTEGER nMaxChars
        m.lnLen = StringFromGUID2(m.lcGuid, @m.lcReturn, 40) - 1
        m.lcReturn = STRCONV(m.lcReturn, 6)
        m.lcReturn = LEFT(m.lcReturn, m.lnLen)
    RETURN m.lcReturn

Tuesday, September 25, 2007 5:49:51 PM (GMT Daylight Time, UTC+01:00)  #    Comments [0]



<September 2007>