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

天大地大妈咪最大 提交于 2019-12-05 18:49:37

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!