What Every Software Engineer Must Know About Windows Vista


Introduction


User Account Control or UAC for short. You've seen the screenshots and maybe even tried it in action. The screen turns dark and you can only interact with a single dialog: it asks for your administrative consent to perform some sort of action. A few clicks later it happens again. What is going on in the background and why? What do you have to keep in mind when developing for Vista?

Microsoft is trying to address a pretty serious problem with UAC. Most people (including corporate users) are using Windows XP with an administrative account. This leads to clueless users opening suspect email attachments, which leads to malware being propagated very quickly with little resistance. The solution? Don't let users run software as administrators, except when they really have to.

UAC - The Concept


There are two main types of user accounts in Vista: "Limited" and "Administrator". There are two subtypes of administrators: "Filtered" and "Real". Finally, there are two subtypes of "Real" administrators: "Built-in" and "Domain". These three groups (Limited Users, Filtered Administrators and Real Administrators) are all handled differently in Vista. (Note that I am oversimplifying this. "Demi-Administrators" like Backup Operators are handled in much the same way as Filtered Administrators.)

  • Real Administrators don't have anything to worry about. If you buy a new PC with Vista pre-installed and enable the built-in Administrator account (blocked by default) then log in with this user, you will not be pestered with UAC prompts. You're a full-blown admin all the time. The same goes for domain administrators. If the PC you're logging in to is a member of a domain and you're using a domain administrator account you're running as a Real Administrator all the time. Note that both practices (using the built-in admin account or running as a domain administrator) are strongly discouraged for everyday use.

  • The Limited Users are at the other end of the spectrum and their life is pretty simple too. In fact, it got significantly better with UAC. Previously when a program was started by a lowly Limited User but needed administrative access, it got an ACCESS_DENIED error from Windows and that was the end of it. Now the system is smart enough to tell you ahead of time, before the acess denied error occurs, that this software will require administrative privileges. The Limited User will be able to enter the password for an administrative account, elevate the process in question to the administrator's level, and continue working. Note that this requires knowledge of an actual admin password - or a flesh and blood administrator at your beck and call.


    A Limited User attempting to change the time: Credentials Prompt
    (click to enlarge)

  • Finally, the controversial Filtered Administrators. These are the guys causing all the problems, running as Admin when they don't really have to - so they get special treatment. If you are an administrator on the local PC only (a member of the local Administrators group) then your account will be subjected to the antics of UAC. You will from time to time get prompted for confirmation, just like Limited Users do - but at least you will not have to type a password. You can just choose the affirmative answer and let everything continue under your own user account.


    A Filtered Administrator attempting to change the time: Consent Prompt
    (click to enlarge)

So the idea is to leave the big guys (Real Administrators) alone, put up roadblocks so the meddlesome Filtered Administrators don't spread a virus every time they try to look at a picture of a naked woman, and almost as a side-effect, do something really useful for the little guys, the Limited Users.

How It Works


So what's going on under the hood? Well, for Real Administrators, nothing special. They barely notice a thing. For Limited Users, it's really just a glorified version of the old RunAs command: the software hungry for elevated privileges will actually be run in the context of the administrator whose credentials were supplied. For Filtered Administrators, however, things are a quite different:

When a Filtered Administrator logs in, Winlogon prepares the full token with all the nice group memberships and default privileges, just like it did in Windows XP. The next thing it does though is somewhat unpleasant: it creates a copy of the token, strips out a set of privileges and sets the Administrators group SID to DENY mode - effectively rendering the user a non-administrator in the context of this token.

Winlogon then proceeds to launch the Windows shell with the filtered token therefore running it in the context of a non-administrator. Every program launched from the shell subsequently will run in a non-administrator context. The real administrator token is guarded strongly by UAC, and the only way to launch a process in the elevated context is to confirm an elevation dialog.

Well, that, and inheritance of course. Since child processes always inherit the parent's access token (unless the parent explicitly overrides this by, say, calling CreateProcessAsUser) the children of an elevated process will also be elevated. If you launch an elevated command prompt (right click the cmd.exe icon and select "Run as Administrator") you're free to start any other process from there, and those new processes will of course inherit the parent process' access token - which happens to come with full admin privileges.

Below is a screenshot illustrating the difference between an elevated token and a filtered token. The cmd.exe on the left was launched with "Run as Administrator", the one on the right with a plain old double-click.


Elevated and Filtered Tokens
(click to enlarge)

Elevation


How does Vista know when it needs to elevate a process? Well, it doesn't just elevate a process. Not a running one, that is. It needs to know about elevation before a process is started. Once a process has been assigned an access token and has been started, its fate has pretty much been sealed. It is either an elevated process or it is not.

You can elevate a process in two ways. One is to mark it for elevation in its manifest. If you choose to do this, Vista will always follow the rules set forth in the manifest. Below is a simple manifest - you're probably familiar with the part I've de-emphasized. The part in bold is new for Vista though - and Microsoft requires this <trustInfo/> section to be present in every application aspiring to the Designed for Windows Vista logo.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
        <dependency>
                <dependentAssembly>
                        <assemblyIdentity
                                type="win32"
                                name="Microsoft.Windows.Common-Controls"
                                version="6.0.0.0"
                                processorArchitecture="X86"
                                publicKeyToken="6595b64144ccf1df"
                                language="*"
                        />
                </dependentAssembly>
        </dependency>
        <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
                <security>
                        <requestedPrivileges>
                                <requestedExecutionLevel 
                                       level="asInvoker" 
                                       uiAccess="false"/>
                        </requestedPrivileges>
                </security>
        </trustInfo>
</assembly>

Note: You must be careful when you add this section. A "malformed" <trustInfo/> section will cause XP or Windows 2003 to bluescreen, as evidenced by this Microsoft KB document. The manifest in the above format is fine - it seems that the ms_asmv3:* prefixes shown in the KB article are the issue. Just be careful when using the VS2005 manifest tool. As far as I know nobody produced an honest-to-God "this is exactly what's bluescreening XP" explanation yet.

The real deal is the requestedExecutionLevel tag. If the "level" attribute is set to asInvoker, no automatic elevation will take place and the process will run with the same parent process' credentials. If requestedExecutionLevel is set to requireAdministrator, elevation will be required and Filtered Administrators will have to click through a consent dialog while Limited Users will receive an admin credentials prompt. Finally, if requestedExecutionLeve is set to highestAvailable, elevation will take place if it is possible to do so with the current user's credentials. If the user is an administrator (or has had his token "split", such as a Backup Operator would) the process will be elevated after a consent dialog is accepted by the user. No elevation dialog will appear if the user is a limited user and the process will run as if asInvoker had been specified.

If you do not know at compile-time whether or not your application will need to be elevated, or if your application can work when not elevated but can provide extra functionality when elevated, there is a way to request elevation from Windows programmatically. Not from the process that you want elevated of course - but a launcher can call ShellExecuteEx like this:

	SHELLEXECUTEINFO sei = {0};
	sei.cbSize = sizeof(sei);
	sei.hwnd = g_hWnd;
	sei.lpVerb = L"runas";
	sei.lpFile = L"MyApp.exe";
	sei.lpParameters = szParams;
	sei.lpDirectory = g_szPath;
	sei.nShow = SW_SHOWNORMAL;
	ShellExecuteEx(&sei);

The entirely non-intuitive "runas" verb will cause UAC to elevate the application that's about to be launched. ShellExecuteEx will not return until the UAC dialog is dismissed. "runas" is the equivalent of level="requireAdministrator" in that it will cause elevation both for an administrator and a limited user. There is no verb equivalent for level="highestAvailable". You can emulate the latter by examining your process' token and acting accordingly. If there is no "Administrators" group SID present then you're running as a limited user, so just omit the "runas" verb.

A word of warning: CreateProcess and its variants will always fail if a non-elevated application attempts to launch another application whose manifest requires elevation. GetLastError will return 740 (ERROR_ELEVATION_REQUIRED) in this case. When this is a possibility and you don't need the fine control over process creation that CreateProcess provides, use ShellExecute instead.

You can also elevate out-of-process COM objects. There have been talks of a CoCreateAsAdmin function but it never became a reality. You can, however, achieve elevation using a moniker. Microsoft recommeds something like the following:

HRESULT CreateElevatedComObject(HWND hwnd, 
                                REFCLSID rclsid, 
                                REFIID riid, 
                                __out void ** ppv)
{
    BIND_OPTS3 bo;
    WCHAR  wszCLSID[50];
    WCHAR  wszMonikerName[300];

    StringFromGUID2(rclsid, wszCLSID, cntof(wszCLSID)); 
    HRESULT hr = StringCchPrintf(wszMonikerName, 
                                 cntof(wszMonikerName)), 
                                 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);
}

//
// Where:
//

#define cntof(a) (sizeof(a)/sizeof(a[0]))


Command-line applications should generally be marked asInvoker. It is possible to elevate a command-line program (and Vista will do it happily) but these will always run in their own new window which is a bit of a usability issue.

GUI Considerations


You will see the shield icon sprinkled liberally throughout the Vista user interface. Buttons have it, context menus have it, even application icons have it overlaid on themselves automatically by the shell if the manifest requires elevation. You will want to follow suit of course. If pressing a button, clicking a hyperlink or choosing a menu item will cause UAC to prompt the user for elevation, you should add the shield icon to your UI.


Shields Abound
(click to enlarge)

Remember, Vista does not know when the result of a user action might require elevation, so it is your job to refactor your user interface and put code that requires elevation into either a different executable that you invoke with ShellExecuteEx, or a COM object that you invoke by calling CreateElevatedComObject.

The Vista engieers have made it fairly easy to add the shield icon to most UI elements:

  • Buttons

    This is very easy. A simple SendMessage call will do:

    SendMessage(GetDlgItem(hWnd, IDOK), BCM_SETSHIELD, 0, TRUE);
    

    Or you can use the new macro that essentially does the same thing:

    Button_SetElevationRequiredState(hwndButton, fRequired);
    

    Unfortunately you cannot mark a BUTTON element in a DIALOG resource to include the shield icon. You'll have to write code in your WM_INITDIALOG handler to take care of it.


  • Syslink / Hyperlink

    Layout an IDI_SHIELD icon next to the UI element in the resource editor.


  • Shell Context Menus

    DefCM (IContextMenu) has had support for icons for quite some time now. Use that.


  • Popup Menus

    I really wish for something as (almost) elegant as the Buttons scenario, but nope. You'll have to load the shield icon yourself:

    // SHSTOCKICONINFO and SHGetStockIconInfo are new to 
    // shell32.dll in Vista.
    
    SHSTOCKICONINFO sii = {0};
    sii.cbSize = sizeof(sii);
    SHGetStockIconInfo(SIID_SHIELD, SHGFI_ICON | SHGFI_SMALLICON, &sii);
    g_hShieldIcon = sii.hIcon;
    

    Then you'll have to assign it to a menu item:

    MENUITEMINFO mii = {0};
    mii.cbSize = sizeof(mii);
    mii.fMask = MIIM_BITMAP | MIIM_DATA;
    mii.hbmpItem = HBMMENU_CALLBACK;
    mii.dwItemData = (ULONG_PTR)g_hShieldIcon;
    SetMenuItemInfo(g_hMenu, ID_MY_COMMAND, FALSE, &mii);
    

    And finally, do the drawing too:

    case WM_MEASUREITEM:
        {
            LPMEASUREITEMSTRUCT pms = (LPMEASUREITEMSTRUCT)lParam;
            if (pms->CtlType == ODT_MENU) {
                pms->itemWidth  = 16;
                pms->itemHeight = 16;
                return TRUE;
            } 
        }
        break;
    
    case WM_DRAWITEM: 
        {
           LPDRAWITEMSTRUCT pds = (LPDRAWITEMSTRUCT)lParam;
           if (pds->CtlType == ODT_MENU) {
               DrawIconEx(pds->hDC, pds->rcItem.left - 15, 
                   pds->rcItem.top, 
                   (HICON)pds->itemData, 
                   16, 16, 0, NULL, DI_NORMAL);
               return TRUE;
           }
        }
        break; 
    

Other Considerations


Following are a few semi-random collection of insights and tips.

  • Interactive Services are gone for good. Services all run in Terminal Server session 0. The console is never attached to this session. Therefore you can never, ever display a GUI from a service anymore. The whole thing was sort of broken in XP and 2003 already, what with Remote Desktop and Fast User Switching steering the console session away from session 0 pretty quickly, this just made it official. Implement your GUI in a separate process and use COM or some other sort of IPC to talk to your service. If you just need a simple user notification, use WTSSendMessage.


  • Administrative Shares are hosed.

    net use \\vistabox\c$ /user:joe@vistabox
    

    This used to be a big timesaver and not much of a security risk - but if "joe" is a Filtered Administrator this will never work. Well, you can hack into the registry, like this:

    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\system]
    "LocalAccountTokenFilterPolicy"=dword:00000000
    
       0 - build filtered token (Remote UAC enabled - default)
       1 - build elevated token (Remote UAC disabled)
    

    This appears pretty ugly though. Apparently the issue is that "joe" would be a candidate for elevation, but there's no way to throw up an elevation prompt since this is a network logon, so the filtered token is used.


  • LogonUser can also surprise you when it is called on a Filtered Administrator account. If you use the LOGON32_LOGON_INTERACTIVE logon type the returned token will be filtered. Use LOGON32_LOGON_NETWORK_CLEARTEXT or something similar in this case.


  • Remote Registry is inaccessible by default. The "Remote Registry" service is set to "manual" start instead of "automatic". When you attempt to connect to the registry on a Vista computer (no matter what user account type you use) RegConnectRegistry will fail with error 53 (ERROR_BAD_NETPATH). You can always enable the service remotely as long as RPC is enabled (and it is, by default) so this is just an annoyance.


  • Discrepancies between Microsoft's documentation and the real world (according to Vista build 5728). There are quire a few of these, but the main one seems to be that while the docs claim that Domain Administrators would be treated as Real Administrators on a domain member computer, this is not the case. Only the two built-in Administrator accounts (the one in the local SAM and the one in the domain) are free from UAC prompts (or can access administrative shares), a simple membership in the Domain Administrators group won't change this.