在LocalSystem Windows服务中使用CreateProcessAsUser以其他用户身份运行可执行文件
我正在尝试通过 services.msc 在 LocalSystem 账户下启动一个 .exe。我想通过这个可执行文件以另一位用户的身份运行一个应用程序。
我研究了一阵子,发现事情并不像看起来那么简单。我把问题和解决方法都来自微软文档本身:
CreateProcessWithLogonW函数 (winbase.h)
你不能从运行在 "LocalSystem" 账户下的进程调用 CreateProcessWithLogonW,因为该函数在调用者令牌中使用了登录SID,而 "LocalSystem" 账户的令牌不包含这个SID。作为替代,请使用 CreateProcessAsUser 和 LogonUser 函数。
于是我想出了这个变通办法:
- 登录用户并创建主令牌:
LogonUser、DuplicateTokenEx - 加载用户配置文件:
LoadUserProfile - 创建用户环境块:
CreateEnvironmentBlock - 以目标用户启动进程:
CreateProcessAsUser
说实话,这个做法是AI给出的,但我深入研究后发现它确实有道理,于是我尝试去实现。
下面是我尝试的内容:
在LocalSystem账户下运行的主EXE:
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
从上述EXE调用的那个EXE应该以不同的用户身份运行:
Imports System.IO
Module Program
Sub Main()
File.AppendAllText(My.Application.Info.DirectoryPath & "\log.txt", Now & " - exe called" & vbCrLf)
End Sub
End Module
当我手动点击该EXE时,它能够工作并向日志文件追加新的一行。 但通过第一个EXE启动时,它就不起作用。
也许可以使用其他应用程序或工具来实现,但我想看看是否能够通过上述方法或类似的方法来实现。我主要想知道这个方法是否真的可行,或者我到底做错了什么。如果这种方法不可行,我也想知道原因。
我花了好几个小时与人工智能较劲,试图找出这到底为什么不起作用,但似乎它再也帮不上忙了,所以现在来寻求人类的帮助。
我查看了类似的帖子,但不幸地没有太大帮助。我不知道这些解决方案是不是太旧,或者是我实现它们的方式不对。
如何在Windows服务中使用C#将非交互式进程以另一用户身份运行?
解决方案
在Windows服务下以 LocalSystem 账户运行一个进程并试图以不同的用户身份启动另一个应用程序,确实并不简单。
原因在于服务运行在 Session 0,出于安全原因与交互式用户会话相隔离。因此,单纯地从服务调用 CreateProcess 并不会在目标用户的桌面会话中启动应用程序。
根据微软文档,通常正确的做法是:
- 使用诸如以下API获取目标用户的令牌:
LogonUser()- 或
WTSQueryUserToken() - 使用以下方式复制令牌:
DuplicateTokenEx()- 使用以下方式启动进程:
CreateProcessAsUser()
你可能还需要:
- 调整诸如
SE_ASSIGNPRIMARYTOKEN_NAME与SE_INCREASE_QUOTA_NAME之类的权限, - 设置正确的桌面(
winsta0\default), - 确保进程在活动用户会话中启动。
示例流程:
LogonUser(...);
DuplicateTokenEx(...);
CreateEnvironmentBlock(...);
CreateProcessAsUser(...);
微软还解释说,由于在较新版本的Windows中引入了Session 0隔离,服务不能再直接与用户桌面交互。
所以,是的,你的理解是正确的 —— 从服务启动一个应用程序以另一用户身份运行,需要使用Windows安全令牌和正确的进程创建API,而不是普通的 CreateProcess() 调用。