How many times have you seen the question asked - How can I set it so the user can interact with my service? Far too often I'm sure.
And how many times have you seen the answer - just check the 'Allow service to interact with desktop' box on the Log On page of the service properties? Probably a lot more often than you have seen the correct answer... "Run away, run away."
Make no mistake, if someone answers the original question with "Allow service to interact with desktop" then they are wrong, horribly wrong. Luckily, they are so wrong that, finally, as of Vista the setting no longer works. When the dreaded setting was introduced with NT 3.51 it was to be a rarely used setting, primarily for debugging and a few very rare services that needed to present options to the logged on user of the computer. The NT Messenger service comes to mind. But almost at once two things happened: many people noticed that running an application in the desktop with the LOCAL SYSTEM credentials was a huge potential security hole; and many service developers started using this setting to interact with the user in all kinds of situations, most of them, to put it bluntly, dumb.
I first heard the warning from Microsoft that this setting should never be used in 1998. And that same year I heard that it would be removed in Windows 2000. As you may have noticed it wasn't. Microsoft's backwards compatibility is often derided but it is essential. So, due to a plethora of sloppy service developers, the setting was saved. Now, finally, after warning after warning, the setting has been removed. Hallelujah!
So what is the answer, besides, "Run away"? Well, "Run away" is actually not a bad answer. If your service needs to interact with a user the correct approach is not to have the service do anything at all. Rather than thinking in terms of the service pushing out information have the user ask for the information. In this scenario your service should always have a client application that runs in the user's desktop, requests information and receives notifications from the service.
Of course, by always I mean almost always. There are a few edge case where it makes sense for a service to push out something to the user's desktop. An example is the Task Scheduler that can be set to run a task on the user's desktop if he or she is logged on. But even in these cases the interact with desktop setting is still the wrong answer. Remember it is still a huge security hole and it doesn't even work on Vista and later OSes. So, what is a poor service developer to do?
Well, I'm glad you asked. There are two techniques for handling the 'service must interact with the user' scenario.
99.9% of services that interact with a user should do so through a client application. These are the services I mention above. These services provide data to the user, notify the user of events that have occurred and need information from the user in the form of instructions or configuration data. These services need to set up a listener that will accept connections from client applications through sockets, pipes, .Net Remoting, Windows Communication Framework services or another of the many communications methods. Client applications can then connect to the service to receive data or notifications. The client applications can be manually started by a user at need or started automatically at log on but they are all started in the user's desktop, by the user! I'll finish this by reiterating - 99.9% or more of services fall under this category.
Finally, after making you wade through a few hundred words, I get to the meat of the article. What do we do with the tiny percentage of service that truly need to interact with the user. Like the Task Scheduler or perhaps a mission critical service that can't rely on a client controlled application that may not be running at a crucial time. Well, it's surprisingly easy to launch a program into a user's session - just follow the 3 step program to interactive goodness:
- Find the desktop to launch into. This may seem facetious but it isn't as simple as it seems. With Terminal Services and Fast User Switching there can be multiple interactive users logged on to the computer at the same time. If you want the user that is currently sitting at the physical console then you're in luck, the Terminal Services API call WTSGetActiveConsoleSessionId will get you the session ID you need. If your needs are more complex (i.e. you need to interact with a specific user on a TS server or you need the name of the window station in a non-interactive session) you'll need to enumerate the Terminal Server sessions with WTSEnumerateSessions and check the session for the information you need with WTSGetSessionInformation.
- Now you know what session you need to interact with and you have its ID. This is the key to the whole process, using WTSQueryUserToken and the session ID you can now retrieve the token of the user logged on to the target session. This completely mitigates the security problem of the 'interact with the desktop' setting, the launched process will not be running with the LOCAL SYSTEM credentials but with the same credentials as the user that is already logged on to that session! No privilege elevation.
- Using CreateProcessAsUser and the token we have retrieved we can launch the process in the normal way and it will run in the target session with the target user's credentials. There are a couple of caveats, both lpCurrentDirectory and lpEnvironment must point to valid values - the normal default resolution methods for these parameters don't work for cross-session launching. You can use CreateEnvironmentBlock to create a default environment block for the target user.
Now that you know how to launch a process into an interactive session I'd just like to say, "Don't do it." Your service will need to run under the LOCAL SYSTEM account in order to succeed or you'll need to give the 'act as part of the operating system' privilege to another account, neither of these is the best scenario. Your service is probably one of the 99.9% that don't need this technique, so think long and hard before jumping in.
Source code to demonstrate this technique (in C#) can be found here.