powershell - How to start a new detached user-process? - Stack Overflow

admin2025-04-22  5

I am currently struggling on the challenge to run some Powershell code under "local system" which should launch a new detached process in the context of the currently logged-in user.

It all seems to work fine except the new process is always a sub-process and NOT really detached. Is is just because of a wrong value for dwCreationFlags? Can someone help here to make this work? Or is this ONLY possible via the task scheduler?

Here the test-code I have so far:

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 = $apppath
$dir = [System.IO.Directory]::GetParent($appPath).Fullname

$dwCreationFlags = 0x08000400 # this works and can interact with the user, but is not detached
$dwCreationFlags = 0x00000008 # in theory this should start a detached process(?)

# start a new user-process:
$procInfo  = new-object PROCESS_INFORMATION
$result = [kernel32]::CreateProcessAsUser($newToken, $appPath, $cmdLine, 0, 0, $false, $dwCreationFlags, 0, $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 {}

I am currently struggling on the challenge to run some Powershell code under "local system" which should launch a new detached process in the context of the currently logged-in user.

It all seems to work fine except the new process is always a sub-process and NOT really detached. Is is just because of a wrong value for dwCreationFlags? Can someone help here to make this work? Or is this ONLY possible via the task scheduler?

Here the test-code I have so far:

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 = $apppath
$dir = [System.IO.Directory]::GetParent($appPath).Fullname

$dwCreationFlags = 0x08000400 # this works and can interact with the user, but is not detached
$dwCreationFlags = 0x00000008 # in theory this should start a detached process(?)

# start a new user-process:
$procInfo  = new-object PROCESS_INFORMATION
$result = [kernel32]::CreateProcessAsUser($newToken, $appPath, $cmdLine, 0, 0, $false, $dwCreationFlags, 0, $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 {}
Share Improve this question asked Jan 21 at 14:37 CarstenCarsten 2,2411 gold badge21 silver badges40 bronze badges 5
  • Do you mean that you don't want it to be a child process of the launching process? Detached just seems to refer to the new process not being attached to the caller's console (and having no new one allocated for it), though I don't know how that relates to a parent/child relationship. – mklement0 Commented Jan 21 at 21:54
  • 1 It should run without being a sub-process of the calling process. – Carsten Commented Jan 24 at 13:33
  • @mklement0 : Every process has a PPID (Parent Process ID). When a application is launched in Windows the parent process is 0 which is the System Process. When an app launches a process the app is the PPID. The OP want an app to be launched in background mode where the PPID is the System Process. – jdweng Commented Jan 29 at 22:08
  • @jdweng: Launching a process from the Windows shell in an interactive window station makes an explorer.exe process the parent process. When a process is launched from a noninteractive context such as a service, e.g. the Task Scheduler or PowerShell remoting, the parent process is an instance of a svchost process. Launching a process from an application process makes the former a child process of the latter by default. The question at hand is how not to make the launched process a child process of the calling one. – mklement0 Commented Jan 30 at 2:29
  • @Carsten: To avoid ambiguity, I suggest using the term child process - while the terms subprocess or subtask are also used, my impression is that child process is the most widely used term. – mklement0 Commented Jan 30 at 2:34
Add a comment  | 

1 Answer 1

Reset to default 0

Here are the flags. You want to set the DETACHED_PROCESS flag

[Flags]
    enum CreateProcessFlags : uint
    {
        DEBUG_PROCESS               = 0x00000001,
        DEBUG_ONLY_THIS_PROCESS         = 0x00000002,
        CREATE_SUSPENDED            = 0x00000004,
        DETACHED_PROCESS            = 0x00000008,
        CREATE_NEW_CONSOLE          = 0x00000010,
        NORMAL_PRIORITY_CLASS           = 0x00000020,
        IDLE_PRIORITY_CLASS         = 0x00000040,
        HIGH_PRIORITY_CLASS         = 0x00000080,
        REALTIME_PRIORITY_CLASS         = 0x00000100,
        CREATE_NEW_PROCESS_GROUP        = 0x00000200,
        CREATE_UNICODE_ENVIRONMENT      = 0x00000400,
        CREATE_SEPARATE_WOW_VDM         = 0x00000800,
        CREATE_SHARED_WOW_VDM           = 0x00001000,
        CREATE_FORCEDOS             = 0x00002000,
        BELOW_NORMAL_PRIORITY_CLASS     = 0x00004000,
        ABOVE_NORMAL_PRIORITY_CLASS     = 0x00008000,
        INHERIT_PARENT_AFFINITY         = 0x00010000,
        INHERIT_CALLER_PRIORITY         = 0x00020000,
        CREATE_PROTECTED_PROCESS        = 0x00040000,
        EXTENDED_STARTUPINFO_PRESENT    = 0x00080000,
        PROCESS_MODE_BACKGROUND_BEGIN       = 0x00100000,
        PROCESS_MODE_BACKGROUND_END     = 0x00200000,
        CREATE_BREAKAWAY_FROM_JOB       = 0x01000000,
        CREATE_PRESERVE_CODE_AUTHZ_LEVEL    = 0x02000000,
        CREATE_DEFAULT_ERROR_MODE       = 0x04000000,
        CREATE_NO_WINDOW            = 0x08000000,
        PROFILE_USER            = 0x10000000,
        PROFILE_KERNEL              = 0x20000000,
        PROFILE_SERVER              = 0x40000000,
        CREATE_IGNORE_SYSTEM_DEFAULT    = 0x80000000,
    }
转载请注明原文地址:http://anycun.com/QandA/1745304058a90620.html