vb.netservicesysteminteractivecreateprocessasuser

Using a vb.net application running as SYSTEM, how do I start a detached process for each logged on user?


After weeks of research on this topic I've finally decided to start a thread of my own in hope there is someone out there with experience that can help. I've scoured the internet trying to understand the various coding examples out there, but I've come up short trying to put a working solution together. Let me start with some background--

Background:
I have a vb.net application that is getting delivered to Windows Servers and PCs in my organization using CA IT Client Manager (ITCM). Much like Microsoft SCCM, CA ITCM has an agent service running as SYSTEM on each PC. Hence, when my application gets delivered and executed on the target PC, it's running in the "NT Authority\SYSTEM" context.

The Problem:
During the initial phase of my application, there's a process running in the context of each logged in user that I need to stop. At the end of my applications execution, I have a requirement of restarting this process for each logged on user to prevent them from having to log off and back on again. The process I'm stopping is actually a system tray process that the user can interact with on their desktop.

Chasing a VB.NET Solution:
Researching endlessly on the internet, it seems there is no native .NET solution for this problem without having the password for each logged on user or prompting the user to enter some credentials. Since this is not an option for me, I need to find a way to start a process without having to know or require the logged on users credentials.

Researching this avenue led me to the CreateProcessAsUser Windows API function. From what I understand, I can do something along these lines -- (see below)

Notes:
This is my first time using unmanaged code calls in VB.NET to Windows APIs. There's a lot of ambiguity around the constants, enumerations and function declarations as I pieced the code together from various postings. Please do let me know if you notice any errors in any of these declarations. I have many questions about when a datatype needs to be "marshaled" as a different type. Please read carefully!!

Since there were countless examples from similar postings, I tried to follow the MSDN example from the CreateProcessAsUser page:

MSDN Link:
http://msdn.microsoft.com/en-us/library/windows/desktop/ms682429(v=vs.85).aspx

Example Link:
http://msdn.microsoft.com/en-us/library/windows/desktop/aa379608(v=vs.85).aspx

In addition to reviewing the validity of each Windows API call, please review the overall order of operations and let me know if I'm over-complicating this or even possibly missing something. The only code I have not implemented from the Microsoft example is allowing each SID full access to the interactive windows station, followed by allowing the SID full access to the interactive desktop. Maybe I'm wrong, but I figure each user should already have access to their interactive desktop already!

Public Class WindowsAPI

    Private Const SE_CREATE_TOKEN_NAME As String = "SeCreateTokenPrivilege"
    Private Const SE_ASSIGNPRIMARYTOKEN_NAME = "SeAssignPrimaryTokenPrivilege"
    Private Const SE_LOCK_MEMORY_NAME = "SeLockMemoryPrivilege"
    Private Const SE_INCREASE_QUOTA_NAME = "SeIncreaseQuotaPrivilege"
    Private Const SE_UNSOLICITED_INPUT_NAME = "SeUnsolicitedInputPrivilege"
    Private Const SE_MACHINE_ACCOUNT_NAME = "SeMachineAccountPrivilege"
    Private Const SE_TCB_NAME = "SeTcbPrivilege"
    Private Const SE_SECURITY_NAME = "SeSecurityPrivilege"
    Private Const SE_TAKE_OWNERSHIP_NAME = "SeTakeOwnershipPrivilege"
    Private Const SE_LOAD_DRIVER_NAME = "SeLoadDriverPrivilege"
    Private Const SE_SYSTEM_PROFILE_NAME = "SeSystemProfilePrivilege"
    Private Const SE_SYSTEMTIME_NAME = "SeSystemtimePrivilege"
    Private Const SE_PROF_SINGLE_PROCESS_NAME = "SeProfileSingleProcessPrivilege"
    Private Const SE_INC_BASE_PRIORITY_NAME = "SeIncreaseBasePriorityPrivilege"
    Private Const SE_CREATE_PAGEFILE_NAME = "SeCreatePagefilePrivilege"
    Private Const SE_CREATE_PERMANENT_NAME = "SeCreatePermanentPrivilege"
    Private Const SE_BACKUP_NAME = "SeBackupPrivilege"
    Private Const SE_RESTORE_NAME = "SeRestorePrivilege"
    Private Const SE_SHUTDOWN_NAME = "SeShutdownPrivilege"
    Private Const SE_DEBUG_NAME = "SeDebugPrivilege"
    Private Const SE_AUDIT_NAME = "SeAuditPrivilege"
    Private Const SE_SYSTEM_ENVIRONMENT_NAME = "SeSystemEnvironmentPrivilege"
    Private Const SE_CHANGE_NOTIFY_NAME = "SeChangeNotifyPrivilege"
    Private Const SE_REMOTE_SHUTDOWN_NAME = "SeRemoteShutdownPrivilege"
    Private Const SE_UNDOCK_NAME = "SeUndockPrivilege"
    Private Const SE_SYNC_AGENT_NAME = "SeSyncAgentPrivilege"
    Private Const SE_ENABLE_DELEGATION_NAME = "SeEnableDelegationPrivilege"
    Private Const SE_MANAGE_VOLUME_NAME = "SeManageVolumePrivilege"
    Private Const SE_IMPERSONATE_NAME = "SeImpersonatePrivilege"
    Private Const SE_CREATE_GLOBAL_NAME = "SeCreateGlobalPrivilege"
    Private Const SE_PRIVILEGE_ENABLED As Integer = &H2

    Private Enum WindowShowStyle As UInteger
        Hide = 0
        ShowNormal = 1
        ShowMinimized = 2
        ShowMaximized = 3
        Maximize = 3
        ShowNormalNoActivate = 4
        Show = 5
        Minimize = 6
        ShowMinNoActivate = 7
        ShowNoActivate = 8
        Restore = 9
        ShowDefault = 10
        ForceMinimized = 11
    End Enum

    Private Enum STARTF As Integer
        STARTF_USESHOWWINDOW = &H1
        STARTF_USESIZE = &H2
        STARTF_USEPOSITION = &H4
        STARTF_USECOUNTCHARS = &H8
        STARTF_USEFILLATTRIBUTE = &H10
        STARTF_RUNFULLSCREEN = &H20
        STARTF_FORCEONFEEDBACK = &H40
        STARTF_FORCEOFFFEEDBACK = &H80
        STARTF_USESTDHANDLES = &H100
        STARTF_USEHOTKEY = &H200
    End Enum

    Private Enum CreateProcessFlags
        DEBUG_PROCESS = &H1
        DEBUG_ONLY_THIS_PROCESS = &H2
        CREATE_SUSPENDED = &H4
        DETACHED_PROCESS = &H8
        CREATE_NEW_CONSOLE = &H10
        NORMAL_PRIORITY_CLASS = &H20
        IDLE_PRIORITY_CLASS = &H40
        HIGH_PRIORITY_CLASS = &H80
        REALTIME_PRIORITY_CLASS = &H100
        CREATE_NEW_PROCESS_GROUP = &H200
        CREATE_UNICODE_ENVIRONMENT = &H400
        CREATE_SEPARATE_WOW_VDM = &H800
        CREATE_SHARED_WOW_VDM = &H1000
        CREATE_FORCEDOS = &H2000
        BELOW_NORMAL_PRIORITY_CLASS = &H4000
        ABOVE_NORMAL_PRIORITY_CLASS = &H8000
        INHERIT_PARENT_AFFINITY = &H10000
        INHERIT_CALLER_PRIORITY = &H20000
        CREATE_PROTECTED_PROCESS = &H40000
        EXTENDED_STARTUPINFO_PRESENT = &H80000
        PROCESS_MODE_BACKGROUND_BEGIN = &H100000
        PROCESS_MODE_BACKGROUND_END = &H200000
        CREATE_BREAKAWAY_FROM_JOB = &H1000000
        CREATE_PRESERVE_CODE_AUTHZ_LEVEL = &H2000000
        CREATE_DEFAULT_ERROR_MODE = &H4000000
        CREATE_NO_WINDOW = &H8000000
        PROFILE_USER = &H10000000
        PROFILE_KERNEL = &H20000000
        PROFILE_SERVER = &H40000000
        CREATE_IGNORE_SYSTEM_DEFAULT = &H80000000
    End Enum

    Private Enum ACCESS_MASK
        DELETE = &H10000
        READ_CONTROL = &H20000
        WRITE_DAC = &H40000
        WRITE_OWNER = &H80000
        SYNCHRONIZE = &H100000
        STANDARD_RIGHTS_REQUIRED = &HF0000
        STANDARD_RIGHTS_READ = &H20000
        STANDARD_RIGHTS_WRITE = &H20000
        STANDARD_RIGHTS_EXECUTE = &H20000
        STANDARD_RIGHTS_ALL = &H1F0000
        SPECIFIC_RIGHTS_ALL = &HFFFF
        ACCESS_SYSTEM_SECURITY = &H1000000
        MAXIMUM_ALLOWED = &H2000000
        GENERIC_READ = &H80000000
        GENERIC_WRITE = &H40000000
        GENERIC_EXECUTE = &H20000000
        GENERIC_ALL = &H10000000
        DESKTOP_READOBJECTS = &H1
        DESKTOP_CREATEWINDOW = &H2
        DESKTOP_CREATEMENU = &H4
        DESKTOP_HOOKCONTROL = &H8
        DESKTOP_JOURNALRECORD = &H10
        DESKTOP_JOURNALPLAYBACK = &H20
        DESKTOP_ENUMERATE = &H40
        DESKTOP_WRITEOBJECTS = &H80
        DESKTOP_SWITCHDESKTOP = &H100
        WINSTA_ENUMDESKTOPS = &H1
        WINSTA_READATTRIBUTES = &H2
        WINSTA_ACCESSCLIPBOARD = &H4
        WINSTA_CREATEDESKTOP = &H8
        WINSTA_WRITEATTRIBUTES = &H10
        WINSTA_ACCESSGLOBALATOMS = &H20
        WINSTA_EXITWINDOWS = &H40
        WINSTA_ENUMERATE = &H100
        WINSTA_READSCREEN = &H200
        WINSTA_ALL_ACCESS = &H37F
    End Enum

    <StructLayout(LayoutKind.Sequential)>
    Private Structure PROCESS_INFORMATION
        Public hProcess As IntPtr
        Public hThread As IntPtr
        Public dwProcessId As System.UInt32
        Public dwThreadId As System.UInt32
    End Structure

    <StructLayout(LayoutKind.Sequential)>
    Private Structure SECURITY_ATTRIBUTES
        Public nLength As System.UInt32
        Public lpSecurityDescriptor As IntPtr
        Public bInheritHandle As Boolean
    End Structure

    <StructLayout(LayoutKind.Sequential)>
    Private Structure STARTUPINFO
        Public cb As System.UInt32
        Public lpReserved As String
        Public lpDesktop As String
        Public lpTitle As String
        Public dwX As System.UInt32
        Public dwY As System.UInt32
        Public dwXSize As System.UInt32
        Public dwYSize As System.UInt32
        Public dwXCountChars As System.UInt32
        Public dwYCountChars As System.UInt32
        Public dwFillAttribute As System.UInt32
        Public dwFlags As System.UInt32
        Public wShowWindow As Short
        Public cbReserved2 As Short
        Public lpReserved2 As IntPtr
        Public hStdInput As IntPtr
        Public hStdOutput As IntPtr
        Public hStdError As IntPtr
    End Structure

    Private Enum SECURITY_IMPERSONATION_LEVEL
        SecurityAnonymous = 0
        SecurityIdentification = 1
        SecurityImpersonation = 2
        SecurityDelegation = 3
    End Enum

    Private Enum TOKEN_TYPE
        TokenPrimary = 1
        TokenImpersonation = 2
    End Enum

    Structure LUID
        Public LowPart As UInt32
        Public HighPart As Integer
    End Structure

    Structure TOKEN_PRIVILEGES
        Public PrivilegeCount As Integer
        Public TheLuid As LUID
        Public Attributes As Integer
    End Structure

    Enum TOKEN_INFORMATION_CLASS
        TokenUser = 1
        TokenGroups
        TokenPrivileges
        TokenOwner
        TokenPrimaryGroup
        TokenDefaultDacl
        TokenSource
        TokenType
        TokenImpersonationLevel
        TokenStatistics
        TokenRestrictedSids
        TokenSessionId
        TokenGroupsAndPrivileges
        TokenSessionReference
        TokenSandBoxInert
        TokenAuditPolicy
        TokenOrigin
        TokenElevationType
        TokenLinkedToken
        TokenElevation
        TokenHasRestrictions
        TokenAccessInformation
        TokenVirtualizationAllowed
        TokenVirtualizationEnabled
        TokenIntegrityLevel
        TokenUIAccess
        TokenMandatoryPolicy
        TokenLogonSid
        MaxTokenInfoClass
    End Enum

    <StructLayoutAttribute(LayoutKind.Sequential)>
    Public Structure SECURITY_DESCRIPTOR
        Public revision As Byte
        Public size As Byte
        Public control As Short
        Public owner As IntPtr
        Public group As IntPtr
        Public sacl As IntPtr
        Public dacl As IntPtr
    End Structure

    <DllImport("advapi32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
    Private Shared Function AdjustTokenPrivileges(ByVal TokenHandle As IntPtr,
                                                  ByVal DisableAllPrivileges As Boolean,
                                                  ByRef NewState As TOKEN_PRIVILEGES,
                                                  ByVal BufferLengthInBytes As UInt32,
                                                  ByRef PreviousState As TOKEN_PRIVILEGES,
                                                  ByRef ReturnLengthInBytes As UInt32) As Boolean
    End Function

    <DllImport("advapi32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
    Private Shared Function CreateProcessAsUser(ByVal hToken As IntPtr,
                                                ByVal lpApplicationName As String,
                                                ByVal lpCommandLine As String,
                                                ByRef lpProcessAttributes As SECURITY_ATTRIBUTES,
                                                ByRef lpThreadAttributes As SECURITY_ATTRIBUTES,
                                                ByVal bInheritHandles As Boolean,
                                                ByVal dwCreationFlags As UInteger,
                                                ByVal lpEnvironment As IntPtr,
                                                ByVal lpCurrentDirectory As String,
                                                ByRef lpStartupInfo As STARTUPINFO,
                                                ByRef lpProcessInformation As PROCESS_INFORMATION) As Boolean
    End Function

    <DllImport("advapi32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
    Private Shared Function DuplicateTokenEx(ByVal hExistingToken As IntPtr,
                                             ByVal dwDesiredAccess As UInteger,
                                             ByRef lpTokenAttributes As SECURITY_ATTRIBUTES,
                                             ByVal ImpersonationLevel As SECURITY_IMPERSONATION_LEVEL,
                                             ByVal TokenType As TOKEN_TYPE,
                                             ByRef phNewToken As IntPtr) As Boolean
    End Function

    <DllImport("advapi32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
    Private Shared Function ImpersonateLoggedOnUser(ByVal hToken As IntPtr) As Boolean
    End Function

    <DllImport("advapi32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
    Private Shared Function OpenProcessToken(ByVal ProcessHandle As IntPtr,
                                             ByVal DesiredAccess As Integer,
                                             ByRef TokenHandle As IntPtr) As Boolean
    End Function

    <DllImport("advapi32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
    Private Shared Function RevertToSelf() As Boolean
    End Function

    <DllImport("kernel32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
    Private Shared Function CloseHandle(ByVal hObject As IntPtr) As Boolean
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
    Private Shared Function GetProcessWindowStation() As IntPtr
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
    Private Shared Function OpenDesktop(ByVal lpszDesktop As String,
                                        ByVal dwFlags As Integer,
                                        ByVal fInderit As Boolean,
                                        ByVal dwDesiredAccess As Integer) As IntPtr
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
    Private Shared Function OpenWindowStation(ByVal lpszWinSta As String,
                                              ByVal fInherit As Boolean,
                                              ByVal dwDesiredAccess As ACCESS_MASK) As IntPtr
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
    Private Shared Function SetProcessWindowStation(ByVal hWinSta As IntPtr) As Boolean
    End Function

    Public Shared Function LaunchProcess(ByVal CmdLine As String) As Boolean

        ' Declare and initialize variables
        Dim ExplorerProcesses As Process()
        Dim UserTokenHandle As IntPtr
        Dim PrimaryTokenHandle As IntPtr
        Dim CurrentWinStationHandle As IntPtr
        Dim InteractiveWinStationHandle As IntPtr
        Dim InteractiveDesktopHandle As IntPtr
        Dim StartupInfo As STARTUPINFO
        Dim ProcessInfo As PROCESS_INFORMATION

        ' Get all explorer.exe IDs
        ExplorerProcesses = Process.GetProcessesByName("explorer")

        ' Verify explorers were found
        If ExplorerProcesses.Length = 0 Then

            ' Return
            Return True

        End If

        ' Iterate each explorer.exe process
        For Each ExplorerProcess As Process In ExplorerProcesses

            ' Get the user token handle address (Query access level)
            If OpenProcessToken(ExplorerProcess.Handle, TokenAccessLevels.MaximumAllowed, UserTokenHandle) = False Then

                ' Do some error handling

                ' Iterate the next process
                Continue For

            End If

            ' Get a primary token
            If DuplicateTokenEx(UserTokenHandle,
                                TokenAccessLevels.MaximumAllowed,
                                Nothing,
                                SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation,
                                TOKEN_TYPE.TokenPrimary,
                                PrimaryTokenHandle) = False Then

                ' Do some error handling

                ' Iterate the next process
                Continue For

            End If

            ' Save a handle to the current window station
            CurrentWinStationHandle = GetProcessWindowStation()

            ' Check for valid handle to the windows station
            If CurrentWinStationHandle = IntPtr.Zero Then

                ' Do some error handling

                ' Iterate the next process
                Continue For

            End If

            ' Get a handle to the interactive window station
            InteractiveWinStationHandle = OpenWindowStation("winsta0", False, ACCESS_MASK.READ_CONTROL Or ACCESS_MASK.WRITE_DAC)

            ' Check for a valid handle
            If InteractiveWinStationHandle = Nothing Then

                ' Do some error handling

                ' Iterate the next user
                Continue For

            End If

            ' To get the correct default desktop, set the caller's window station to the interactive window station
            If SetProcessWindowStation(InteractiveWinStationHandle) = False Then

                ' Do some error handling

                ' Iterate the next user
                Continue For

            End If

            ' Get handle to interactive desktop
            InteractiveDesktopHandle = OpenDesktop("default",
                                                   0,
                                                   False,
                                                   ACCESS_MASK.READ_CONTROL Or
                                                   ACCESS_MASK.WRITE_DAC Or
                                                   ACCESS_MASK.DESKTOP_WRITEOBJECTS Or
                                                   ACCESS_MASK.DESKTOP_READOBJECTS)

            ' Restore the caller's window station
            If SetProcessWindowStation(CurrentWinStationHandle) = False Then

                ' Do some error handling

                ' Iterate the next user
                Continue For

            End If

            ' Check for a valid handle
            If InteractiveDesktopHandle = IntPtr.Zero Then

                ' Do some error handling

                ' Iterate the next user
                Continue For

            End If

            ' Initialize process and startup info
            ProcessInfo = New PROCESS_INFORMATION
            StartupInfo = New STARTUPINFO
            StartupInfo.cb = Marshal.SizeOf(StartupInfo)
            StartupInfo.lpDesktop = "winsta0\default"

            ' Impersonate client to ensure access to executable file
            If ImpersonateLoggedOnUser(PrimaryTokenHandle) = False Then

                ' Do some error handling

                ' Iterate the next user
                Continue For

            End If

            ' Launch the process in the client's logon session
            If CreateProcessAsUser(PrimaryTokenHandle,
                                   Nothing,
                                   CmdLine,
                                   Nothing,
                                   Nothing,
                                   False,
                                   CreateProcessFlags.CREATE_UNICODE_ENVIRONMENT Or
                                   CreateProcessFlags.NORMAL_PRIORITY_CLASS Or,
                                   Nothing,
                                   Nothing,
                                   StartupInfo,
                                   ProcessInfo) = False Then

                ' Do some error handling

                ' Iterate the next user
                Continue For

            End If

            ' End impersonation of client
            If RevertToSelf() = False Then

                ' Do some error handling

                ' Iterate the next user
                Continue For

            End If

        Next

        ' Check for open handle
        If Not PrimaryTokenHandle = IntPtr.Zero Then

            ' Close the handle
            CloseHandle(PrimaryTokenHandle)

        End If

        ' Return
        Return True

    End Function

End Class

My Result:
Currently, I'm testing calls to the LaunchProcess function from my Administrator account (running my solution from Visual Studio 2010 on my local machine) and by delivering the application through ITCM (running from the SYSTEM account on my local machine). In both cases I get the specified process to start in the users session, but with the following error:

"The application was unable to start correctly (0xc0000142). Click OK to close the application."

I'm hoping someone can review my code and point me in the right direction. Otherwise, please advise on how I can possibly debug what the heck is going wrong here.

Many thanks for all your input in advance.


Solution

  • The Solution:

    Before posting the full code solution, I wanted to share how I found my answer. After revisiting the MSDN article on the CreateProcessAsUser API function, I realized I needed to verify if my process actually held the required privileges mentioned by the article:

    SE_INCREASE_QUOTA_NAME

    SE_ASSIGNPRIMARYTOKEN_NAME

    Also, not mentioned in the article, but perhaps critical to some of the other related Windows API calls for looking up and adjusting token privileges to enable the above privileges is:

    SE_TCB_NAME

    Recall my application is getting delivered using CA's IT Client Manager (ITCM) software to target Windows Servers and PCs. The core CA ITCM agent service logs on as "Local System", but its plugins that perform all the dirty work startup and run as the SYSTEM account. Apparently there is a HUGE difference between the SYSTEM account and the "Local System" account.

    Using SysInternal's Process Explorer tool, I was able to inspect my application and find that it did not hold all of the required privileges. It's been a while since I ran the test, so I forget which privilege was actually not held.

    The initial mistake I made was writing unmanaged Windows API function calls to try to enable the missing privilege, but unfortunately it doesn't work that way. Either your process has the privilege or it doesn't, period. If it holds the privilege, then your only responsibility it to make sure the privilege is enabled.

    To overcome this, I had to use a different approach. In order to obtain the privileges I required, my application needed to be installed and executed as a service, so it could be running as the "Local System" account. However, to redesign the entire application as an installable service just to satisfy this one simple requirement didn't make sense.

    Instead, I created a second VB.NET project, which is the code I'm posting. The second project is a simple Windows Service that takes startup parameters. The first startup parameter is the application you would like launched for each logged on user. Any remaining startup parameters are passed as startup switches to the application you specify :-)

    Armed with a service that now held the proper privileges and can dynamically receive startup parameters for specifying exactly what you want launched for each logged in user, I embedded the resultant executable in my first project.

    My first project, running as the SYSTEM account, HAS the rights/permissions/privileges to install a new system services. It also has the rights to start that system services, passing the parameters I needed to start the tray service for each logged in user in the system. Problem SOLVED!

    Here's the code for my Windows Service--

    LaunchService.vb:

    '****************************** Class Header *******************************\
    ' Project Name: LaunchService
    ' Class Name:   LaunchService
    ' File Name:    LaunchService.vb
    ' Author:       fonbr01
    ' 
    ' THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    ' EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    ' MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
    ' IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
    ' OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
    ' ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
    ' OTHER DEALINGS IN THE SOFTWARE.
    '***************************************************************************/
    
    Public Class LaunchService
    
        Protected Overrides Sub OnStart(ByVal args() As String)
    
            ' Local Variables
            Dim AppName As String
    
            ' Get the application name
            AppName = args(0)
            args(0) = " "
    
            ' Check for additional arguments
            If args.Length > 1 Then
    
                ' Shift the arguments
                For i As Integer = 1 To args.Length - 1
    
                    ' Swap the args
                    args(i - 1) = args(i)
    
                Next
    
                ' Remove the last argument
                args(args.Length - 1) = ""
    
            End If
    
            ' Launch the App for all users
            WindowsAPI.LaunchProcess(AppName, args)
    
        End Sub
    
        Protected Overrides Sub OnStop()
    
        End Sub
    
    End Class
    

    WindowsAPI.vb:

    '****************************** Class Header *******************************\
    ' Project Name: LaunchService
    ' Class Name:   WindowsAPI
    ' File Name:    WindowsAPI.vb
    ' Author:       fonbr01
    ' 
    ' THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    ' EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    ' MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
    ' IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
    ' OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
    ' ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
    ' OTHER DEALINGS IN THE SOFTWARE.
    '***************************************************************************/
    
    ' Imports
    Imports Microsoft.Win32.SafeHandles
    Imports System.ComponentModel
    Imports System.Runtime.InteropServices
    Imports System.Security.Principal
    Imports System.Diagnostics
    
    ' Windows API Class
    Public Class WindowsAPI
    
        ' *************************
        ' * Windows API Functions
        ' *************************
    
        <DllImport("advapi32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
        Private Shared Function AdjustTokenPrivileges(
            <[In]()> ByVal TokenHandle As SafeTokenHandle,
            <[In](), MarshalAs(UnmanagedType.Bool)> ByVal DisableAllPrivileges As Boolean,
            <[In]()> ByRef NewState As TOKEN_PRIVILEGES,
            <[In]()> ByVal BufferLengthInBytes As UInt32,
            <Out()> ByRef PreviousState As TOKEN_PRIVILEGES,
            <Out()> ByRef ReturnLengthInBytes As UInt32) As <MarshalAs(UnmanagedType.Bool)> Boolean
        End Function
    
        <DllImport("advapi32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
        Private Shared Function CreateProcessAsUser(
            <[In]()> ByVal hToken As SafeTokenHandle,
            <[In](), MarshalAs(UnmanagedType.LPWStr)> ByVal lpApplicationName As String,
            <[In](), Out(), MarshalAs(UnmanagedType.LPWStr)> ByVal lpCommandLine As String,
            <[In]()> ByRef lpProcessAttributes As SECURITY_ATTRIBUTES,
            <[In]()> ByRef lpThreadAttributes As SECURITY_ATTRIBUTES,
            <[In](), MarshalAs(UnmanagedType.Bool)> ByVal bInheritHandles As Boolean,
            <[In]()> ByVal dwCreationFlags As UInteger,
            <[In]()> ByVal lpEnvironment As IntPtr,
            <[In](), MarshalAs(UnmanagedType.LPWStr)> ByVal lpCurrentDirectory As String,
            <[In]()> ByRef lpStartupInfo As STARTUPINFO,
            <Out()> ByRef lpProcessInformation As PROCESS_INFORMATION) As <MarshalAs(UnmanagedType.Bool)> Boolean
        End Function
    
        <DllImport("advapi32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
        Private Shared Function DuplicateToken(
            <[In]()> ByVal ExistingTokenHandle As SafeTokenHandle,
            <[In]()> ByVal ImpersonationLevel As SECURITY_IMPERSONATION_LEVEL,
            <Out()> ByRef DuplicateTokenHandle As SafeTokenHandle) As <MarshalAs(UnmanagedType.Bool)> Boolean
        End Function
    
        <DllImport("advapi32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
        Private Shared Function DuplicateTokenEx(
            <[In]()> ByVal hExistingToken As IntPtr,
            <[In]()> ByVal dwDesiredAccess As UInteger,
            <[In]()> ByRef lpTokenAttributes As SECURITY_ATTRIBUTES,
            <[In]()> ByVal ImpersonationLevel As SECURITY_IMPERSONATION_LEVEL,
            <[In]()> ByVal TokenType As TOKEN_TYPE,
            <Out()> ByRef phNewToken As SafeTokenHandle) As <MarshalAs(UnmanagedType.Bool)> Boolean
        End Function
    
        <DllImport("advapi32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
        Private Shared Function LookupPrivilegeValue(
            <[In](), MarshalAs(UnmanagedType.LPWStr)> ByVal lpSystemName As String,
            <[In](), MarshalAs(UnmanagedType.LPWStr)> ByVal lpName As String,
            <Out()> ByRef lpLuid As LUID) As <MarshalAs(UnmanagedType.Bool)> Boolean
        End Function
    
        <DllImport("advapi32.dll", CharSet:=CharSet.Auto, SetLastError:=True)> _
        Private Shared Function OpenProcessToken(
            <[In]()> ByVal hProcess As IntPtr,
            <[In]()> ByVal desiredAccess As UInt32,
            <Out()> ByRef hToken As SafeTokenHandle) As <MarshalAs(UnmanagedType.Bool)> Boolean
        End Function
    
    
        ' *************************
        ' * Structures
        ' *************************
    
        <StructLayout(LayoutKind.Sequential)>
        Private Structure PROCESS_INFORMATION
            Public hProcess As IntPtr
            Public hThread As IntPtr
            Public dwProcessId As System.UInt32
            Public dwThreadId As System.UInt32
        End Structure
    
        <StructLayout(LayoutKind.Sequential)>
        Private Structure SECURITY_ATTRIBUTES
            Public nLength As System.UInt32
            Public lpSecurityDescriptor As IntPtr
            Public bInheritHandle As Boolean
        End Structure
    
        <StructLayout(LayoutKind.Sequential)>
        Private Structure STARTUPINFO
            Public cb As System.UInt32
            Public lpReserved As String
            Public lpDesktop As String
            Public lpTitle As String
            Public dwX As System.UInt32
            Public dwY As System.UInt32
            Public dwXSize As System.UInt32
            Public dwYSize As System.UInt32
            Public dwXCountChars As System.UInt32
            Public dwYCountChars As System.UInt32
            Public dwFillAttribute As System.UInt32
            Public dwFlags As System.UInt32
            Public wShowWindow As Short
            Public cbReserved2 As Short
            Public lpReserved2 As IntPtr
            Public hStdInput As IntPtr
            Public hStdOutput As IntPtr
            Public hStdError As IntPtr
        End Structure
    
        Private Structure LUID
            Public LowPart As UInt32
            Public HighPart As Integer
        End Structure
    
        Private Structure LUID_AND_ATTRIBUTES
            Public Luid As LUID
            Public Attributes As Integer
        End Structure
    
        Private Structure TOKEN_PRIVILEGES
            Public PrivilegeCount As UInt32
            <MarshalAs(UnmanagedType.ByValArray)> Public Privileges() As LUID_AND_ATTRIBUTES
        End Structure
    
    
        ' ******************************
        ' * Enumerations
        ' ******************************
    
        Private Enum CreateProcessFlags
            DEBUG_PROCESS = &H1
            DEBUG_ONLY_THIS_PROCESS = &H2
            CREATE_SUSPENDED = &H4
            DETACHED_PROCESS = &H8
            CREATE_NEW_CONSOLE = &H10
            NORMAL_PRIORITY_CLASS = &H20
            IDLE_PRIORITY_CLASS = &H40
            HIGH_PRIORITY_CLASS = &H80
            REALTIME_PRIORITY_CLASS = &H100
            CREATE_NEW_PROCESS_GROUP = &H200
            CREATE_UNICODE_ENVIRONMENT = &H400
            CREATE_SEPARATE_WOW_VDM = &H800
            CREATE_SHARED_WOW_VDM = &H1000
            CREATE_FORCEDOS = &H2000
            BELOW_NORMAL_PRIORITY_CLASS = &H4000
            ABOVE_NORMAL_PRIORITY_CLASS = &H8000
            INHERIT_PARENT_AFFINITY = &H10000
            INHERIT_CALLER_PRIORITY = &H20000
            CREATE_PROTECTED_PROCESS = &H40000
            EXTENDED_STARTUPINFO_PRESENT = &H80000
            PROCESS_MODE_BACKGROUND_BEGIN = &H100000
            PROCESS_MODE_BACKGROUND_END = &H200000
            CREATE_BREAKAWAY_FROM_JOB = &H1000000
            CREATE_PRESERVE_CODE_AUTHZ_LEVEL = &H2000000
            CREATE_DEFAULT_ERROR_MODE = &H4000000
            CREATE_NO_WINDOW = &H8000000
            PROFILE_USER = &H10000000
            PROFILE_KERNEL = &H20000000
            PROFILE_SERVER = &H40000000
            CREATE_IGNORE_SYSTEM_DEFAULT = &H80000000
        End Enum
    
        Private Enum SECURITY_IMPERSONATION_LEVEL
            SecurityAnonymous = 0
            SecurityIdentification
            SecurityImpersonation
            SecurityDelegation
        End Enum
    
        Private Enum TOKEN_TYPE
            TokenPrimary = 1
            TokenImpersonation = 2
        End Enum
    
    
        ' ******************************
        ' * Constants
        ' ******************************
    
        Private Const SE_ASSIGNPRIMARYTOKEN_NAME As String = "SeAssignPrimaryTokenPrivilege"
        Private Const SE_INCREASE_QUOTA_NAME As String = "SeIncreaseQuotaPrivilege"
        Private Const SE_TCB_NAME As String = "SeTcbPrivilege"
        Private Const SE_PRIVILEGE_ENABLED As UInt32 = &H2
    
    
        ' ******************************
        ' * Safe Token Handle Class
        ' ******************************
    
        Private Class SafeTokenHandle
            Inherits SafeHandleZeroOrMinusOneIsInvalid
    
            Private Sub New()
                MyBase.New(True)
            End Sub
    
            Friend Sub New(ByVal handle As IntPtr)
                MyBase.New(True)
                MyBase.SetHandle(handle)
            End Sub
    
            <DllImport("kernel32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
            Friend Shared Function CloseHandle(ByVal handle As IntPtr) As Boolean
            End Function
    
            Protected Overrides Function ReleaseHandle() As Boolean
                Return SafeTokenHandle.CloseHandle(MyBase.handle)
            End Function
    
        End Class
    
    
        ' ******************************
        ' * Increase Privileges Function
        ' ******************************
    
        Public Shared Function IncreasePrivileges() As Boolean
    
            ' Local variables
            Dim hToken As SafeTokenHandle = Nothing
            Dim luid As LUID
            Dim NewState As TOKEN_PRIVILEGES
            NewState.PrivilegeCount = 1
            ReDim NewState.Privileges(0)
    
            ' Get current process token
            If OpenProcessToken(Diagnostics.Process.GetCurrentProcess.Handle, TokenAccessLevels.MaximumAllowed, hToken) = False Then
    
                ' Write debug
                WriteEvent("Error: Windows API OpenProcessToken function returns an error." + Environment.NewLine +
                           "Windows API error code: " + Marshal.GetLastWin32Error.ToString, EventLogEntryType.Error)
    
                ' Return
                Return False
    
            End If
    
            ' Lookup SeIncreaseQuotaPrivilege
            If Not LookupPrivilegeValue(Nothing, SE_INCREASE_QUOTA_NAME, luid) Then
    
                ' Write debug
                WriteEvent("Error: Windows API LookupPrivilegeValue function returns an error." + Environment.NewLine +
                           "Windows API error code: " + Marshal.GetLastWin32Error.ToString, EventLogEntryType.Error)
    
                ' Return
                Return False
    
            End If
    
            ' Enable SeIncreaseQuotaPrivilege
            NewState.Privileges(0).Luid = luid
            NewState.Privileges(0).Attributes = SE_PRIVILEGE_ENABLED
    
            ' Adjust the token privileges
            If Not AdjustTokenPrivileges(hToken, False, NewState, Marshal.SizeOf(NewState), Nothing, Nothing) Then
    
                ' Write debug
                WriteEvent("Error: Windows API AdjustTokenPrivileges function returns an error." + Environment.NewLine +
                           "Windows API error code: " + Marshal.GetLastWin32Error.ToString, EventLogEntryType.Error)
    
                ' Return
                Return False
    
            End If
    
            ' Lookup SeAssignPrimaryTokenPrivilege
            If Not LookupPrivilegeValue(Nothing, SE_ASSIGNPRIMARYTOKEN_NAME, luid) Then
    
                ' Write debug
                WriteEvent("Error: Windows API LookupPrivilegeValue function returns an error." + Environment.NewLine +
                           "Windows API error code: " + Marshal.GetLastWin32Error.ToString, EventLogEntryType.Error)
    
                ' Return
                Return False
    
            End If
    
            ' Enable SeAssignPrimaryTokenPrivilege
            NewState.Privileges(0).Luid = luid
            NewState.Privileges(0).Attributes = SE_PRIVILEGE_ENABLED
    
            ' Adjust the token privileges
            If Not AdjustTokenPrivileges(hToken, False, NewState, Marshal.SizeOf(NewState), Nothing, Nothing) Then
    
                ' Write debug
                WriteEvent("Error: Windows API AdjustTokenPrivileges function returns an error." + Environment.NewLine +
                           "Windows API error code: " + Marshal.GetLastWin32Error.ToString, EventLogEntryType.Error)
    
                ' Return
                Return False
    
            End If
    
            ' Lookup SeTcbPrivilege
            If Not LookupPrivilegeValue(Nothing, SE_TCB_NAME, luid) Then
    
                ' Write debug
                WriteEvent("Error: Windows API LookupPrivilegeValue function returns an error." + Environment.NewLine +
                           "Windows API error code: " + Marshal.GetLastWin32Error.ToString, EventLogEntryType.Error)
    
                ' Return
                Return False
    
            End If
    
            ' Enable SeTcbPrivilege
            NewState.Privileges(0).Luid = luid
            NewState.Privileges(0).Attributes = SE_PRIVILEGE_ENABLED
    
            ' Adjust the token privileges
            If Not AdjustTokenPrivileges(hToken, False, NewState, Marshal.SizeOf(NewState), Nothing, Nothing) Then
    
                ' Write debug
                WriteEvent("Error: Windows API AdjustTokenPrivileges function returns an error." + Environment.NewLine +
                           "Windows API error code: " + Marshal.GetLastWin32Error.ToString, EventLogEntryType.Error)
    
                ' Return
                Return False
    
            End If
    
            ' Return
            Return True
    
        End Function
    
    
        ' ******************************
        ' * Launch Process Sub
        ' ******************************
    
        Public Shared Sub LaunchProcess(ByVal CmdLine As String, ByVal args As String())
    
            ' Local variables
            Dim Arguments As String = ""
            Dim ExplorerProcesses As Process()
            Dim hToken As SafeTokenHandle = Nothing
            Dim principle As WindowsIdentity
            Dim phNewToken As SafeTokenHandle = Nothing
            Dim si As STARTUPINFO
            Dim pi As PROCESS_INFORMATION
    
            ' Process arguments
            For Each arg As String In args
    
                ' Build argument string
                Arguments += " " + arg
    
            Next
    
            ' Increase Privileges
            If IncreasePrivileges() = False Then
    
                ' Write debug
                WriteEvent("Warning: Failed to increase current process privileges.", EventLogEntryType.Warning)
    
            End If
    
            ' Get all explorer.exe IDs
            ExplorerProcesses = Process.GetProcessesByName("explorer")
    
            ' Verify explorers were found
            If ExplorerProcesses.Length = 0 Then
    
                ' Write debug
                WriteEvent("Warning: No explorer.exe processes found.", EventLogEntryType.Warning)
    
                ' Return
                Exit Sub
    
            End If
    
            ' Iterate each explorer.exe process
            For Each hProcess As Process In ExplorerProcesses
    
                ' Get the user token handle
                If OpenProcessToken(hProcess.Handle, TokenAccessLevels.MaximumAllowed, hToken) = False Then
    
                    ' Write debug
                    WriteEvent("Error: Windows API OpenProcessToken function returns an error." + Environment.NewLine +
                               "Windows API error code: " + Marshal.GetLastWin32Error.ToString, EventLogEntryType.Error)
    
                    ' Iterate the next process
                    Continue For
    
                End If
    
                ' Get the windows identity
                principle = New WindowsIdentity(hToken.DangerousGetHandle)
    
                ' Get a primary token
                If Not DuplicateTokenEx(hToken.DangerousGetHandle,
                    TokenAccessLevels.MaximumAllowed,
                    Nothing,
                    SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation,
                    TOKEN_TYPE.TokenPrimary,
                    phNewToken) Then
    
                    ' Write debug
                    WriteEvent("Error: Windows API DuplicateTokenEx function returns an error." + Environment.NewLine +
                               "Windows API error code: " + Marshal.GetLastWin32Error.ToString, EventLogEntryType.Error)
    
                    ' Iterate the next process
                    Continue For
    
                End If
    
                ' Initialize process and startup info
                pi = New PROCESS_INFORMATION
                si = New STARTUPINFO
                si.cb = Marshal.SizeOf(si)
                si.lpDesktop = Nothing
    
                ' Launch the process in the client's logon session
                If Not CreateProcessAsUser(phNewToken,
                    Nothing,
                    CmdLine + Arguments,
                    Nothing,
                    Nothing,
                    False,
                    CreateProcessFlags.CREATE_UNICODE_ENVIRONMENT,
                    Nothing,
                    Nothing,
                    si,
                    pi) Then
    
                    ' Write debug
                    WriteEvent("Error: Windows API CreateProcessAsUser function returns an error." + Environment.NewLine +
                               "Windows API error code: " + Marshal.GetLastWin32Error.ToString, EventLogEntryType.Error)
    
                Else
    
                    ' Write debug
                    WriteEvent("Created new user process: " + Environment.NewLine +
                               "User:     " + principle.Name + Environment.NewLine +
                               "Process:  " + CmdLine + Arguments + Environment.NewLine +
                               "PID:      " + pi.dwProcessId.ToString, EventLogEntryType.Information)
    
                End If
    
                ' Free resources
                hToken.Close()
                hToken = Nothing
                phNewToken.Close()
                phNewToken = Nothing
                principle = Nothing
                pi = Nothing
                si = Nothing
    
            Next
    
        End Sub
    
    
        ' ******************************
        ' * Write Event Log Sub
        ' ******************************
    
        Public Shared Sub WriteEvent(EventMessage As String, EntryType As EventLogEntryType)
    
            ' Check if event source exists
            If Not EventLog.SourceExists("WinOffline Launch Service") Then
    
                ' Create the event source
                EventLog.CreateEventSource("WinOffline Launch Service", "System")
    
            End If
    
            ' Write the message
            EventLog.WriteEntry("WinOffline Launch Service", EventMessage, EntryType)
    
        End Sub
    
    End Class
    

    Implementation:

    As mentioned above, I've taken the service executable from my second project, added it in Visual Studio as an "existing item" to my first project. I then changed the "build action" to embed the service executable into my first applications executable.

    When the first application executes on the target, it runs code to extract the embedded executable to the target machine. You can Google for that code, it's rather simplistic.

    After the embedded service executable is extracted to a path on the local system, here's the four "sc" commands I run--

    sc create <ServiceName> binpath= <Full Path to Service Executable> start= demand
    sc start <ServiceName> <Full Path to App to Launch for all Users> <Parameters>
    sc stop <ServiceName>
    sc delete <ServiceName>
    

    Note: In the first sc command remember to put spaces after the equal signs.

    Using sc commands is far simpler than using installutil.exe or creating a setup project to package the service in an MSI and install it. Just make sure you WAIT for each sc command to return before proceeding to the next one.

    Final Note:

    To all those that provided positive insight and feedback, thank you very much for all your help. It's for good people like yourselves that I gladly post my code. For all those who told me this couldn't be done or tried to assert it's somehow malicious to poke into users desktops and to run something, I politely encourage you to start thinking out of the box. I wouldn't want you working on my team!

    It's a slippery slope to walk through life with the mentality that just because something could have malicious or dangerous implications in the wrong hands that it shouldn't be accomplished at all. Your stinking thinking is contrary to all that's beautiful in the universe.