How to Launch a Process as Another User from a LocalSystem Windows Service?

The core requirement is to run an EXE as a service under local system account which will execute another EXE under different windows user using saved credentials.

Approach 1:

Public Function CreateProcess( _
ByVal Domain As String, _
ByVal username As String, _
ByVal Password As String, ByVal ApplicationName As String, _
Optional ByVal ExecuteAsynch As Boolean = False, _
Optional ByVal Visible As Boolean = False) As Long
On Error GoTo CreateProcess_EH

    'Define local variables
    Dim strCommandLine      As String
    Dim strCurrentDirectory As String
    Dim suiStartUpInfo      As STARTUPINFO
    Dim piProcessInfo       As PROCESS_INFORMATION
    Dim lngExitCode         As Long
    Dim lngReturnValue      As Long
    Dim strMessage          As String
    'Initialize local variables
    strCommandLine = vbNullString               'Make sure all of your parameters are part of the ApplicationName
    strCurrentDirectory = vbNullString          'Use the standard, default assignment
    With suiStartUpInfo
        .cb = LenB(suiStartUpInfo)              'Size of the structure
        .lpReserved = API_NULL                  'Reserved
        .cbReserved2 = API_NULL                 'Reserved
        .dwFlags = STARTF_USESHOWWINDOW         'Use the wShowWindow parameter
        .wShowWindow = IIf(Visible, SW_SHOW, SW_HIDE)  'Hide or show any window that might be shown
    End With
    'Create the new process with the credentials provided
    If CreateProcessWithLogon(StrPtr(username), StrPtr(Domain), StrPtr(Password), _
                                LOGON_WITH_PROFILE, StrPtr(ApplicationName), StrPtr(strCommandLine), _
                                CREATE_DEFAULT_ERROR_MODE Or CREATE_NEW_CONSOLE Or CREATE_NEW_PROCESS_GROUP, _
                                ByVal 0&, StrPtr(strCurrentDirectory), suiStartUpInfo, piProcessInfo) = API_FAILURE Then
        'There is no pointin moving on if this failed
        'Unable to launch the new process
        CreateProcess = 0
        Exit Function
    Else
        CreateProcess = piProcessInfo.dwProcessId
    End If
    
    
    'Close the handles to the thread & process, since we don't use them
    'NOTE: Closing the handles of the main thread and the process do not terminate the process
    Call CloseHandle(piProcessInfo.hThread)
    Call CloseHandle(piProcessInfo.hProcess)
    
Exit Function

Approach 2:

Created windows scheduled task to run as different user & tried to trigger that scheduled task from Service process (Scheduled task wont run when the triggering application is running in Local System account)

Approach 3:

I then tried another approach which followed the following flow: authenticate the target user with LogonUser(), create a primary token using DuplicateTokenEx(), load the user's profile and environment, and finally call CreateProcessAsUser() to launch the executable under that user's account.

The main executable, which runs as a service under the LocalSystem account, is shown below:

Imports System
Imports System.Runtime.InteropServices
Imports System.Text
 
Public Class Form1
    ' CONSTANTS
    Const LOGON32_LOGON_INTERACTIVE As Integer = 2
    Const LOGON32_PROVIDER_DEFAULT As Integer = 0
 
    Const TOKEN_ASSIGN_PRIMARY As Integer = &H1
    Const TOKEN_DUPLICATE As Integer = &H2
    Const TOKEN_QUERY As Integer = &H8
    Const TOKEN_ALL_ACCESS As Integer = &HF01FF
 
    Const CREATE_NEW_CONSOLE As Integer = &H10
    Const CREATE_UNICODE_ENVIRONMENT As Integer = &H400
 
    Const SE_PRIVILEGE_ENABLED As Integer = &H2
    Const TOKEN_ADJUST_PRIVILEGES As Integer = &H20
    Const SE_BACKUP_NAME As String = "SeBackupPrivilege"
    Const SE_RESTORE_NAME As String = "SeRestorePrivilege"
    Const INFINITE As Integer = -1
 
    ' STRUCTURES
<StructLayout(LayoutKind.Sequential)>
    Public Structure STARTUPINFO
        Public cb As Integer
        Public lpReserved As String
        Public lpDesktop As String
        Public lpTitle As String
        Public dwX As Integer
        Public dwY As Integer
        Public dwXSize As Integer
        Public dwYSize As Integer
        Public dwXCountChars As Integer
        Public dwYCountChars As Integer
        Public dwFillAttribute As Integer
        Public dwFlags As Integer
        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
 
    <StructLayout(LayoutKind.Sequential)>
    Public Structure PROCESS_INFORMATION
        Public hProcess As IntPtr
        Public hThread As IntPtr
        Public dwProcessId As Integer
        Public dwThreadId As Integer
    End Structure
 
    <StructLayout(LayoutKind.Sequential)>
    Public Structure SECURITY_ATTRIBUTES
        Public nLength As Integer
        Public lpSecurityDescriptor As IntPtr
<MarshalAs(UnmanagedType.Bool)>
        Public bInheritHandle As Boolean
    End Structure
 
    <StructLayout(LayoutKind.Sequential)>
    Public Structure LUID
        Public LowPart As UInteger
        Public HighPart As Integer
    End Structure
 
    <StructLayout(LayoutKind.Sequential)>
    Public Structure LUID_AND_ATTRIBUTES
        Public Luid As LUID
        Public Attributes As Integer
    End Structure
 
    <StructLayout(LayoutKind.Sequential)>
    Public Structure TOKEN_PRIVILEGES
        Public PrivilegeCount As Integer
        Public Privileges As LUID_AND_ATTRIBUTES
    End Structure
 
    <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Auto)>
    Public Structure PROFILEINFO
        Public dwSize As Integer
        Public dwFlags As Integer
        Public lpUserName As String
        Public lpProfilePath As String
        Public lpDefaultPath As String
        Public lpServerName As String
        Public lpPolicyPath As String
        Public hProfile As IntPtr
    End Structure
 
    ' API DECLARATIONS
 
    <DllImport("advapi32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
    Public Shared Function LogonUser(
        lpszUsername As String,
        lpszDomain As String,
        lpszPassword As String,
        dwLogonType As Integer,
        dwLogonProvider As Integer,
        ByRef phToken As IntPtr) As Boolean
    End Function
 
    <DllImport("advapi32.dll", SetLastError:=True)>
    Public Shared Function DuplicateTokenEx(
        hExistingToken As IntPtr,
        dwDesiredAccess As Integer,
        lpTokenAttributes As IntPtr,
        ImpersonationLevel As Integer,
        TokenType As Integer,
        ByRef phNewToken As IntPtr) As Boolean
    End Function
 
    <DllImport("advapi32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
    Public Shared Function CreateProcessAsUser(
        hToken As IntPtr,
        lpApplicationName As String,
        lpCommandLine As String,
        ByRef lpProcessAttributes As SECURITY_ATTRIBUTES,
        ByRef lpThreadAttributes As SECURITY_ATTRIBUTES,
        bInheritHandles As Boolean,
        dwCreationFlags As Integer,
        lpEnvironment As IntPtr,
        lpCurrentDirectory As String,
        ByRef lpStartupInfo As STARTUPINFO,
        ByRef lpProcessInformation As PROCESS_INFORMATION) As Boolean
    End Function
 
    <DllImport("userenv.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
    Public Shared Function LoadUserProfile(
        hToken As IntPtr,
        ByRef lpProfileInfo As PROFILEINFO) As Boolean
    End Function
 
    <DllImport("userenv.dll", SetLastError:=True)>
    Public Shared Function UnloadUserProfile(
        hToken As IntPtr,
        hProfile As IntPtr) As Boolean
    End Function
 
    <DllImport("userenv.dll", SetLastError:=True)>
    Public Shared Function CreateEnvironmentBlock(
        ByRef lpEnvironment As IntPtr,
        hToken As IntPtr,
        bInherit As Boolean) As Boolean
    End Function
 
    <DllImport("userenv.dll", SetLastError:=True)>
    Public Shared Function DestroyEnvironmentBlock(
        lpEnvironment As IntPtr) As Boolean
    End Function
 
    <DllImport("kernel32.dll", SetLastError:=True)>
    Public Shared Function CloseHandle(hObject As IntPtr) As Boolean
    End Function
 
    <DllImport("advapi32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
    Public Shared Function LookupPrivilegeValue(
        lpSystemName As String,
        lpName As String,
        ByRef lpLuid As LUID) As Boolean
    End Function
 
    <DllImport("advapi32.dll", SetLastError:=True)>
    Public Shared Function AdjustTokenPrivileges(
        TokenHandle As IntPtr,
        DisableAllPrivileges As Boolean,
        ByRef NewState As TOKEN_PRIVILEGES,
        BufferLength As Integer,
        ByRef PreviousState As TOKEN_PRIVILEGES,
        ByRef ReturnLength As Integer) As Boolean
    End Function
 
    <DllImport("advapi32.dll", SetLastError:=True)>
    Public Shared Function OpenProcessToken(
        ProcessHandle As IntPtr,
        DesiredAccess As Integer,
        ByRef TokenHandle As IntPtr) As Boolean
    End Function
 
    <DllImport("kernel32.dll")>
    Public Shared Function GetCurrentProcess() As IntPtr
    End Function
 
    <DllImport("kernel32.dll", SetLastError:=True)>
    Public Shared Function WaitForSingleObject(
        hHandle As IntPtr,
        dwMilliseconds As Integer) As Integer
    End Function
 
    Private Function NormalizeLocalUncPath(path As String) As String
        If String.IsNullOrWhiteSpace(path) Then
            Return path
        End If
 
        If Not path.StartsWith("\\") Then
            Return path
        End If
 
        Dim parts() As String = path.TrimStart("\"c).Split("\"c)
        If parts.Length < 3 Then
            Return path
        End If
 
        Dim share As String = parts(1)
 
        If share.Length = 1 AndAlso Char.IsLetter(share(0)) Then
            Dim rest As String = String.Join("\", parts, 2, parts.Length - 2)
            Dim localCandidate As String = share & ":\\" & rest
            If System.IO.File.Exists(localCandidate) Then
                Return localCandidate
            End If
        End If
 
        If share.Length = 2 AndAlso Char.IsLetter(share(0)) AndAlso share(1) = "$"c Then
            Dim rest As String = String.Join("\", parts, 2, parts.Length - 2)
            Dim localCandidate As String = share(0) & ":\\" & rest
            If System.IO.File.Exists(localCandidate) Then
                Return localCandidate
            End If
        End If
 
        Return path
    End Function
 
    Private Sub EnablePrivilege(privilegeName As String)
        Dim hToken As IntPtr = IntPtr.Zero
        If Not OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES Or TOKEN_QUERY, hToken) Then
            Throw New Exception("OpenProcessToken failed: " & Marshal.GetLastWin32Error())
        End If
 
        Dim tp As New TOKEN_PRIVILEGES()
        tp.PrivilegeCount = 1
        tp.Privileges.Attributes = SE_PRIVILEGE_ENABLED
 
        If Not LookupPrivilegeValue(Nothing, privilegeName, tp.Privileges.Luid) Then
            CloseHandle(hToken)
            Throw New Exception("LookupPrivilegeValue failed: " & Marshal.GetLastWin32Error())
        End If
 
        Dim prev As New TOKEN_PRIVILEGES()
        Dim retLen As Integer = 0
        If Not AdjustTokenPrivileges(hToken, False, tp, Marshal.SizeOf(tp), prev, retLen) Then
            CloseHandle(hToken)
            Throw New Exception("AdjustTokenPrivileges failed: " & Marshal.GetLastWin32Error())
        End If
 
        CloseHandle(hToken)
    End Sub
 
    ' MAIN FUNCTION
    Public Sub RunProcessAsUser(username As String, domain As String, password As String, appPath As String)
 
        Dim userToken As IntPtr = IntPtr.Zero
        Dim primaryToken As IntPtr = IntPtr.Zero
        Dim envBlock As IntPtr = IntPtr.Zero
 
        ' 1. Logon user -> get token
        If Not LogonUser(username, domain, password,
                        LOGON32_LOGON_INTERACTIVE,
                        LOGON32_PROVIDER_DEFAULT,
                        userToken) Then
 
            Throw New Exception("LogonUser failed: " & Marshal.GetLastWin32Error())
        End If
 
        ' Convert to primary token
        If Not DuplicateTokenEx(userToken,
                                TOKEN_ALL_ACCESS,
                                IntPtr.Zero,
                                2,
                                1,
                                primaryToken) Then
 
            Throw New Exception("DuplicateTokenEx failed: " & Marshal.GetLastWin32Error())
        End If
 
        EnablePrivilege(SE_BACKUP_NAME)
        EnablePrivilege(SE_RESTORE_NAME)
 
        ' 2. Load user profile 
        Dim profileInfo As New PROFILEINFO()
        profileInfo.dwSize = Marshal.SizeOf(profileInfo)
        profileInfo.lpUserName = username
 
        If Not LoadUserProfile(userToken, profileInfo) Then
            Throw New Exception("LoadUserProfile failed: " & Marshal.GetLastWin32Error())
        End If
 
        ' 3. Create environment block for the user
        If Not CreateEnvironmentBlock(envBlock, primaryToken, False) Then
            Throw New Exception("CreateEnvironmentBlock failed: " & Marshal.GetLastWin32Error())
        End If
 
        Dim si As New STARTUPINFO()
        si.cb = Marshal.SizeOf(si)
        si.lpDesktop = "winsta0\default" 
 
        Dim pi As New PROCESS_INFORMATION()
 
        Dim sa As New SECURITY_ATTRIBUTES()
        sa.nLength = Marshal.SizeOf(sa)
 
        Dim resolvedAppPath As String = NormalizeLocalUncPath(appPath)
 
        If Not System.IO.File.Exists(resolvedAppPath) Then
            Throw New Exception("Target EXE not found: " & resolvedAppPath)
        End If
 
        ' 4. Create process as user
        Dim workingDir As String = System.IO.Path.GetDirectoryName(resolvedAppPath)
 
        Dim commandLine As String = """" & resolvedAppPath & """"
 
        Dim result As Boolean = CreateProcessAsUser(
            primaryToken,
            Nothing,
            commandLine,
            sa,
            sa,
            False,
            CREATE_NEW_CONSOLE Or CREATE_UNICODE_ENVIRONMENT,
            envBlock,
            workingDir,
            si,
            pi)
 
        If Not result Then
            Dim err As Integer = Marshal.GetLastWin32Error()
            DestroyEnvironmentBlock(envBlock)
            UnloadUserProfile(userToken, profileInfo.hProfile)
            CloseHandle(primaryToken)
            CloseHandle(userToken)
            Throw New Exception("CreateProcessAsUser failed: " & err)
        End If
 
        WaitForSingleObject(pi.hProcess, INFINITE)
 
        DestroyEnvironmentBlock(envBlock)
        CloseHandle(pi.hProcess)
        CloseHandle(pi.hThread)
        UnloadUserProfile(userToken, profileInfo.hProfile)
        CloseHandle(primaryToken)
        CloseHandle(userToken)
 
    End Sub
    Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
        Call RunProcessAsUser("test123", "pc187", "test123", "\\pc187\D\RandD\RunProcessAsUser\CallExe1\CallExe1\bin\Debug\CallExe1.exe")
        MsgBox("program ran")
        Application.Exit()
    End Sub
 
    Private Sub Form1_Shown(sender As Object, e As System.EventArgs) Handles Me.Shown
        Me.Hide()
    End Sub
End Class

The executable started by the above application is very simple. It just appends a line to a log file:

Imports System.IO

Module Program
    Sub Main()
        File.AppendAllText(My.Application.Info.DirectoryPath & "\log.txt", Now & " - exe called" & vbCrLf)
    End Sub
End Module

The executable running as service wrote the below log :

called - in start
called - in try
Process launched OK. PID=14532 SessionId=1
Process exited quickly! ExitCode=-2147023782 (0x8007045A)
called - in start
called - in try
Process launched OK. PID=6816 SessionId=1
Process exited quickly! ExitCode=-2147023782 (0x8007045A)
called - in start
called - in try
Process launched OK. PID=9336 SessionId=1
Process exited quickly! ExitCode=-2147023782 (0x8007045A)

If I run the second executable directly, it works exactly as expected and a new line gets appended to the log file. The problem is that when the first executable tries to launch it, nothing happens. The log file isn't updated, so it looks like the process either isn't being created or is failing before it reaches the code that writes to the file.

Unfortunately any of these approach did not yield the result.

While searching for similar issues, I also found the following Stack Overflow answer that describes almost the same approach:

https://stackoverflow.com/questions/4278373/how-to-start-a-process-from-windows-service-into-currently-logged-in-users-sess

One of the answers in the above stack overflow question, it provided a similar approach with few suggested changes.

https://stackoverflow.com/a/79940698

I went through this answer carefully and updated my implementation based on those suggestions. Unfortunately, it still didn't work, so I don't think the issue is simply that I skipped one of those documented steps.

At this point, I'm mainly trying to understand whether this approach is actually supposed to work when the parent process is running under the LocalSystem account, or whether I'm missing something that's required for this scenario.

Since Windows services running as system user runs in Session 0 while interactive users run in separate sessions, I'm wondering whether Session 0 isolation is preventing the process from being created or executed in the target user's session.

Some of the questions I have are:

  • Is this overall approach valid when launching a process from a LocalSystem service?
  • Am I missing a privilege, token, or session-related step that's required before calling CreateProcessAsUser()?
  • Or is there some Windows limitation that prevents this from working the way I'm expecting?

TAGS: windows, windows-services, createprocessasuser, logonuser, localsystem

Posted Jun 25, 2026, 7:04 AM UTC

10
Loading comments…