How to start a GUI process from service, under Windows Vista/7
Today I had a interesting challenge in front of me. I had to start a GUI application under Windows Vista or 7 with elevated privileges. And I had to start it from a restricted account. Why? Because the process was meant to start from a USB drive and act as a data synchronization tool. So in order to do some more complicated things, it has to have enough privileges. But it has to be started from a restricted user account, as this will be the account that will synchronize the data via the USB.
After some debating, the decision was, to use a service that will start the GUI application. It goes like this:
- The USB is inserted.
- The GUI application is auto-started, or the service detects that the USB was inserted.
- The GUI application informs the service via IPC that it needs to be elevated and exits immediately.
- The service (running under SYSTEM account) starts the process with SYSTEM account token.
- The GUI application detects that is was started by the service.
- The GUI application has all possible privileges and does its job.
Let me say here, that I am aware of the security risks involved in such procedure. But with proper approach this can be made safe. OK the theory is looking fine. And it works with no problems under Windows 2000/XP. But under Vista/7 it is a different story altogether. Here is the Microsoft paper on the topic:
http://msdn.microsoft.com/en-us/library/ms683502%28VS.85%29.aspx
To make it short, the service running under SYSTEM account is running in an non-interactive window station. In other words, that means, that it cannot interact with the desktop. This was tightened in Windows Vista. You have to specify the “Interactive service” flag which is discouraged and can even be nullified in the registry (no interactive services). So starting a process from service, makes the process invisible because it is not running in the correct (logged on user) session.
What if we could just get a session for the logged on user, or even the user token and start the new process in the correct session. But wait we can do that, let me show you how. I found a good example at the code project:
http://www.codeproject.com/KB/vista-security/VistaSessions.aspx
I started there. I ported the code to Delphi, cleaned it and removed the parts that are not needed. Here is the result:
function LaunchAppIntoDifferentSession(const FileName, Params: string; const WaitFor: Boolean): Cardinal; var PI: PROCESS_INFORMATION; SI: STARTUPINFO; bResult: Boolean; LaucherApp: string; dwSessionId: DWORD; hUserTokenDup, hPToken: THANDLE; dwCreationFlags: DWORD; CommandLine: string; Directory: string; tp: TOKEN_PRIVILEGES; pEnv: Pointer; LD: LUID; begin try LaucherApp := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))) + cLauncherApp; dwCreationFlags := NORMAL_PRIORITY_CLASS or CREATE_NEW_CONSOLE; CommandLine := Format('%s "%s" "%s"', [LaucherApp, FileName, Params]); Directory := ExtractFilePath(LaucherApp); // get the current active session and the token dwSessionId := WtsGetActiveConsoleSessionID; //WTSQueryUserToken(dwSessionId, &hUserToken); // initialize startup info FillChar(SI, SizeOf(SI), #0); SI.cb := SizeOf(STARTUPINFO); SI.lpDesktop := PChar('winsta0\Default'); SI.dwFlags := STARTF_USESHOWWINDOW; SI.wShowWindow := SW_SHOWNORMAL; if OpenProcessToken(GetCurrentProcess, TOKEN_ADJUST_PRIVILEGES or TOKEN_QUERY or TOKEN_DUPLICATE or TOKEN_ASSIGN_PRIMARY or TOKEN_ADJUST_SESSIONID or TOKEN_READ or TOKEN_WRITE, &hPToken) then begin if LookupPrivilegeValue(nil, SE_DEBUG_NAME, LD) then begin tp.PrivilegeCount := 1; tp.Privileges[0].Luid := LD; tp.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED; DuplicateTokenEx(hPToken, MAXIMUM_ALLOWED, nil, SecurityIdentification, TokenPrimary, &hUserTokenDup); SetTokenInformation(hUserTokenDup, TokenSessionId, @dwSessionId, SizeOf(DWORD)); if CreateEnvironmentBlock(pEnv, hUserTokenDup, True) then dwCreationFlags := dwCreationFlags or CREATE_UNICODE_ENVIRONMENT else pEnv := nil; // Launch the process in the client's logon session. bResult := CreateProcessAsUser(hUserTokenDup, // client's access token nil, // file to execute PChar(CommandLine), // command line nil, // pointer to process SECURITY_ATTRIBUTES nil, // pointer to thread SECURITY_ATTRIBUTES False, // handles are not inheritable dwCreationFlags, // creation flags pEnv, // pointer to new environment block PChar(Directory), // name of current directory &si, // pointer to STARTUPINFO structure &pi); // receives information about new process if not bResult then begin Result := GetLastError; Exit; end; end else begin Result := GetLastError; Exit; end; end else begin Result := GetLastError; Exit; end; if WaitFor then begin WaitForSingleObject(PI.hProcess, INFINITE); GetExitCodeProcess(PI.hProcess, Result); end; finally // close all handles CloseHandle(hUserTokenDup); CloseHandle(PI.hProcess); CloseHandle(PI.hThread); CloseHandle(hPToken); end; end;
The code is still not properly cleaned up, especially the error conditions, but it gives you the idea how it is done. In short it does the following:
- It gets the session token for the currently logged on user with “WtsGetActiveConsoleSessionID“.
- It gets the user token of the current process, that is the token for the service under SYSTEM account.
- It duplicates that user token.
- It sets the session for the duplicated token to be the session of the logged on user.
- It launches the process with CreateProcessAsUser passing the duplicated, adjusted user token.
Quite simple, but really powerful. What we get is a process with SYSTEM account privileges, but running in the logged on user session. At first I could not get the new process to be shown on the top of the desktop. It was started, I could see it in the taskbar, but I had to manually click on it to restore it to the original (non minimized) state . This is probably the side effect of the “hack “, because of two different sessions. But I had an Idea. What if I first create a “launcher” process, just a simple pascal “program” that just gets some parameters and then starts the real GUI application. This way my process will be started by process that is already running in the correct session. It tried the approach and now it worked perfectly. I get a process with all the priveleges, but it behaves just as any other GUI process. The launcher process is very simple:
program GetMeUpLauncher; uses Windows, SysUtils, ShellAPI; begin ShellExecute(0, 'Open', PChar(ParamStr(1)), '/ADMIN', nil, SW_SHOWNORMAL); end.
The “ADMIN” parameter tells the GUI application that it was started by the service with elevated privileges. I just want to mention another interesting function which is commented in the above example. “WTSQueryUserToken” can get the user token of the currently logged on user. It is a really powerful function. It means we can get the token from a service for the current user and do things in his/her name. This only works if it is called from a process under SYSTEM account. As it is not needed in my example I commented it.
You can get the complete example with the service, launcher and demo client from my downloads page. But you will need my IPC and Jedi JWA in order to compile it. This is why I included precompiled binaries if you want to try it out.
EDIT:
One of the readers pointed out that it would be better, if the service was doing the actual synchronization. I agree with him. It is always better to use standard and supported techniques than to use hacks or undocumented features. Troubles are always around the next corner in such cases. However, as it usually happenes, we were under severe time constraint and had and already working application, that all of a sudden had to do some more. The easiest and fastest way was to do what we did. But in the future we will probably redesign it to a more standard solution, where the service will do the actual synchronization and the app will only act as the progress monitor.
The “hack” is still worth blogging about through
