Monday, March 30, 2009
A couple of days ago a friend of mine called with a problem. She had a Windows service written in C# that, under certain conditions, needed to launch some applications on behalf of two specific users and she was getting one error after another. Oddly enough, I had just answered a question on the same subject over at Stack Overflow and I had seen a few other references to similar problems in other forums (usually with incorrect answers).
This isn't a common need but it also isn't that rare, so I thought I had better post this step by step guide to troubleshooting the launching of a process under impersonated credentials. This is based on using the Start method of the .Net Process class but it is also applicable to the underlying API calls: CreateProcessWithLogonW and CreateProcessWithTokenW.
Access Denied - The first attempt and an access denied exception right off the bat. This is the most common initial problem and is caused by the fact that the service is running under the LOCAL SYSTEM account. Strangely, the SYSTEM account is the most powerful account on the computer but one of the few things it cannot do is launch a process using CreateProcessWithLogonW which is the API underlying the call to Process.Start. So change your service account to Local Service, it's probably the more appropriate account anyway.
Access Denied Again - Aargh, I thought we solved this. Oops, double check the permissions on the application that you are trying to launch. Remember that the system tries to access the application file as the user account that the process will be running under, not the service account.
Invalid Directory Error - What? All the paths are correct. All the directories are spelled correctly, no invalid characters. This is an incredibly annoying error and is not very consistent. Usually when we run a process we don't bother setting the WorkingDirectory property and just accept the default from the parent process. When starting a process with new credentials you can't do that, you must explicitly set a path for the WorkingDirectory or you'll get a "The directory name is invalid." Win32Exception.
Failure: No Error? - Process.Start handles the creation of the Environment block for the new process for you quite well. So this is a problem only if you are using the underlying API. When calling one of the CreateProcess* APIs it is normal to leave the lpEnvironment parameter as NULL and have the system use the default of copying the block from the parent process. But when launching under new credentials you must create an environment block explicitly, either manually or using CreateEnvironmentBlock. What makes this worse is, if you leave this out the CreateProcess* call will fail but GetLastError will return ERROR_SUCCESS and if you make an error creating your environment block there will be no error but the process may just not run at all.
Application Failed to Initialize Properly - No more exceptions, you've solved all the problems and the process has been launched. Oops again, where is the process? Check the event log (or you may have received an Application Error pop-up). There should be an entry for Application Error that says that your process was the faulting application, either user32.dll or kernel32.dll was the faulting module and the exception was: 0xC0000142. There may be some minor variation in this but basically it is saying that your application could not initialize. The reason for this is that on initialization, before any application code is run, all processes are attached to a Window Station and all threads are attached to a Desktop but the user you are launching under does not have permission to access the Window Station and Desktop in which your process is being launched, ergo it can't initialize. The security descriptors for the Window Station and Desktop must be adjusted to give AllAccess permission to the user the process is being launched under. This is a devil to do directly in .Net, so you might find the security wrapper classes here useful.
No More Errors - Really, no more errors, your process should be running smoothly now. There may be some variations in what you need to do based on who the user is (for instance an administrator will already have the correct permissions in some cases) or what kind of session you are launching in. But following these steps should make your life smooth and easy (well maybe not your whole life).
Source code (C#) supporting this article can be found here.
Sunday, March 29, 2009
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.
Welcome to Omphalos.
I'm Stephen Martin: a software designer and developer in Toronto, Ontario. I'm probably not going to blow you away with my writing prowess but I think you'll find that I post some useful, and not easily found elsewhere, technical musings.
Over the years I've used FORTRAN, Lisp, C, C++, VB, C# and a couple of varieties of Assembly on VMS, Unix, Linux and Windows but I'm mainly into Windows system software and .Net at the moment so most posts (or at least the sample code illustrating them) will probably be .Net related for the foreseeable future.
I've turned off comments for this blog. While having comments directly attached to a blog article is convenient in many ways, I find that the more fluid style of conversation promoted by a discussion group is much more helpful in the long run. So, if you have any comments, good, bad or ugly, please join me at Omphalos Talk for further discussion.
Thanks for dropping by, I hope you find it useful and even, occasionally, entertaining.
Posted by Stephen Martin at 11:14 AM