Launching a process that the currently logged on user can see on their desktop session (and interact with) from a Windows service is a popular topic – and there are a wide variety of answers out there when someone asks how to do this, some people say it is not even possible on Windows Vista or Windows 7. Turns out it is actually very easy…
Firstly, I do not really recommend this method because ideally you should have a process running in the user’s session already that communicates with your service via Named Pipes, TCP/IP, or whatever – this process can receive a command from your service and launch/do whatever you want and it will already be running in the user’s session and security context. The only reason for posting this method is because in some scenarios that ideal method is not always possible or not warranted. I think of this as a quick and dirty way of doing it if you can’t do it a more ‘proper’ way. I’m using it as part of an installation package for a program that we deploy via SCCM (which therefore gets run from the SCCM Agent service) to run a one off job that needs to be executed as the currently logged on user to avoid them needing to log out and back on before the newly deployed application will work correctly (and no its not just HKCU registry edits or anything like that which could be done in an easier way).
I’ve seen plenty of other different ways of doing this, some unnecessarily complex, but this method I am going to demonstrate seems to be by far the most simple and elegant. Oh and you don’t need to mark your service as an Interactive service and the user will not get switched to the ‘services’ desktop when they try to interact with the newly launched process.
Here is the most basic example I could come up with (needs error handling etc):
Dim UserTokenHandle As IntPtr = IntPtr.Zero
WindowsApi.WTSQueryUserToken(WindowsApi.WTSGetActiveConsoleSessionId, UserTokenHandle)
Dim ProcInfo As New WindowsApi.PROCESS_INFORMATION
Dim StartInfo As New WindowsApi.STARTUPINFOW
StartInfo.cb = CUInt(Runtime.InteropServices.Marshal.SizeOf(StartInfo))
WindowsApi.CreateProcessAsUser(UserTokenHandle, «C:\Windows\System32\cmd.exe», IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, False, 0, IntPtr.Zero, Nothing, StartInfo, ProcInfo)
If Not UserTokenHandle = IntPtr.Zero Then
WindowsApi.CloseHandle(UserTokenHandle)
End If
Obviously this example launches command prompt but you can replace C:\Windows\System32\cmd.exe with whatever application you want to launch. As soon as CreateProcessAsUser is called, the process will be started and will appear on the user’s screen and act just like any other program. Well with one exception, with that basic example the user’s environmental variables will not be set – so for example if from that newly launched command prompt process you type “Echo %USERNAME%” then it will show that the %USERNAME% variable is currently set to the local system account. I’m sure you could get the environmental variables to get setup correctly by calling CreateEnvironmentBlock and passing the result in to the lpEnvironment argument of CreateProcessAsUser (instead of passing in Nothing as I do in this example).
Also note that this method assumes your service is running as Local System – other accounts wont have the required permissions for this to work.
For anyone that can’t be bothered (or doesn’t know how) to define the Windows APIs used in this example, here you go (I have also included each of these in my Windows API library)
Imports System.Runtime.InteropServices
Public Class WindowsApi
<DllImport(«kernel32.dll», EntryPoint:=«WTSGetActiveConsoleSessionId», SetLastError:=True)> _
Public Shared Function WTSGetActiveConsoleSessionId() As UInteger
End Function
<DllImport(«Wtsapi32.dll», EntryPoint:=«WTSQueryUserToken», SetLastError:=True)> _
Public Shared Function WTSQueryUserToken(ByVal SessionId As UInteger, ByRef phToken As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
End Function
<DllImport(«kernel32.dll», EntryPoint:=«CloseHandle», SetLastError:=True)> _
Public Shared Function CloseHandle(<InAttribute()> ByVal hObject As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
End Function
<DllImport(«advapi32.dll», EntryPoint:=«CreateProcessAsUserW», SetLastError:=True)> _
Public Shared Function CreateProcessAsUser(<InAttribute()> ByVal hToken As IntPtr, _
<InAttribute(), MarshalAs(UnmanagedType.LPWStr)> ByVal lpApplicationName As String, _
ByVal lpCommandLine As System.IntPtr, _
<InAttribute()> ByVal lpProcessAttributes As IntPtr, _
<InAttribute()> ByVal lpThreadAttributes As IntPtr, _
<MarshalAs(UnmanagedType.Bool)> ByVal bInheritHandles As Boolean, _
ByVal dwCreationFlags As UInteger, _
<InAttribute()> ByVal lpEnvironment As IntPtr, _
<InAttribute(), MarshalAsAttribute(UnmanagedType.LPWStr)> ByVal lpCurrentDirectory As String, _
<InAttribute()> ByRef lpStartupInfo As STARTUPINFOW, _
<OutAttribute()> ByRef lpProcessInformation As PROCESS_INFORMATION) As <MarshalAs(UnmanagedType.Bool)> Boolean
End Function
<StructLayout(LayoutKind.Sequential)> _
Public Structure SECURITY_ATTRIBUTES
Public nLength As UInteger
Public lpSecurityDescriptor As IntPtr
<MarshalAs(UnmanagedType.Bool)> _
Public bInheritHandle As Boolean
End Structure
<StructLayout(LayoutKind.Sequential)> _
Public Structure STARTUPINFOW
Public cb As UInteger
<MarshalAs(UnmanagedType.LPWStr)> _
Public lpReserved As String
<MarshalAs(UnmanagedType.LPWStr)> _
Public lpDesktop As String
<MarshalAs(UnmanagedType.LPWStr)> _
Public lpTitle As String
Public dwX As UInteger
Public dwY As UInteger
Public dwXSize As UInteger
Public dwYSize As UInteger
Public dwXCountChars As UInteger
Public dwYCountChars As UInteger
Public dwFillAttribute As UInteger
Public dwFlags As UInteger
Public wShowWindow As UShort
Public cbReserved2 As UShort
Public lpReserved2 As IntPtr
Public hStdInput As IntPtr
Public hStdOutput As IntPtr
Public hStdError As IntPtr
End Structure
<StructLayout(LayoutKind.Sequential)> _
Public Structure PROCESS_INFORMATION
Public hProcess As IntPtr
Public hThread As IntPtr
Public dwProcessId As UInteger
Public dwThreadId As UInteger
End Structure
End Class
Hope it helps someone out
Chris
If you have some knowledge about Windows services and their functionalities then go ahead. Otherwise you may not understand some of the points mentioned here.
Are you suffering from starting a process from your windows service ? I have a cunning solution for that. Normally we cannot start any process either on our local machine or from a remote machine through a windows service.
We can start the process by enabling the Desktop Interactive of the service, but the UAC system of Vista and Windows 7 is a problem, that every time we start the process from a service it asks whether to allow the desktop interactivity. We can’t ask our clients to stop the UAC system. So what is the solution ?
Now I think you have some idea why can’t we start a process from service. But you might have a question in your mind why we need a process for a service ? There can be several answers
- We need to perform a CPU intensive operation
- COM interoperability is clashing with some threading components of the service
- Some actions can be performed more efficiently by a separate process rather than a service
I figured out this solution as I suffered from the bolded point. Here the method I have used.
Create an ASPX webpage (it should be hosted in a later version of IIS 4.0)
In the Page_Load method start the process.
Then Create the Windows Service to access the the web page. (Access it through normal WebRequest and WebResponse). Cool !
The real cool thing is you can almost start all the processes by using this method from a windows service. Even processes with arguments and Verbs.’
Here’s the code for the webpage
protected void Page_Load(object sender, EventArgs e) { Process p = new Process(); p.StartInfo.FileName = "path to your process (your .exe file)" p.StartInfo.UseShellExecute = false; p.StartInfo.CreateNoWindow = true; p.Start(); p.WaitForExit(); p.Dispose(); } In the service within the OnStart() put the following code try { HttpWebRequest req = (HttpWebRequest)WebRequest.Create("ASPX page path"); HttpWebResponse resp = (HttpWebResponse)req.GetResponse(); } catch (Exception ex) { }
It isn’t usually possible to launch a GUI (Graphical User Interface) application from a Windows Service. There are good reasons for this; aside from the security considerations, being interrupted while doing something important by a badly behaving background application would not be fun!
However, there are some limited use cases for starting processes that feature a GUI from a Windows Service, such as for specific kinds of software updates and monitoring systems. If you have already looked at alternatives and believe that launching a GUI application from a Windows Service is most appropriate for your scenario, read on and I’ll show you how to achieve this.
A lot of the information available online regarding this topic tends to be a bit on the vague side. By contrast, I will provide you with a full working implementation and a link to the source code which you can clone/download and run to get started.
Solution background
In older versions of the Windows operating system, such as Windows XP, GUI applications could be started from a Windows Service by enabling the Allow service to interact with desktop option for the service.
However, this all changed in 2007 when Microsoft released their latest version of Windows.
Session 0
Remember the all-but-forgotten Windows Vista? When it was first introduced, Microsoft introduced Session 0 Isolation whereby all Windows Services run in a special non-interactive system session referred to as ‘Session 0’, whereas all user sessions run in ‘Session 1’ or above.
With Session 0 Isolation, the ‘Allow service to interact with desktop’ option takes on a new meaning. It now means that a GUI application can be started by a Windows Service, but its user interface will appear within a virtual desktop in Session 0 which is hidden from the user. As you can imagine, this isn’t going to be of much benefit to very many applications.
Security
Security is a major reason why it isn’t normally possible to launch a GUI application from a Windows Service. If a Windows Service is running as a highly privileged user, such as the Local System account and a user-interactive application is launched under the context of this account by the service, the end-user could potentially use that application to exploit the system by carrying out actions that they would normally not have permission to do.
However, there is a lesser-known, safer way of launching a GUI application under the context of the currently logged-in user from a Windows Service that works by obtaining the ‘primary token’ of the user. The aforementioned method will launch the application with the standard permissions that the user has, without allowing any administrative elevation. Most applications do not need to run as administrator, so this approach will work just fine for the majority of scenarios and it is better for security as the application will fail to launch if administrative privileges are required.
The key thing to bear in mind is that the Windows Service in question must be running as the Local System account. As mentioned above, even though Local System is a highly privileged account, with the approach that I am documenting it is only possible to launch an application with the same privileges as the currently logged-in user. Regardless of whether or not the current user is an administrator, trying to launch the application with administrative rights using this approach will not work, and that is a good thing.
With all that said and without further, let’s take a look at the solution implementation.
Solution implementation
The solution involves invoking WIN32 methods that are imported into a .NET application so that we can call them using C# code. Ultimately, the native method that is called to launch the GUI application is the CreateProcessAsUser
method.
It can be challenging to piece together all of the things you need to call the CreateProcessAsUser
method correctly. Therefore, I have distilled all of the code that is needed to make the process creation happen correctly into a class that is documented below for your reference and ease of use.
using JC.Samples.WindowsServiceGuiLauncher.Services.Interfaces; using System.Diagnostics; using System.Runtime.InteropServices;namespace JC.Samples.WindowsServiceGuiLauncher.Services; /// <summary> /// Provides services for launching new processes. /// </summary> public class ProcessServices : IProcessServices { #region WIN32 #region Constants private const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400; private const int GENERIC_ALL_ACCESS = 0x10000000; private const int STARTF_FORCEONFEEDBACK = 0x00000040; private const int STARTF_USESHOWWINDOW = 0x00000001; private const short SW_SHOW = 5; private const uint TOKEN_ASSIGN_PRIMARY = 0x0001; private const uint TOKEN_DUPLICATE = 0x0002; private const uint TOKEN_QUERY = 0x0008; #endregion #region Structs [StructLayout(LayoutKind.Sequential)] private struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public uint dwProcessId; public uint dwThreadId; } [StructLayout(LayoutKind.Sequential)] private struct SECURITY_ATTRIBUTES { public uint nLength; public IntPtr lpSecurityDescriptor; public bool bInheritHandle; } [StructLayout(LayoutKind.Sequential)] private struct STARTUPINFO { public uint cb; public string lpReserved; public string lpDesktop; public string lpTitle; public uint dwX; public uint dwY; public uint dwXSize; public uint dwYSize; public uint dwXCountChars; public uint dwYCountChars; public uint dwFillAttribute; public uint dwFlags; public short wShowWindow; public short cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } #endregion #region Enums private enum SECURITY_IMPERSONATION_LEVEL { SecurityAnonymous = 0, SecurityIdentification = 1, SecurityImpersonation = 2, SecurityDelegation = 3 } private enum TOKEN_TYPE { TokenPrimary = 1, TokenImpersonation = 2 } #endregion #region Imports [DllImport("kernel32.dll", SetLastError = true)] private static extern bool CloseHandle( IntPtr hObject); [DllImport("userenv.dll", SetLastError = true)] private static extern bool CreateEnvironmentBlock( ref IntPtr lpEnvironment, IntPtr hToken, bool bInherit); [DllImport("advapi32.dll", SetLastError = true)] private static extern bool CreateProcessAsUser( IntPtr hToken, string? lpApplicationName, string? lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes, ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string? lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); [DllImport("userenv.dll", SetLastError = true)] private static extern bool DestroyEnvironmentBlock( IntPtr lpEnvironment); [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx", SetLastError = true)] private static extern bool DuplicateTokenEx( IntPtr hExistingToken, uint dwDesiredAccess, ref SECURITY_ATTRIBUTES lpTokenAttributes, SECURITY_IMPERSONATION_LEVEL impersonationLevel, TOKEN_TYPE tokenType, ref IntPtr phNewToken); [DllImport("advapi32.dll", SetLastError = true)] private static extern bool OpenProcessToken( IntPtr processHandle, uint desiredAccess, ref IntPtr tokenHandle); #endregion #endregion #region Readonlys private readonly ILogger<ProcessServices>? _logger; #endregion #region Constructor /// <summary> /// Default Constructor. /// </summary> public ProcessServices() { } /// <summary> /// Constructor. /// </summary> /// <param name="logger"><see cref="ILogger"/></param> public ProcessServices(ILogger<ProcessServices> logger) { _logger = logger; } #endregion #region Methods #region Public /// <summary> /// Starts a process as the currently logged in user. /// </summary> /// <param name="processCommandLine">The full process command-line</param> /// <param name="processWorkingDirectory">The process working directory (optional)</param> /// <param name="userProcess">The user process to get the Primary Token from (optional)</param> /// <returns>True if the process started successfully, otherwise false</returns> public bool StartProcessAsCurrentUser( string processCommandLine, string? processWorkingDirectory = null, Process? userProcess = null) { bool success = false; if (userProcess == null) { // If a specific user process hasn't been specified, use the explorer process. Process[] processes = Process.GetProcessesByName("explorer"); if (processes.Any()) { userProcess = processes[0]; } } if (userProcess != null) { IntPtr token = GetPrimaryToken(userProcess); if (token != IntPtr.Zero) { IntPtr block = IntPtr.Zero; try { block = GetEnvironmentBlock(token); success = LaunchProcess(processCommandLine, processWorkingDirectory, token, block); } finally { if (block != IntPtr.Zero) { DestroyEnvironmentBlock(block); } CloseHandle(token); } } } return success; } #endregion #region Private /// <summary> /// Gets the Environment Block based on the specified token. /// </summary> /// <param name="token">The token pointer</param> /// <returns>The Environment Block pointer</returns> private IntPtr GetEnvironmentBlock(IntPtr token) { IntPtr block = IntPtr.Zero; bool result = CreateEnvironmentBlock(ref block, token, false); if (!result) { _logger?.LogError("CreateEnvironmentBlock Error: {0}", Marshal.GetLastWin32Error()); } return block; } /// <summary> /// Gets the Primary Token for the specified process. /// </summary> /// <param name="process">The process to get the token for</param> /// <returns>The token pointer</returns> private IntPtr GetPrimaryToken(Process process) { IntPtr primaryToken = IntPtr.Zero; // Get the impersonation token. IntPtr token = IntPtr.Zero; bool openResult = OpenProcessToken(process.Handle, TOKEN_DUPLICATE, ref token); if (openResult) { try { var securityAttributes = new SECURITY_ATTRIBUTES(); securityAttributes.nLength = (uint)Marshal.SizeOf(securityAttributes); // Convert the impersonation token into a Primary token. openResult = DuplicateTokenEx( token, TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_QUERY, ref securityAttributes, SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, TOKEN_TYPE.TokenPrimary, ref primaryToken); } finally { CloseHandle(token); } if (!openResult) { _logger?.LogError("DuplicateTokenEx Error: {0}", Marshal.GetLastWin32Error()); } } else { _logger?.LogError("OpenProcessToken Error: {0}", Marshal.GetLastWin32Error()); } return primaryToken; } /// <summary> /// Launches the process as the user indicated by the token and Environment Block. /// </summary> /// <param name="commandLine">The full process command-line</param> /// <param name="workingDirectory">The process working directory</param> /// <param name="token">The token pointer</param> /// <param name="environmentBlock">The Environment Block pointer</param> /// <returns>True if the process was launched successfully, otherwise false</returns> private bool LaunchProcess( string commandLine, string? workingDirectory, IntPtr token, IntPtr environmentBlock) { var startupInfo = new STARTUPINFO(); startupInfo.cb = (uint)Marshal.SizeOf(startupInfo); // If 'lpDesktop' is NULL, the new process will inherit the desktop and window station of its parent process. // If it is an empty string, the process does not inherit the desktop and window station of its parent process; // instead, the system determines if a new desktop and window station need to be created. // If the impersonated user already has a desktop, the system uses the existing desktop. startupInfo.lpDesktop = @"WinSta0\Default"; // Modify as needed. startupInfo.dwFlags = STARTF_USESHOWWINDOW | STARTF_FORCEONFEEDBACK; startupInfo.wShowWindow = SW_SHOW; var processSecurityAttributes = new SECURITY_ATTRIBUTES(); processSecurityAttributes.nLength = (uint)Marshal.SizeOf(processSecurityAttributes); var threadSecurityAttributes = new SECURITY_ATTRIBUTES(); threadSecurityAttributes.nLength = (uint)Marshal.SizeOf(threadSecurityAttributes); bool result = CreateProcessAsUser( token, null, commandLine, ref processSecurityAttributes, ref threadSecurityAttributes, false, CREATE_UNICODE_ENVIRONMENT, environmentBlock, workingDirectory, ref startupInfo, out _); if (!result) { _logger?.LogError("CreateProcessAsUser Error: {0}", Marshal.GetLastWin32Error()); } return result; } #endregion #endregion }
Note that you should update the namespace and using statements in the code sample according to your project.
In the following sub-sections, I will provide a breakdown of each part of the above class. I highly recommend that you check out the associated GitHub repository so that you can see how the ProcessServices
class is used along with the public StartProcessAsCurrentUser
method to launch a process from a .NET Core Worker Service which is configured to run as a Windows Service.
Interface
The ProcessServices
class implements an interface that is documented below for reference.
using System.Diagnostics; namespace JC.Samples.WindowsServiceGuiLauncher.Services.Interfaces; /// <summary> /// Process Services interface. /// </summary> public interface IProcessServices { #region Methods bool StartProcessAsCurrentUser( string processCommandLine, string? processWorkingDirectory = null, Process? userProcess = null); #endregion }
The interface is used in the sample project on GitHub for dependency injection. If you don’t need the interface in your application, feel free to remove it and the associated using statement.
WIN32
The solution relies heavily on native WIN32 methods. The ‘WIN32’ region at the top of the ProcessServices
class contains the definition of all the constants, structs, and method imports that are needed to facilitate calls into the unmanaged code.
PInvoke.net is a useful resource for establishing the correct signatures and types to use when you want to invoke methods within unmanaged libraries from managed code within your .NET application.
The Microsoft Learn website is also an invaluable resource for cross-referencing purposes to help ensure that method signatures and types are matching up with the original C++ code. For example, the PROCESS_INFORMATION
page contains Syntax, Members and Remarks sections that helpfully document each aspect of the PROCESS_INFORMATION
struct.
I find it helpful to keep the WIN32-related code as faithful as possible to the original C++ implementation, using the same capitalisation for constants/structs/enums and the same variable naming conventions. This makes it easier to cross-reference with online documentation and hints that it is unmanaged code we are calling into.
The DllImport
attribute along with the extern
keyword is used to indicate that the method signatures within the ‘Import’ sub-region are implemented within external DLLs (Dynamic Link Libraries).
Initialisation
The ProcessServices
class features a default constructor and a second constructor that accepts a typed ILogger
instance as a parameter, which naturally is used for logging events. If you don’t need logging, you can use the default parameterless constructor to create a ProcessServices
object instance.
Public methods
The main public interface method is named StartProcessAsCurrentUser
and accepts the process command line as a required parameter. This is intended to represent the full command line of the process to start and should therefore include the process name along with any arguments/switches that are to be passed to the process.
The StartProcessAsCurrentUser
method also accepts a second, optional parameter that allows the working directory for the process to be set. This can be quite useful as the default working directory is typically the Windows System folder (C:\Windows\System32) and depending on your application this could affect its operation significantly.
The third parameter of the StartProcessAsCurrentUser
method is also optional and allows a Process
object from which to obtain the user’s primary token to be passed in. If unspecified this will default to the ‘explorer’ process, as this process is normally running all of the time on a user’s system and will typically be running under the context of the current user.
At a high level, the StartProcessAsCurrentUser
method obtains a pointer to the user’s primary token by calling the private GetPrimaryToken
method. The private GetEnvironmentBlock
method is subsequently called to get a pointer to the user’s environment block (environment variables). The token and block pointers are then passed to the third private method named LaunchProcess
to start the process as the currently logged-in user.
The code ensures that resources relating to the primary token and environment block are cleaned up properly by calling the unmanaged DestroyEnvironmentBlock
and CloseHandle
methods respectively and doing so within a try-finally
block. This is very important and is something that is frequently missed out when developers are attempting to smash unfamiliar code together.
Private methods
The first private method that is called by the public StartProcessAsCurrentUser
method is the GetPrimaryToken
method which gets a pointer to a primary token based on the specified process.
The GetPrimaryToken
method calls the unmanaged OpenProcessToken
method to open the access token for the specified process. The unmanaged DuplicateTokenEx
method is then called to create a duplicate primary token with the specified attributes. Again, the code is careful to clean things up by closing the handle to the access token. The static Marshal.GetLastWin32Error
method is called when logging errors and it retrieves the error code that was set by the last unmanaged method call. This can be very useful for troubleshooting purposes.
The GetEnvironmentBlock
method calls the unmanaged CreateEnvironmentBlock
method to get environment variables for a user based on the specified token. The Marshal.GetLastWin32Error
method is called if the result returned by the CreateEnvironmentBlock
method is false for error logging purposes.
The LaunchProcess
method features several parameters; the command line, working directory, primary token, and environment block to use when creating the process. Within the method, the STARTUPINFO
struct is first created and its fields are set with the appropriate values. ‘WinSta0\Default’ is a special value that is used to represent the default desktop for the currently logged-in user. Aside from this, notice the flags that are used to show the application window when it is launched, you can adjust these flags according to your requirements.
After creating process and thread security attributes using the SECURITY_ATTRIBUTES
struct, the CreateProcessAsUser
method is called. The CreateProcessAsUser
parameter values that have been specified in the code sample should be suitable for the majority of applications, however, you are free to review the relevant Microsoft Learn documentation further and adjust things according to your specific needs.
As per the CreateEnvironmentBlock
method, the CreateProcessAsUser
method returns a boolean value indicating if the method call was successful. If the process was not created successfully the result will be false and the Marshal.GetLastWin32Error
method will be called to determine the error code to log.
Testing the implementation
To test the implementation properly you’ll need to install your application as a Windows Service that runs as the Local System account and check that the service can launch a GUI application successfully.
If you are unsure of how to install a Windows Service, check out the Windows Service installation section within my How to create and install a .NET Core Windows Service article. You’ll need to adjust the sc
command to set the ‘obj’ argument to ‘LocalSystem’, as shown below.
sc create WindowsServiceGuiLauncher binPath= "<path_to_publish_directory>\JC.Samples.WindowsServiceGuiLauncher.exe" DisplayName= "Windows Service GUI Launcher" obj= LocalSystem start= auto
Note that you must change the name of the service and path to match your application.
Regarding code, you will need to create an instance of the ProcessServices
class and call the StartProcessAsCurrentUser
method with the appropriate parameter values within your Windows Service. A basic example of this is shown below for your reference.
var processServices = new ProcessServices(); processServices.StartProcessAsCurrentUser("notepad");
I’m assuming that you already know the basics of how to create a Windows Service project and that you can determine the most appropriate place to call the StartProcessAsCurrentUser
method within your application code. If you are new to Windows Services you can check out my How to create and install a .NET Core Windows Service article for information on how to create a .NET Core Worker Service and wire it up to run as a Windows Service.
If you have placed the above code somewhere in your program such that it will execute after the Windows Service starts running (as a test) you should see an instance of the Notepad program created and visible within your desktop environment!
Before wrapping things up, I would like to recommend again that you check out my GitHub repository which uses dependency injection to create an instance of the ProcessServices
class and automatically injects an ILogger
instance as part of a Worker Service project that is configured to run as a Windows Service.
Summary
In this article, I have documented a class that provides a public method you can call to start a GUI application process from a Windows Service that is running as the Local System account.
I started by providing some background on ‘Session 0 Isolation’ and security considerations.
I then dived into the implementation of the ProcessServices
class which contains the public StartProcessAsCurrentUser
method that you can call from within your Windows Service codebase to successfully launch a GUI application in the context of the currently logged-in user.
I finished by explaining the basic steps you need to carry out to test the solution for your application. I highly recommend that you clone or download the associated GitHub repository which includes all of the code from this article and a working .NET Core Worker Service application that you can build and run to see the functionality in action.
The process that’s causing this pop-up might be running on a different machine connected to your network than the one you’re working from. If the server is not in an active state it may try and start an application for which the user does not have permission, hence the message. You can use the «SELINUX /System» command to get a list of processes currently running on a Windows 7 machine:
selogin /System
This will show you a list of processes along with their permissions. If the process that’s causing the popup is not showing as a permitted process, then it might be located in another computer connected to your network. You can use «SELINUX /Control» command to control running programs and see where the current one has its permissions coming from:
selogin /Control
You should be able to see where that process is getting its permissions from by examining its permission level.
A system admin is trying to debug a Windows application and encounters similar issues as in the user’s case: he receives a pop-up stating, «An unauthorised program is trying to display a message». He uses two commands to help him identify the culprit, but due to some technical limitations of his system, they are unable to show a detailed permission level.
The only details provided are that:
- The program responsible for displaying the pop-up can be controlled by at most five distinct processes (let’s call them Process A, B, C, D and E).
- The system admin has limited time and wants to make an educated guess of which process is causing the problem.
- Each process could have one or more permissions allowed by another process in any possible order: no two processes share the exact same set of permissions (each has a unique combination), but permission levels overlap slightly at some point, meaning that it’s possible for one process to get its permissions from multiple other processes.
Using his understanding of property of transitivity and using logical reasoning, can you help him identify which is the probable cause of pop-up?
The only data he has are:
a) Process D gets permission ‘read’ and ‘write’ but not ‘exec’.
b) Process E gets ‘exec’, ‘create’, ‘open file’ but not ‘read’ and ‘write’.
First, let’s identify the permissions of Process A, B, C.
We know that these processes have permission level overlaps with some other processes which means they share at least one permission level between them. Let’s denote by L1 to L5 their set of allowed permissions in no particular order (no repetition).
If we take two distinct combinations for process A, B, C and consider them for D and E — that are those that overlap with a unique combination (a union of L2 to L4), and those that have intersection between L1 and L2. Then we will have:
The union of permissions would include at least one ‘read’, ‘write’ from processes B, C or E which should not cause the pop-up in A as these are forbidden for read and write permissions according to the command he used. Similarly, a combination having only two processes D and E that allow ‘exec’ (L4), and ‘open file’ (L3) but not ‘read’ and ‘write’ won’t give him an answer as we know those permissions can be present in any of B, C or A’s permission sets.
For the combinations where we consider a unique combination for processes D and E that have intersection with some permission level between L1 (the most restricted permission) to L2: these would definitely not give him an answer. These are because they will allow some permissions in common which should never occur. Hence, he needs to eliminate such combinations.
Answer: The possible cause of pop-up is either process A, B, C, as it allows two permissions from D and E’s set of permissions. He has not received a permission that occurs in more than one of D and E’s sets which means they can’t be the culprit for his issue.
This article reviews several scenarios of process creating from Windows service like: creating process win GUI from non-interactive service, creating process under another user account and in the different session. The article doesn’t contain code examples however it explains how to achieve the result with a free command line tool DevxExec. This text requires basic level of familiarity of Windows API.
There are could be different situations when you need to create an additional process from your main process. One of those situations is when you need to isolate some code of your service because that code could be a reason of resource leak or can damage your common process somehow. Another case is when your application needs to perform action that requires changing primary process token. Finally you just may need to run a 3rd party application.
From first glance creating a new process is a very simple operation. That is actually true. There are 3 different functions CreateProcess, CreateProcessAsUser and CreateProcessWithLogon in Windows API that you can use to create a new process. However there are situations when they don’t work in the way you could expect.
How to run a process from the Windows service
To allow several users work with the same computer at the same time Windows isolates processes of each user with special object called session. During system startup it creates session with ID=0. OS uses this session to isolate windows services processes from users’ processes. Also Windows creates another session (ID=1) during startup and run first instance of winlogon.exe under that session. Therefore first logged in (via console) user will work under session with ID=1. When another user wants to login to computer system creates a new session for him.
Window always creates a new session before the user logs in. Another words it happens when a new remote desktop connection is established or when user connected via console uses “switch user” function. Once the session is created, Windows runs a new instance of winlogon.exe. So the login screen, where user types his credentials, always relates to its own session.
Every session has own windows station. Windows station is another isolation object that has come to us from older version of system. Every window station contains a dedicated clipboard and a number of desktops.
Here is the typical hierarchy of sessions, window stations and desktops in Windows Vista and Seven (Windows 7).
In older systems like XP or 2003, it is different. The user logged in via console and the services are running in the same session and interactive services even use the same windows station and the same desktop.
When you are running a process from windows service under user account, that process may not have (usually it doesn’t) access to the window station and to the desktop related to session 0. Of course, if your process doesn’t create any window then you are just lucky. However it’s actually not easy to write an application that doesn’t create any window at all. Typical Windows application infrastructure is based on a window even if this window is newer shown. The console application written on the most of languages (like Visual C++ or C#) creates a window too. Therefore in 99% of cases our application needs the access to the window station and to the desktop.
Information
When you run a console application and the user under which the process is running doesn’t have an access to the window station on some versions of OS you may see the error message like this:
To solve this problem you can add permissions to the window station and the desktop. Usually services run under the separate window station “Service-0x0-3e7$”. However if the option “Allow interact with Desktop” is on for the service then it runs under “WinSta0”. The difference between those windows stations is that “Service-0x0-3e7$” can’t be ever interactive so user will be not able to see the GUI of applications are running there. In case of “WinSta0” user can see the GUI but remember that in Windows Vista/Seven/2008 interactive services and logged in user have different window stations with the same name but in different sessions.
In Windows Vista/Seven/2008, ones a window is created into the Session0Winsta0Default, operation system will show a dialog like this. This dialog allows switching active user desktop to Session0Winsta0Default.
It’s actually not really important if you service runs in interactive mode or not. You always can specify window station and a desktop while creating a new process. There is a special parameter in StartupInfo structure for this.
You also can use the Developex free tool DevxExec to run a process from the windows service under another user.
DevxExec.exe /user: Administrator /password:qwerty “cmd.exe”
This command automatically grand to user “Administrator” permissions on the appropriate window station and the desktop and run a command shell.
How to run an application for a user from the Windows service
Let’s see the following scenario. You have a Windows service running. User Marcus has logged to the computer. And you want yours service to start the application that will be shown to Marcus on his current desktop. In other words, you need to run a new process in another session.
It sounds like an easy task. Yes, you can change a window station and a desktop, but unfortunately, there is no option to specify a session ID to CreateProcess functions. However Windows security token is always related to one session and fortunately we can change the session for the security token. Therefore if we create a new token for our user (for example using LogonUser), then change a session for this token using SetTokenInformation function and finally, call CreateProcessAsUser, the new process will be created in the appropriate session.
Again you can use DevxExec. It has a special parameter /sessionid to specify the session you what to run a new process in. For example this command could start a command shell for a logged in user under Administrator.
DevxExec.exe /user: Marcus /password:qwerty /sessionid:1 “cmd.exe”
How to run an application for a user from the Windows service (part 2)
Here is another scenario. User Marcus has logged to the computer. And you want your service to start an application as Annette that Marcus can work with on his current desktop. The solution above will work in that scenario too, but only if Annette is a member of the local “Administrators” group. Otherwise she will not have access to the Marcus’s desktop and windows station.
So our service (that runs in session = 0) need to give permission to Annette to Marcus’s desktop (session ≠ 0). The problem is how to get a handle of desktop that lives in another session. If you find out how to do it please let me know. Unfortunately I haven’t found how to do it but here is a workaround solution:
- Get a primary process (OpenProcessToken).
- Create a primary copy of this token (DuplicateTokenEx)
- Modify session id of the replica (SetTokenInformation).
- Create a new temporary process using that taken with CreateProcessAsUser.
- This process will be run into the Marcus’s session therefore it could access his window station and his desktop and give permission to Annette.
DevxExec works approximately that way so you can create a process for either user in either session.
To be continued…
Mike Makarov
Developex CTO