● Launching a GUI application from a Windows Service using C#

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 method.CreateProcessAsUser

It can be challenging to piece together all of the things you need to call the 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.CreateProcessAsUser

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 class is used along with the public method to launch a process from a .NET Core Worker Service which is configured to run as a Windows Service.ProcessServicesStartProcessAsCurrentUser.

Interface

The class implements an interface that is documented below for reference.ProcessServices.

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 class contains the definition of all the constants, structs, and method imports that are needed to facilitate calls into the unmanaged code.ProcessServices

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 page contains Syntax, Members and Remarks sections that helpfully document each aspect of the struct.PROCESS_INFORMATION PROCESS_INFORMATION

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 attribute along with the keyword is used to indicate that the method signatures within the ‘Import’ sub-region are implemented within external DLLs (Dynamic Link Libraries)DllImport extern

Initialisation

The class features a default constructor and a second constructor that accepts a typed 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 object instance.ProcessServices ILogger ProcessServices

Public methods

The main public interface method is named 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.StartProcessAsCurrentUser

The 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.StartProcessAsCurrentUser

The third parameter of the method is also optional and allows a 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.StartProcessAsCurrentUser Process

At a high level, the method obtains a pointer to the user’s primary token by calling the private method. The private 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 to start the process as the currently logged-in user.StartProcessAsCurrentUser GetPrimaryTokenGet EnvironmentBlockLaunch Process

The code ensures that resources relating to the primary token and environment block are cleaned up properly by calling the unmanaged and methods respectively and doing so within a block. This is very important and is something that is frequently missed out when developers are attempting to smash unfamiliar code together.DestroyEnvironmentBlock CloseHandle try-finally

Private methods

The first private method that is called by the public method is the method which gets a pointer to a primary token based on the specified process.StartProcessAsCurrentUser GetPrimaryToken

The method calls the unmanaged method to open the access token for the specified process. The unmanaged 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 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.GetPrimaryToken (OpenProcessToken](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocesstoken) DuplicateTokenEx Marshal.GetLastWin32Error

The method calls the unmanaged method to get environment variables for a user based on the specified token. The method is called if the result returned by the method is false for error logging purposes.GetEnvironmentBlock CreateEnvironmentBlock Marshal.GetLastWin32Error CreateEnvironmentBlock

The method features several parameters; the command line, working directory, primary token, and environment block to use when creating the process. Within the method, the 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.LaunchProcess STARTUPINFO

After creating process and thread security attributes using the struct, the method is called. The 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.SECURITY_ATTRIBUTES CreateProcessAsUser(https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessasusera) CreateProcessAsUser

As per the method, the 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 method will be called to determine the error code to log.CreateEnvironmentBlock CreateProcessAsUser Marshal.GetLastWin32Error

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 command to set the ‘obj’ argument to ‘LocalSystem’, as shown below.sc

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 class and call the method with the appropriate parameter values within your Windows Service. A basic example of this is shown below for your reference.ProcessServices StartProcessAsCurrentUser

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 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.Start ProcessAsCurrentUser

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 class and automatically injects an instance as part of a Worker Service project that is configured to run as a Windows Service.ProcessServices ILogger

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 class which contains the public 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.ProcessServices StartProcessAsCurrentUser

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.

I hope you enjoyed this post! Comments are always welcome and I respond to all questions.

If you like my content and it helped you out, please check out the button below 🙂

From: 《Launching a GUI application from a Windows Service using C#》