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.