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
Bruce McGee wrote,
Interesting technical issue.
Since you already have a special purpose Widows service running on the PC under a SYSTEM account, would it make sense for this service to perform the synchronization? The GUI app can still be auto-started and communicate with the service to show the service’s progress through IPC.
It would require fewer hoops, is less susceptible to a “fix” in a Windows update and less likely that someone can rename any arbitrary application to the name of your GUI and have it execute as a SYSTEM user on any of these machines.
Just a thought.
Link | January 14th, 2010 at 12:10 am
Iztok Kacin wrote,
You have a good point and yes I thought of that. But I did not reveal all the details in the post. Let me explain. We have a number of laptops that are under our control. On them an application is running, that is collecting some data through user input. Our people go to the streets collectiong that data with laptops. When they want to send us the data they put in the USB (they have no connectivity on the streets) and when they come home, or to some computer with connectivity the send the data over HTTP to our server. Now if the do this at home they do it on their computer and there we have no control over that. They have no service installed there, so the GUI app must work alone. Then they receive the possible new data from the server that is written back to USB. When they go to work again, they put the USB to the laptop and the new data is synchronized.
Now when the data is transfered from USB to the laptop, it can contain software upgrades for instance. If we have a bug in our software we must upgrade it without calling our people from work. So this is why we need more rights that ususal, to install update.
So the idea is that data is encrypted and freely reachable for the user. Only data is synchronized from laptops to our server, but other things can be synchronized from our server to the laptops. I hope I made things a little clearer. Thanks for the tip anyway
And the communication can be made secure, so no other executable can fake to be ours.
Link | January 14th, 2010 at 7:38 am
Iztok Kacin wrote,
@Bruce
After thinking what you said, I must say I agree with you. Even with all I described in my reply, it can still be done cleaner. The app should do only the HTTP communication with the server and should call the service to do the actual synchronization. Then it should only act as a “monitor” to display progress and errors to the user via the IPC.
The truth is we were in a hurry and had so much to do before the project started, that this was the easiest way for us. It works and for now it will be sufficient. But your proposed approach is the correct one and I updated my post with that fact. We will redesign the solution when time permits it.
Link | January 14th, 2010 at 8:54 am
gabr wrote,
Very interesting info, thanks!
Link | January 14th, 2010 at 11:52 am
Christian Wimmer wrote,
>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.
To safety this is a long and hard way. Unfortunately, starting a SYSTEM process in a user session is not the right way. But I understand the need for a fast solution. However, are you sure that you will redo a working solution to make it nice some time later?
It is hard to introduce security in later stages of a software project. Security must be added from the beginning because it affects all parts of a software. Make security to a product quality of your software and give it some space.
>The truth is we were in a hurry and had so much to do before the project started, that this was the easiest way for us. It works and for now it will be sufficient. But your proposed approach is the correct one and I updated my post with that fact. We will redesign the solution when time permits it.
Therefore I suggest to use JWSCL instead of plain WinAPI
It is much faster and easier to use. And it is free with source. You can learn from reading it
>The GUI application has all possible privileges and does its job.
Well, instead you should investigate which privilege or group is necessary to do the job. For this you can add the necessary privilege with gpedit.msc to the user account that runs the job. E.g. for data sync I would check the BACKUP and RESTORE privileges. Use Process Explorer and Process Monitor to see how the application is failing.
>It gets the session token for the currently logged on user with “WtsGetActiveConsoleSessionID“.
Sorry, that is not correct. The function returns the ID of the session that is connected to the physical console. It means the session that is controlled by keyboard and monitor. If a user is logged onto the system using RDP then the result is still the same session ID but without a logged on user in the phys console. E.g. user logs onto using RDP. She gets the ID 2. Although the “keyboard session” is still number 1 – which is returned by the function.
>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.
The services in Vista and above are running in a separate session (aka Terminal Session) that has the number 0. All new users start with session number 1. In Windows XP and older the first user and the services share session 0. Each session can have different window stations.
>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.
Yes, that’s right. However, ineractive services are no more interactive although the switch still exists. However, there is a compatiblity feature that allows to switch to the service session to see messages from legacy services.
> if LookupPrivilegeValue(nil, SE_DEBUG_NAME, LD) then
I think you forgot to remove the privilege stuff here. There is no need to use the DEBUG privilege since you don’t use OpenProcess.
>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.
Have you tried to remove the desktop name from
SI.lpDesktop := PChar(‘winsta0\Default’); ?
Just use an empty string. Sometimes I had trouble with different things when using it.
> if CreateEnvironmentBlock(pEnv, hUserTokenDup, True) then
You should not create an env block using SYSTEM variables. Usually these env vars are system default and may also not exist (like the SYSTEM user folder). Instead use the env vars of the user in the target session.
Furthermore you do not free the env block using DestroyEnvironmentBlock .
> if not bResult then
begin
Result := GetLastError;
Exit;
end;
Have you considered using “RaiseLastOsError;” and try/finally.
In this way you can free all vars in a finally block ignoring all possible function exits.
Example: http://blog.delphi-jedi.net/2008/04/11/createprocess-in-full-glory/
Link | January 14th, 2010 at 1:18 pm
Iztok Kacin wrote,
@Christian
Thanks for the very good comments. Let me answer.
>>To safety this is a long and hard way. Unfortunately, starting a SYSTEM process in a user session is not the right way…
Yes I know that security is not a light topic. I did not imply that it is, I only said that the approach can be made safe and secure if done properly. And as I said the approach is not the best one. I hope we will redesign it, but if you are given so little time to prepare things then sadly you have to take shortcusts. This is out of my hands.
>> Therefore I suggest to use JWSCL instead of plain WinAPI
I know of JWSCL. I will take a close look, thanks
>> Well, instead you should investigate which privilege or group is necessary to do the job…
Again I would like to, but there was no time
>> The services in Vista and above are running in a separate session (aka Terminal Session) that has the number 0.
I read about it, but didn’t quite get all the details obviously. Thanks for explanation.
>> Sorry, that is not correct. The function returns the ID of the session that is connected to the physical console.
True, I wrote that the wrong way. Again thanks for explanation.
>> Have you tried to remove the desktop name from SI.lpDesktop := PChar(‘winsta0\Default’); ?
Yes I tried and it didn’t work. And if I remove it then it doesn’t work under XP anymore.
>> You should not create an env block using SYSTEM variables. Usually these env vars are system default and may also not exist (like the SYSTEM user folder). Instead use the env vars of the user in the target session.
Thanks, I will fix that.
>> Have you considered using “RaiseLastOsError;” and try/finally.
As I mentioned in the post I haven’t cleaned the code properly yet. Especially the error handling is very poorly done. Now with your input on the topic it will be much easier
Link | January 14th, 2010 at 1:38 pm
Christian Wimmer wrote,
>>Have you considered using “RaiseLastOsError;” and try/finally.
>As I mentioned in the post I haven’t cleaned the code properly yet. Especially the error handling is very poorly done. Now with your input on the topic it will be much easier
This is not about code cleaning but programming habits. Using exceptions is a great way to influence program behaviour. I have some complex C programs to maintain that can’t use exceptions but instead need to worry about freeing stuff in every condition (every return statement). Exceptions help there a lot.
Set a try after each memory allocation statement (e.g. GetMem, Create …) and add a finally with a freeing statement (FreeMem, FreeAndNil) immediately. There is no time loss!
I did this in JWSCL and other projects, and it helped to diminish memory leaks.
Link | January 14th, 2010 at 4:44 pm
Iztok Kacin wrote,
Exceptions are not silver bullet and should not be used to often and for everything. They are also expensive. Indy for instance overuses them and the results are not something I like. Often it is better that a function returns an error code and exception is then called higher in the chain if neccessary. Functions like the one I posted, should not handle errors with exceptions. Windows API too just return exit codes and it is up to the user to decide what to do next. If I make another example, ICS (socket suite) handles things that way.
I you looked at my example you can see that I am not leaking anything. Everything is cleaned in the finally statement. Except for the enviroment pointer which i forgot about. I am very strict about resource management and have no problem with memory leaks in a very complex system (multiprocess, multithreaded) which I develop and maintain. What I meant about cleanup is that the code is clumsy with error handling but it does the job. It is often hard to do the proper handling when a lot of Win32 API calls are in the game.
Link | January 14th, 2010 at 5:09 pm
Christian Wimmer wrote,
>Exceptions are not silver bullet and should not be used to often and for everything. They are also expensive. Indy for instance overuses them and the results are not something I like
How is it expressed? Why and how are they expensive? Please tell.
What means overuse? Do you mean that exceptions are thrown because some general event occured? Yes, I think this is a misuse of exceptions. If you run some application in Delphi and all the time an exception is thrown because something happens that happens all the time (again). Well, in JWSCL I had a similar case. I created an method that tried to get the token from the thread. If this failed the process token was used instead. However, since I used the method that threw an exception if it failed, I got this exception every time in Delphi debugger. The method had a try/except block that caught this one, so no harm’s done. But debugging was a lot harder in this way. So I decided to remove such things and duplicate code that doesn’t throw exceptions so I can handle them gracefully.
However, if don’t use exception but return values instead, people tend to just ignore them. I can show it here:
DuplicateTokenEx(hPToken, MAXIMUM_ALLOWED, nil, SecurityIdentification, TokenPrimary, &hUserTokenDup);
SetTokenInformation(hUserTokenDup, TokenSessionId, @dwSessionId, SizeOf(DWORD));
I can cite a lot of forum threads where posts are written that asks what happened to their code and what the error was. However, since they don’t check error values they get errors elsewhere not even related to the actual error line.
Well ,I wrote about that
http://blog.delphi-jedi.net/2010/01/14/programming-habits/
PS.
I have a lot of experience with WinAPI. Believe me that using exceptions is a great way to avoid strange errors with WinAPI. If you need speed, first, you should use a profiler (like ProDelphi) that shows you your costs. I have used it and I must say that it showed me areas that I would have never expected to be so expensive. So my experience is: First make it right and then make it fast.
On the other hand, I’m consulted a lot because of such bugs so I am throwing away gold here. Thus ignore it, please
Link | January 15th, 2010 at 3:02 pm
IL wrote,
Thank you Mr.Kacin and Mr.Christian both indeed. Could you advice any books about Windows security API useful to beginner Delphi programmer?
Link | February 2nd, 2010 at 4:23 pm
Iztok Kacin wrote,
I don’t think there are good books on Windows security API for delphi. Probably the best way is to learn as you go and look at places like MSDN and various forums. Also the security on Windows is a general topic, used in all programing languages. So there probably are books about that, but I can’t recomend any specific one, because I have not read any that were good enough.
Link | February 8th, 2010 at 5:37 pm
IL wrote,
Anyway thank you Mr.Kacin. Many years ago I’ve read master-class book by Michael Howard about secure web-based applications only. But time has gone greatly since.
Link | February 9th, 2010 at 2:57 pm
Christian Wimmer wrote,
Try this:
Rather old book (Win2000 area) from Keith Brown : Programing Windows Security.
I did my first steps with it.
This page is the online version : http://alt.pluralsight.com/wiki/default.aspx/Keith.GuideBook.HomePage
Link | February 18th, 2010 at 11:39 pm
Iztok Kacin wrote,
@Christian
Just to answer your latest comments. I considered what you said about error handling regarding windows API. I do agree with you, after thinking about it. In most cases it is probably best to throw an exception and prevent further damage by the user. There are still cases though, that that approach is not appropriate. But in general I agree with you. That said, I will adjust the example in this post to reflect that.
Link | February 19th, 2010 at 7:54 am
IL wrote,
Thank you Mr.Wimmer. This book is published in 2004 and “highlights new features in Windows Server 2003 and previews features of the upcoming version 2.0 of the .NET Framework.” Shall use it!
Link | February 19th, 2010 at 8:09 am
Michael Nixon wrote,
Thank you very much for this informative article. I have been looking for a solution for this for months, putting my project away. Looked again today and came across this article.
Very useful!
Link | February 24th, 2010 at 9:34 pm