Tuesday, 18 February 2014

Elevating just one part of an application

Elevation

In Windows Vista and up, even if you have administrator rights, applications you run can't perform actions that REQUIRE administrator rights unless the user is prompted with a UAC. The documentation refers to this as a 'split user access token' where the one you run with normally has restricted privileges but when you go to do something that needs administrator access it uses the full access token (after warning you).

I had to implement code in an MFC application for making changes to the registry that required administrator access. Our customers said they didn't want to run as an administrative user just to run the application (which is fair enough) so we modified the application to only require administrative rights when the GUI for editing the HKLM registry was invoked.

How it Works

Essentially you can invoke an out-of-process COM object and request that the object run with elevated rights. The COM object then displays a dialog for the user to edit the settings.

In my case most of the GUI to be displayed already existed so all I needed to do was to provide a wrapper for it.

In the dialog where the elevated GUI is invoked, the button that invokes the elevated dialog is initialized to display the familiar shield icon (so users know it requires elevation). 

Also, we need to be able to still run on older OSes. If we are not on Vista (or later) we check if the user has access to the HKLM registry. If we are on an older OS and the user doesn't have access we just disable the button.

if ( m_isVista )
{
    Button_SetElevationRequiredState(
        m_editWithElevationButton.m_hWnd,TRUE);
}
else
{
    if ( ! canWriteHKLM() )
    {
        m_editWithElevationButton.EnableWindow(false);
    }
}

MFC and ATL

Making an MFC COM object proved to be too hard. The easiest route was to make an ATL DLL that supported MFC. There is an article here that explains how to do this but in essence what was required was to create a WinApp instance and to register the MFC extension DLLs where the GUI was loaded from.

Invoking with Privilege

For a COM object to be elevated it must satisfy a number of requirements (documented in this link MSDN). The elevated GUI Wrapper COM component must:
  • Have a LocalizedString value under the HKEY_LOCAL_MACHINE/Software/CLSID/{XXX} registration point that refers to a resource where the display name of the components is stored.
  • Have an Elevation key with a DWORD Enabled=1 below the registration point.
  • Optionally have a IconReference under the Elevation key referring to an icon resource within a DLL that is to be displayed.
  • If Over-The-Shoulder elevation is required (where the administrator logs on with their credentials over the shoulder of the non-administrator user) then the DACL on the registry keys for the COM object must be updated (see MSDN reference above).
Also the COM object must run as an Out-of-process server (or local server). To enable this the DllSurrogate registry value must be present below both the COM registration location and below the registration location of the associated 'app ID'.

In addition it is desirable for the COM object to be signed. Otherwise the user will be warned that the application is not signed when they are prompted to elevate.

The registry requirements can be achieved by editing the .RGS file resource as follows which results in the registry being updated when the COM object is registered either by regsvr32 or by the installer:

HKCR
{
    NoRemove AppID
    {
        '%APPID%' = s 'ElevatedGUIWrapper'
        {
            val DllSurrogate = s ''
        }
         
        'ElevatedGUIWrapper.DLL'
        {
            val AppID = s '%APPID%'
        }
    }
}


And then the following registry is required for the COM class:

HKCR
{
    ElevatedGUIWrapper.ElevatedEditor.1 = s 'ElevatedEditor Class'
    {
        CLSID = s '{...}'
    }

    ElevatedGUIWrapper.ElevatedEditor = s 'ElevatedEditor Class'
    {
        CLSID = s '{...}'
        CurVer = s 'ElevatedGUIWrapper.ElevatedEditor.1'
    }

    NoRemove CLSID
    {
        ForceRemove {...} = s 'ElevatedEditor Class'
        {
            val AppID = s '%APPID%'
            val LocalizedString = s '@%MODULE%,-101'
            ProgID = s 'ElevatedWrapper.ElevatedEditor.1'
            VersionIndependentProgID = s 'ElevatedGUIWrapper.ElevatedEditor'
            ForceRemove 'Programmable'
            InprocServer32 = s '%MODULE%'
            {
                val ThreadingModel = s 'Apartment'
            }

            'TypeLib' = s '{...}'
            Elevation
            {
                val Enabled = d 1
            }
        }
    }
}

To invoke the COM object with administrator rights you need to do something like this:

HRESULT CoCreateInstanceAsAdmin(HWND hwnd, REFCLSID rclsid, REFIID riid, __out void ** ppv)
{
    BIND_OPTS3 bo;
    WCHAR  wszCLSID[50];
    WCHAR  wszMonikerName[300];
 
    StringFromGUID2(rclsid, wszCLSID, sizeof(wszCLSID)/sizeof(wszCLSID[0]));
    HRESULT hr = StringCchPrintf(wszMonikerName, sizeof(wszMonikerName)/sizeof(wszMonikerName[0]), L"Elevation:Administrator!new:%s", wszCLSID);
    if (FAILED(hr))
        return hr;
    memset(&bo, 0, sizeof(bo));
    bo.cbStruct = sizeof(bo);
    bo.hwnd = hwnd;
    bo.dwClassContext  = CLSCTX_LOCAL_SERVER;
    return CoGetObject(wszMonikerName, &bo, riid, ppv);
}

When elevating there are two scenarios depending on the rights of the logged on user. If the user is an administrator then elevating simply enables their administrative rights. If the logged on user is not an administrator then windows asks the user to logon using an administrator credential. In the second case the idea is the user asks an administrator to log on for them and watches over their shoulder while they perform the required action. This is the 'over-the-shoulder' or OTS scenario.

To enable OTS elevation the COM object's launch and activation permissions must be configured to permit a non-administrative user to launch/activate the COM object. The MSDN guide on creating elevated COM objects suggests the "LaunchPermission" registry value below the App registration should be set to a security descriptor built from the SDDL below:

O:BAG:BAD:(A;;0xb;;;WD)S:(ML;;NX;;;LW)

The 'AccessPermission" value should be set to the following SDDL:

O:BAG:BAD:(A;;0x3;;;IU)(A;;0x3;;;SY)

Making it look Modal

If the application simply instantiated the COM object and called the method to display the dialog, then while the method was blocked waiting for the COM object the application would be starved of updates and would not re-paint itself correctly. Ultimately the application would be treated by windows as a non-responsive and (after a few errors are displayed) would get terminated.

We want the elevated dialog to appear like a normal modal dialog to the user so we do a number of things:
  1. The implementation invokes a thread that instantiates the COM object (either elevated or just as a regular object) and calls the method to invoke the elevated GUI. This method will block the thread until the editor has completed.
  2. The Window handle of the main application window is passed to the COM object. The COM object opens the dialog modal to this parent window which has the effect of disabling parent window and implementing the usual blink behaviour if the user tries to interact with the parent window.
  3. After invoking the thread the process uses a peek message loop to process any windows message and wait for the thread to terminate. See code snippet below.
  4. If an exception is thrown as a result of an error interacting with the COM object, the thread saves the error as a member of the class and when the thread completes the main thread of execution throws the saved exception to the caller.
The peek message loop using MsgWaitForMultipleObjects looks like this (see below). This has the effect of processing any messages that occur in the parent application while otherwise blocking the application until the thread exits. The thread waits for the COM object and exits when the dialog is dismissed.

while(MsgWaitForMultipleObjects(1, &m_hThread, FALSE, INFINITE, QS_ALLINPUT) != WAIT_OBJECT_0 )
{
    AfxPumpMessage();
}