FAQ: How do I start a program as the desktop user from an elevated app?

Common Vista/Win7 scenario:  the app you’ve written runs with elevated permissions, but then needs to start another program as the non-elevated desktop user.  For example, you want to display web content.  Now, you could just launch the web browser from your app, and let the web browser run as admin.  What could go wrong?  (Hint:  the correct answer will include the word “catastrophic”)

A very common mistake that programmers make is to grab a copy of the elevated, High Integrity Level access token from the current process and then “dumb it down”.  I.e., disable powerful group memberships, remove powerful privileges, and change the integrity level to Medium.  They then launch the new process with that “dumbed down” token.  This breaks for a number of reasons.

The new “LUA bug” of Vista/Win7

First and foremost, that approach makes the invalid assumption that the elevated app is running under the same user identity as the desktop user who originally logged on.  This is the new “LUA bug” of Vista and Win7.  (Refresher:  “LUA” = “limited user account”; “LUA bug” = failure that occurs when running as LUA and not administrator.  #1 cause of LUA bugs:  assumption that the end user will be an administrator.)  In Vista/Win7, everything runs as “LUA” by default, unless you specifically allow something to run elevated.  If you’re a member of the Administrators group, by default this involves a simple “consent” prompt.  The resulting app still runs as you, but with full admin rights.  If you’re not a member of Administrators, the elevation prompt requires the credentials of another account that is a member of Administrators.  The resulting app then runs as a different user.  A number of apps fail to take this second scenario into consideration.  “Dumbing down” the current process token is one example of that kind of failure.  The new program runs with reduced permissions, but not as the intended user.

There are at least a couple of other failures in that approach too, that are more obscure.  Let’s say you are a member of Administrators.  When you log on, the Windows LSA (Local Security Authority) generates two access tokens in two separate LSA-managed logon sessions.  One is the fully privileged, full-admin token; the other is the standard-user version, which is marked as linked to the full-admin token.  When you create a “dumbed-down” copy of the elevated one, the new token is still associated with the elevated session, and marked as being the “high half” of a split token.  As a result, if you start Internet Explorer with that token, Protected Mode will be disabled.  Also, if your “dumbed-down” process tries to launch an elevated app, it will simply launch the new process with the “dumbed-down” token, since it’s already marked as “elevated.”

“Enough nerditude.  Tell me what I need to do.”

So here’s one sequence that works well:

  1. Enable the SeIncreaseQuotaPrivilege in your current token (sample)
  2. Get an HWND representing the desktop shell (GetShellWindow)
  3. Get the Process ID (PID) of the process associated with that window (GetWindowThreadProcessId)
  4. Open that process (OpenProcess)
  5. Get the access token from that process (OpenProcessToken)
  6. Make a primary token with that token (DuplicateTokenEx)
  7. Start the new process with that primary token (CreateProcessWithTokenW)

I’ve attached an example C++ project, built with VS2008 and the MFC AppWizard, and tested with x86 and x64 builds.  The meat of the sample is in RunAsDesktopUser_Implementation.cpp.  I’m sure it can be done in managed code, but that will be someone else’s project, not mine.


Please note that there are a bunch of caveats about this approach:

  • This runs the new program in the same context as the desktop shell.  If the desktop shell process is not running (crashed or intentionally terminated), GetShellWindow fails, and there is no process token to do anything with.  Also, GetShellWindow fails if the default shell (Explorer) has been replaced with a custom shell.
  • If you have terminated the desktop shell and restarted it elevated (strongly discouraged), then the new process will also run elevated – as will pretty much everything else you start.
  • This code assumes that it is running already elevated.  If you’re not running elevated, then there is no need for this code.  If you’re not running as admin, then the necessary step of enabling SeIncreaseQuotaPrivilege won’t work anyway.
  • CreateProcessWithTokenW requires Vista or newer.  So:  this approach won’t work on pre-Vista (e.g., XP with runas); and if you want to incorporate this code in a program that can run on XP/2003, you need to use LoadLibrary/GetProcAddress to get the CreateProcessWithTokenW entry point.