如何以本地系统身份启动独立的登录用户进程?
问题描述
在本地系统权限下运行PowerShell代码,尝试以当前登录用户的上下文启动一个独立进程。目前代码可正常运行,但新进程始终是调用进程的子进程,无法实现真正独立。疑问如下:
- 是否是
dwCreationFlags参数值设置错误? - 有没有办法通过调整代码实现需求?
- 是否只能通过任务计划程序来实现?
测试代码
cls remove-variable * -ea 0 $ErrorActionPreference = 'stop' #requires -runasadmin # pinvoke-code to switch into users context: Add-Type -TypeDefinition @" using System; using System.Runtime.InteropServices; [StructLayout(LayoutKind.Sequential)] public struct STARTUPINFO { public uint cb; public string lpReserved; public string lpDesktop; public string lpTitle; public uint dwX; public uint dwY; public uint dwXSize; public uint dwYSize; public uint dwXCountChars; public uint dwYCountChars; public uint dwFillAttribute; public uint dwFlags; public short wShowWindow; public short cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } [StructLayout(LayoutKind.Sequential)] public struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public uint dwProcessId; public uint dwThreadId; } public static class Kernel32 { [DllImport("kernel32.dll")] public static extern uint WTSGetActiveConsoleSessionId(); [DllImport("Wtsapi32.dll")] public static extern bool WTSQueryUserToken(uint SessionId, ref IntPtr phToken); [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")] public static extern bool DuplicateTokenEx( IntPtr ExistingTokenHandle, uint dwDesiredAccess, IntPtr lpThreadAttributes, int TokenType, int ImpersonationLevel, ref IntPtr DuplicateTokenHandle); [DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUser", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)] public static extern uint CreateProcessAsUser( IntPtr hToken, string lpApplicationName, System.Text.StringBuilder lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandle, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool CloseHandle(IntPtr hSnapshot); } "@ # get the users token: $userToken = [IntPtr]::Zero $session = [kernel32]::WTSGetActiveConsoleSessionId() $null = [kernel32]::WTSQueryUserToken($session, [ref]$userToken) # runs as "local system" only # copy the token: $newToken = [intPtr]::Zero $null = [kernel32]::DuplicateTokenEx($userToken, 0xF01FF, [IntPtr]::Zero, 2, 1, [ref]$newToken); $null = [kernel32]::CloseHandle($userToken) # define startup-conditions for new process: $startInfo = new-object STARTUPINFO $marshal = [System.Runtime.InteropServices.Marshal] $startInfo.cb = $marshal::SizeOf($startInfo) $startInfo.lpDesktop = "winsta0\\default" $startInfo.dwFlags = 1 # to respect wShowWindow-value $startInfo.wShowWindow = 1 # 0=hidden $appPath = "C:\\Windows\\System32\\notepad.exe" $cmdLine = New-Object System.Text.StringBuilder $appPath $dir = [System.IO.Directory]::GetParent($appPath).Fullname $dwCreationFlags = 0x08000400 # this works and can interact with the user, but is not detached # $dwCreationFlags = 0x00000008 # CREATE_NO_WINDOW,仅隐藏窗口,不改变父进程关系 # start a new user-process: $procInfo = new-object PROCESS_INFORMATION $result = [kernel32]::CreateProcessAsUser($newToken, $appPath, $cmdLine, [IntPtr]::Zero, [IntPtr]::Zero, $false, $dwCreationFlags, [IntPtr]::Zero, $dir, [ref]$startInfo, [ref]$procInfo) write-host "result: $result" write-host $procInfo.dwProcessId # clean-up: $null = [kernel32]::CloseHandle($newToken) $null = [kernel32]::CloseHandle($procInfo.hThread) $null = [kernel32]::CloseHandle($procInfo.hProcess) # check, if the new process is REALLY detached or not: try { $child = [System.Management.ManagementObjectSearcher]::new("select * from Win32_Process where ProcessId = $($procInfo.dwProcessId)").Get() $parent = ([System.Management.ManagementObjectSearcher]::new("select * FROM Win32_Process WHERE ProcessId = $($child.ParentProcessId)").Get()).Name write-host "Parent is '$parent'" Stop-Process -Id $child.ProcessId -Force } catch {}
问题分析与解决方案
关于dwCreationFlags的误区
你使用的0x00000008是CREATE_NO_WINDOW标志,它仅作用于控制台程序,用于隐藏其窗口,和进程是否独立无关。而0x08000400是CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT的组合,只是创建新控制台并使用Unicode环境,同样无法改变父进程关系。
为什么CreateProcessAsUser无法创建"真正独立"的进程
Windows的进程父子关系是在创建时直接绑定的:新进程的父进程就是调用CreateProcessAsUser的进程(即你的System权限PowerShell进程)。即使你关闭了新进程的句柄,父进程关系依然存在,直到调用进程退出,此时新进程的父进程会被系统接管(变为wininit.exe或services.exe等系统进程)。
实现真正独立进程的可行方案
方案1:让调用进程退出
如果你的需求允许调用进程(System权限的PowerShell)在启动新进程后退出,那么新进程会自动被系统接管,成为独立进程。但这种方式依赖调用进程的生命周期,不适合需要持续运行的场景。
方案2:使用任务计划程序(推荐)
这是创建独立用户进程最可靠的方式,任务计划程序会通过系统服务启动进程,启动完成后宿主进程退出,新进程的父进程直接变为系统进程。示例代码如下:
$taskName = "Temp_StartUserProcess" $action = New-ScheduledTaskAction -Execute "C:\Windows\System32\notepad.exe" $principal = New-ScheduledTaskPrincipal -UserId ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) -LogonType Interactive $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries # 注册并立即运行任务 Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal -Settings $settings -Force Start-ScheduledTask -TaskName $taskName # 延迟几秒后删除任务(可选,根据需求调整) Start-Sleep -Seconds 2 Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
方案3:通过中间进程中转
可以创建一个极简的中间进程,让它仅负责启动目标进程后立即退出,这样目标进程的父进程会变成系统进程。但这种方式需要额外的可执行文件,不如任务计划便捷。
内容的提问来源于stack exchange,提问作者Carsten
相关产品推荐
相关产品推荐

