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:
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