You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何以本地系统身份启动独立的登录用户进程?

问题描述

在本地系统权限下运行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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 19:52:33