winapiuacshellexecutecreateprocessasuser

starting a UAC elevated process from a non-interactive service (win32/.net/powershell)


I'm using a thrid party Windows service that handles some automation tasks by running scripts and executables using CreateProcessAsUser(). I'm running into problems on Windows Server 2008 due to UAC and way the LUA elevation is handled through the APIs.

The service runs as LocalSystem and does not have "Interact With Desktop" enabled. The processes are being run as users in the Administrators group, but not the Administrator account (which is exempted from many UAC restrictions). All the UAC default settings in place.

I can pass arbitrary commands or powershell code to the service, but I can't seem to 'break out' of the non-elevated, non-interactive process that gets kicked off by the service.

The crux of the issue seems to be that the only (public) API option for starting an elevated process is ShellExecute() with the 'runas' verb, but as far as i can tell that can't be called from a non-interactive service or you get errors like "This operation requires an interactive window station".

The only workaround I've found is mentioned here: http://www.eggheadcafe.com/software/aspnet/29620442/how-to-proper-use-sendinp.aspx

In Vista, the official documented way to elevate a process is only using the shell API ShellExecute(Ex)(not CreateProcess or CreateProcessAsUser). So your application must call ShellExecute(Ex) to launch a helper elevated to call SendInput. Furthermore, due to Session 0 isolation, a service can only use CreateProcessAsUser or CreateProcessWithLogonW(can not use ShellExecute(Ex)) to specify the interactive desktop.

..I think there is no direct way to spawn an elevated process from a windows service. We can only first use CreateProcessAsUser or CreateProcessWithLogonW to spawn a non-elevated process into the user session(interactive desktop). Then in the non-elevated process, it may use ShellExecute(Ex) to spawn an elevated process for the real task.

To do this from .net/powershell code, it looks like I'd have to do some elaborate P/Invoke stuff to call CreateProcessAsUser or CreateProcessWithLogonW since the .Net System.Diagnostics.ProcessStartInfo doesn't have an equivalent of lpDesktop that I could set to "winsta0\default". And I'm not clear on if LocalSystem even has the rights to call CreateProcessAsUser or CreateProcessWithLogonW.

I also looked at http://blogs.msdn.com/alejacma/archive/2007/12/20/how-to-call-createprocesswithlogonw-createprocessasuser-in-net.aspx and Process.Start with different credentials with UAC on

Based on all that, I'm reaching the conclusion that there's no straightforward way to do this. Am I missing something? This really doesn't seem like it should be so hard. It feels like UAC was just never designed to handle non-interactive use cases.

And if any Microsoft people end up reading this, I noticed that the way ShellExecute internally handles elevation is by calling out to Application Information Service (AIS). Why isn't that same call to AIS available through some Win32 or .NET API? http://msdn.microsoft.com/en-us/library/bb756945.aspx

Sorry that ran a bit long. Thanks for any ideas.


Solution

  • The "official" way to break session zero isolation is to use a combination of the terminal services API and CreateProcessAsUser() to launch a process within a user's session. At my old job, we did just that, as we needed to display a dialog to the user from a service prior to installing a downloaded update So, I know it works, on WinXP, Win2K3, Vista, and Win7 at least, but I don't expect that Win 2K8 would be too different. Basically, the process goes as follows:

    1. Call WTSGetActiveConsoleSessionId() to get the active console session id (VERY important, as the interactive session is NOT always session 1, even on client systems). This API will also return a -1 if there is no active user logged into the interactive session (that is, logged in locally to the physical machine, as opposed to using RDP).
    2. Pass the session id from the previous API call to WTSQueryUserToken() to get an open token that reprents the user logged into the console.
    3. Call DuplicateTokenEx() to convert the impersonation token (from WTSQueryUserToken) into a primary token.
    4. Call CreateEnvironmentBlock() to create a new environment for the process (optional, but if you don't, the process won't have one).
    5. Pass the primary token from step #3 into a call to CreateProccessAsUser(), along with the command line for the executable. If you created an environment block from step #4, you must pass the CREATE_UNICODE_ENVIRONMENT flag as well (always). This may seem silly, but the API fails horribly if you don't (with ERROR_INVALID_PARAMTER).
    6. If you created an environment block, then you need to call DestroyEnvironmentBlock, otherwise you will generate a memory leak. The process is given a separate copy of the environment block when it launches, so you are only destroying local data.

    And voila! Windows does some internal magic, and you see the application launch. However, although this will launch and interactive process from a service, I am not sure if it will bypass UAC (but don't quote me on that). In other words, it may not launch as an elevated process unless the registry or internal manifest says to do so, and even then, you will might still get a UAC prompt. If the token you get from step #3 is a restricted token, you may be able to use AdjustTokenPrivileges() to restore the elevated (full) token, but don't quote me on that, either. However, as stated in the MSDN docs, please note that it is not possible to "add" privileges on a token that did not already have them (e.g. you can't turn a restricted user token into an administrator by using AdjustTokenPrivileges; the underlying user would have to be an admin to start with).

    It is technically possible to do all this from Win2K forward. However, it is really only feasible starting with WinXP, as Win2K lacks the WTSGetActiveConsoleSessionId() and WTSQueryUserToken() API's (along with WTSEnumerateProcesses() for Win2K Pro). You can hard code 0 as the session id (since that is always the case in Win2K), and I suppose you might be able to get the user token by enumerating the running processes and duplicating one of their tokens (it should be one that has the interactive SID present). Regardless, the CreateProcessAsUser() will behave the same way when passed an interactive user token, even if you don't select "Interact with the Desktop" from the service settings. It is also more secure than launching directly from the service anyway, as the process will not inherit the godly LocalSystem access token.

    Now, I don't know if your third party app does any of this when it runs the script/process, but if you want to do it from a service, that is how (and with Vista or Win7, it's the only way to overcome session 0 isolation).