Skip to content

Instantly share code, notes, and snippets.

@simonchen
Forked from heri16/ProcessExtensions.cs
Created March 27, 2024 09:09
Show Gist options
  • Select an option

  • Save simonchen/7629ee6d8ba0f5212f18c0f1c181a7ab to your computer and use it in GitHub Desktop.

Select an option

Save simonchen/7629ee6d8ba0f5212f18c0f1c181a7ab to your computer and use it in GitHub Desktop.

Revisions

  1. @heri16 heri16 revised this gist Jul 13, 2016. No changes.
  2. @heri16 heri16 created this gist Jul 13, 2016.
    516 changes: 516 additions & 0 deletions ProcessExtensions.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,516 @@
    using System;
    using System.Runtime.InteropServices;
    using Microsoft.Win32.SafeHandles;
    using System.IO;

    namespace heri16
    {

    /// <summary>
    /// Static class to help Start a GUI/Console Windows Process as any user that is logged-in to an Interactive Terminal-Session (e.g. RDP).
    /// </summary>
    /// <devdoc>
    /// Console-type processes when created with a new console, don't always write to the redirected stdOutput and stdError.
    /// To fix this, the application executed should always detach from its current console (if any), and
    /// call AttachConsole(-1) to attach to the console of the parent process.
    ///
    /// <para>
    /// [DllImport("kernel32.dll")]
    /// static extern bool FreeConsole();
    ///
    /// [DllImport("kernel32.dll")]
    /// static extern bool AttachConsole(uint dwProcessID);
    /// <para>
    /// </devdoc>
    public static class ProcessExtensions
    {
    #region Win32 Constants

    private const uint INVALID_SESSION_ID = 0xFFFFFFFF;
    private static readonly IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero;

    #endregion

    #region DllImports

    [DllImport("userenv.dll", SetLastError = true)]
    private static extern bool CreateEnvironmentBlock(out IntPtr lpEnvironment, SafeHandle hToken, bool bInherit);

    [DllImport("userenv.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment);

    [DllImport("kernel32.dll")]
    private static extern uint WTSGetActiveConsoleSessionId();

    [DllImport("Wtsapi32.dll")]
    private static extern uint WTSQueryUserToken(uint SessionId, out SafeUserTokenHandle phToken);

    [DllImport("wtsapi32.dll", SetLastError = true)]
    private static extern int WTSEnumerateSessions(
    IntPtr hServer,
    int Reserved,
    int Version,
    out IntPtr ppSessionInfo,
    out int pCount);

    [DllImport("wtsapi32.dll", SetLastError = true)]
    private static extern bool WTSQuerySessionInformation(
    System.IntPtr hServer,
    uint sessionId,
    WTS_INFO_CLASS wtsInfoClass,
    out System.IntPtr ppBuffer,
    out uint pBytesReturned);

    [DllImport("wtsapi32.dll")]
    private static extern void WTSFreeMemory(IntPtr pMemory);

    #endregion

    #region Win32 Structs

    private enum WTS_CONNECTSTATE_CLASS
    {
    WTSActive,
    WTSConnected,
    WTSConnectQuery,
    WTSShadow,
    WTSDisconnected,
    WTSIdle,
    WTSListen,
    WTSReset,
    WTSDown,
    WTSInit
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct WTS_SESSION_INFO
    {
    public readonly UInt32 SessionID;

    [MarshalAs(UnmanagedType.LPStr)]
    public readonly String pWinStationName;

    public readonly WTS_CONNECTSTATE_CLASS State;
    }

    private enum WTS_INFO_CLASS
    {
    WTSInitialProgram,
    WTSApplicationName,
    WTSWorkingDirectory,
    WTSOEMId,
    WTSSessionId,
    WTSUserName,
    WTSWinStationName,
    WTSDomainName,
    WTSConnectState,
    WTSClientBuildNumber,
    WTSClientName,
    WTSClientDirectory,
    WTSClientProductId,
    WTSClientHardwareId,
    WTSClientAddress,
    WTSClientDisplay,
    WTSClientProtocolType
    }

    #endregion

    /// <devdoc>
    /// Gets the user token from the currently active session. Application must be running within the context of the LocalSystem Account.
    /// </devdoc>
    private static bool GetSessionUserToken(ref SafeUserTokenHandle phUserToken, string user_filter = null)
    {
    var bResult = false;
    SafeUserTokenHandle hImpersonationToken = new SafeUserTokenHandle();
    var activeSessionId = INVALID_SESSION_ID;
    var pSessionInfo = IntPtr.Zero;
    var sessionCount = 0;

    IntPtr userPtr = IntPtr.Zero;
    IntPtr domainPtr = IntPtr.Zero;
    uint bytes = 0;

    // Get a handle to the user access token for the current active session.
    if (WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, out pSessionInfo, out sessionCount) != 0)
    {
    var arrayElementSize = Marshal.SizeOf(typeof(WTS_SESSION_INFO));
    var current = pSessionInfo;

    for (var i = 0; i < sessionCount; i++)
    {
    var si = (WTS_SESSION_INFO)Marshal.PtrToStructure((IntPtr)current, typeof(WTS_SESSION_INFO));
    current += arrayElementSize;

    WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, si.SessionID, WTS_INFO_CLASS.WTSUserName, out userPtr, out bytes);
    WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, si.SessionID, WTS_INFO_CLASS.WTSDomainName, out domainPtr, out bytes);

    var user = Marshal.PtrToStringAnsi(userPtr);
    var domain = Marshal.PtrToStringAnsi(domainPtr);

    WTSFreeMemory(userPtr);
    WTSFreeMemory(domainPtr);

    if ((user_filter == null && si.State == WTS_CONNECTSTATE_CLASS.WTSActive) || (user == user_filter) )
    {
    activeSessionId = si.SessionID;
    }

    }
    }

    // If enumerating did not work, fall back to the old method
    if (activeSessionId == INVALID_SESSION_ID)
    {
    activeSessionId = WTSGetActiveConsoleSessionId();
    }

    if (WTSQueryUserToken(activeSessionId, out hImpersonationToken) != 0)
    {
    // Convert the impersonation token to a primary token
    bResult = SafeUserTokenHandle.DuplicateTokenEx(hImpersonationToken, 0, null,
    NativeMethods.IMPERSONATION_LEVEL_SecurityImpersonation, NativeMethods.TOKEN_TYPE_TokenPrimary,
    out phUserToken);

    //CloseHandle(hImpersonationToken);
    }

    return bResult;
    }

    /// <devdoc>
    /// Starts a Process as the last logged-in user that is currently active.
    ///
    /// <para>
    /// Example:
    /// psexec -ids powershell.exe
    /// Add-Type -Path .\src\ProcessExtensions.cs
    /// [murrayju.ProcessExtensions]::StartProcessAsCurrentUser("C:\Windows\System32\cmd.exe", "cmd.exe /K echo running");
    /// </para>
    /// </devdoc>
    public static bool StartProcessAsCurrentUser(string appPath, string cmdLine = null, string workDir = null, bool visible = true)
    {
    return StartProcessAsUser(null, appPath, cmdLine, workDir, visible);
    }

    /// <devdoc>
    /// Starts a Process as any logged-in user with an active or disconnected session.
    ///
    /// <para>
    /// Example:
    /// psexec -ids powershell.exe
    /// Add-Type -Path .\src\ProcessExtensions.cs
    /// [murrayju.ProcessExtensions]::StartProcessAsUser("Mailin", "D:\RENE\XmlImport\ReneXmlImport.exe", "ReneXmlImport.exe D:\RENE\Data\Import\Adj_Selling_Price_3001.xml");
    /// </para>
    /// </devdoc>
    public static bool StartProcessAsUser(string user, string appPath, string cmdLine = null, string workDir = null, bool visible = true)
    {
    SafeUserTokenHandle hUserToken = null;
    var startupInfo = new NativeMethods.STARTUPINFO();
    var processInfo = new SafeNativeMethods.PROCESS_INFORMATION();
    //var procSH = new SafeProcessHandle();
    //var threadSH = new SafeThreadHandle();

    var environmentPtr = IntPtr.Zero;
    int iResultOfCreateProcessAsUser;

    //SafeFileHandle standardInputWritePipeHandle = null;
    SafeFileHandle standardOutputReadPipeHandle = null;
    SafeFileHandle standardErrorReadPipeHandle = null;

    try
    {
    if (!GetSessionUserToken(ref hUserToken, user))
    {
    throw new Exception("StartProcessAsCurrentUser: GetSessionUserToken failed.");
    }

    int creationFlags = NativeMethods.CREATE_UNICODE_ENVIRONMENT | (visible ? NativeMethods.CREATE_NEW_CONSOLE : NativeMethods.CREATE_NO_WINDOW);
    startupInfo.wShowWindow = (short)(visible ? NativeMethods.SW_SHOW : NativeMethods.SW_HIDE);
    startupInfo.lpDesktop = "winsta0\\default";

    CreatePipe(out standardOutputReadPipeHandle, out startupInfo.hStdOutput, false);
    CreatePipe(out standardErrorReadPipeHandle, out startupInfo.hStdError, false);
    startupInfo.dwFlags = NativeMethods.STARTF_USESTDHANDLES;

    if (!CreateEnvironmentBlock(out environmentPtr, hUserToken, false))
    {
    throw new Exception("StartProcessAsCurrentUser: CreateEnvironmentBlock failed.");
    }

    if (String.IsNullOrEmpty(workDir)) { workDir = Environment.CurrentDirectory; }

    if (!NativeMethods.CreateProcessAsUser(hUserToken,
    appPath, // Application Name
    cmdLine, // Command Line
    null,
    null,
    true, // Terminal Services: You cannot inherit handles across sessions
    creationFlags,
    new HandleRef(null, environmentPtr),
    workDir, // Working directory
    startupInfo,
    processInfo))
    {
    iResultOfCreateProcessAsUser = Marshal.GetLastWin32Error();
    throw new Exception("StartProcessAsCurrentUser: CreateProcessAsUser failed due to Error " + iResultOfCreateProcessAsUser.ToString() + ".\n");
    }

    }
    finally
    {
    //CloseHandle(hUserToken);

    if (environmentPtr != IntPtr.Zero)
    {
    DestroyEnvironmentBlock(environmentPtr);
    }
    startupInfo.Dispose();

    UnsafeNativeMethods.CloseHandle(processInfo.hThread);
    UnsafeNativeMethods.CloseHandle(processInfo.hProcess);
    }


    StreamReader standardOutput = new StreamReader(new FileStream(standardOutputReadPipeHandle, FileAccess.Read, 0x1000, false), Console.OutputEncoding, true, 0x1000);
    StreamReader standardError = new StreamReader(new FileStream(standardErrorReadPipeHandle, FileAccess.Read, 0x1000, false), Console.OutputEncoding, true, 0x1000);

    while (!standardOutput.EndOfStream)
    {
    string line = standardOutput.ReadLine();
    if (line.Length>0) Console.WriteLine("stdOutput: " + line);
    }

    return true;
    }

    /// <devdoc>
    /// Implementation from: http://referencesource.microsoft.com/#System/services/monitoring/system/diagnosticts/Process.cs,64d2d72d3ee2e6f9
    /// </devdoc>
    private static void CreatePipe(out SafeFileHandle parentHandle, out SafeFileHandle childHandle, bool parentInputs)
    {
    NativeMethods.SECURITY_ATTRIBUTES lpPipeAttributes = new NativeMethods.SECURITY_ATTRIBUTES();
    lpPipeAttributes.bInheritHandle = true;

    SafeFileHandle hWritePipe = null;
    try
    {
    if (parentInputs)
    CreatePipeWithSecurityAttributes(out childHandle, out hWritePipe, lpPipeAttributes, 0);
    else
    CreatePipeWithSecurityAttributes(out hWritePipe, out childHandle, lpPipeAttributes, 0);
    if (!NativeMethods.DuplicateHandle(new HandleRef(null, NativeMethods.GetCurrentProcess()), hWritePipe, new HandleRef(null, NativeMethods.GetCurrentProcess()), out parentHandle, 0, false, NativeMethods.DUPLICATE_SAME_ACCESS))
    throw new Exception();
    }
    finally
    {
    if ((hWritePipe != null) && !hWritePipe.IsInvalid)
    {
    hWritePipe.Close();
    }
    }
    }

    /// <devdoc>
    /// Implementation from: http://referencesource.microsoft.com/#System/services/monitoring/system/diagnosticts/Process.cs,9136e8bd1abc4d01
    /// </devdoc>
    private static void CreatePipeWithSecurityAttributes(out SafeFileHandle hReadPipe, out SafeFileHandle hWritePipe,
    NativeMethods.SECURITY_ATTRIBUTES lpPipeAttributes, int nSize)
    {
    bool ret = NativeMethods.CreatePipe(out hReadPipe, out hWritePipe, lpPipeAttributes, nSize);
    if ((!ret || hReadPipe.IsInvalid) || hWritePipe.IsInvalid)
    throw new Exception();
    }

    }

    /// <devdoc>
    /// Implementation from: http://referencesource.microsoft.com/#System/compmod/microsoft/win32/NativeMethods.cs
    /// </devdoc>
    internal static class NativeMethods
    {
    public const int STARTF_USESTDHANDLES = 0x00000100;
    public const int DUPLICATE_SAME_ACCESS = 2;

    public const int CREATE_NO_WINDOW = 0x08000000;
    public const int CREATE_UNICODE_ENVIRONMENT = 0x00000400;
    public const int CREATE_NEW_CONSOLE = 0x00000010;

    public const int SW_HIDE = 0;
    public const int SW_SHOWNORMAL = 1;
    public const int SW_NORMAL = 1;
    public const int SW_SHOWMINIMIZED = 2;
    public const int SW_SHOWMAXIMIZED = 3;
    public const int SW_MAXIMIZE = 3;
    public const int SW_SHOWNOACTIVATE = 4;
    public const int SW_SHOW = 5;
    public const int SW_MINIMIZE = 6;
    public const int SW_SHOWMINNOACTIVE = 7;
    public const int SW_SHOWNA = 8;
    public const int SW_RESTORE = 9;
    public const int SW_SHOWDEFAULT = 10;
    public const int SW_MAX = 10;

    public const int IMPERSONATION_LEVEL_SecurityAnonymous = 0;
    public const int IMPERSONATION_LEVEL_SecurityIdentification = 1;
    public const int IMPERSONATION_LEVEL_SecurityImpersonation = 2;
    public const int IMPERSONATION_LEVEL_SecurityDelegation = 3;

    public const int TOKEN_TYPE_TokenPrimary = 1;
    public const int TOKEN_TYPE_TokenImpersonation = 2;

    [StructLayout(LayoutKind.Sequential)]
    public class SECURITY_ATTRIBUTES {
    public int nLength = 12;
    public IntPtr lpSecurityDescriptor = IntPtr.Zero;
    public bool bInheritHandle = false;
    }

    [StructLayout(LayoutKind.Sequential)]
    public class STARTUPINFO {
    public int cb;
    public IntPtr lpReserved = IntPtr.Zero;
    //public IntPtr lpDesktop = IntPtr.Zero;
    [MarshalAs(UnmanagedType.LPTStr)]
    public String lpDesktop = String.Empty;
    public IntPtr lpTitle = IntPtr.Zero;
    public int dwX = 0;
    public int dwY = 0;
    public int dwXSize = 0;
    public int dwYSize = 0;
    public int dwXCountChars = 0;
    public int dwYCountChars = 0;
    public int dwFillAttribute = 0;
    public int dwFlags = 0;
    public short wShowWindow = 0;
    public short cbReserved2 = 0;
    public IntPtr lpReserved2 = IntPtr.Zero;
    public SafeFileHandle hStdInput = new SafeFileHandle(IntPtr.Zero, false);
    public SafeFileHandle hStdOutput = new SafeFileHandle(IntPtr.Zero, false);
    public SafeFileHandle hStdError = new SafeFileHandle(IntPtr.Zero, false);

    public STARTUPINFO() {
    cb = Marshal.SizeOf(this);
    }

    public void Dispose() {
    // close the handles created for child process
    if(hStdInput != null && !hStdInput.IsInvalid) {
    hStdInput.Close();
    hStdInput = null;
    }

    if(hStdOutput != null && !hStdOutput.IsInvalid) {
    hStdOutput.Close();
    hStdOutput = null;
    }

    if(hStdError != null && !hStdError.IsInvalid) {
    hStdError.Close();
    hStdError = null;
    }
    }
    }

    [DllImport(ExternDll.Advapi32, CharSet=System.Runtime.InteropServices.CharSet.Auto, SetLastError=true, BestFitMapping=false)]
    [System.Security.SuppressUnmanagedCodeSecurityAttribute()]
    public extern static bool CreateProcessAsUser(
    SafeHandle hToken,
    string lpApplicationName,
    string lpCommandLine,
    SECURITY_ATTRIBUTES lpProcessAttributes,
    SECURITY_ATTRIBUTES lpThreadAttributes,
    bool bInheritHandles,
    int dwCreationFlags,
    HandleRef lpEnvironment,
    string lpCurrentDirectory,
    STARTUPINFO lpStartupInfo,
    SafeNativeMethods.PROCESS_INFORMATION lpProcessInformation
    );

    [DllImport(ExternDll.Kernel32, CharSet=System.Runtime.InteropServices.CharSet.Auto, SetLastError=true)]
    public static extern bool CreatePipe(out SafeFileHandle hReadPipe, out SafeFileHandle hWritePipe, SECURITY_ATTRIBUTES lpPipeAttributes, int nSize);

    [DllImport(ExternDll.Kernel32, CharSet=System.Runtime.InteropServices.CharSet.Ansi, SetLastError=true, BestFitMapping=false)]
    public static extern bool DuplicateHandle(
    HandleRef hSourceProcessHandle,
    SafeHandle hSourceHandle,
    HandleRef hTargetProcess,
    out SafeFileHandle targetHandle,
    int dwDesiredAccess,
    bool bInheritHandle,
    int dwOptions
    );

    [DllImport(ExternDll.Kernel32, CharSet=System.Runtime.InteropServices.CharSet.Ansi, SetLastError=true)]
    public static extern IntPtr GetCurrentProcess();
    }

    /// <devdoc>
    /// Implementation from: http://referencesource.microsoft.com/#System/compmod/microsoft/win32/SafeNativeMethods.cs
    /// <devdoc>
    internal static class SafeNativeMethods
    {
    [StructLayout(LayoutKind.Sequential)]
    internal class PROCESS_INFORMATION {
    // The handles in PROCESS_INFORMATION are initialized in unmanaged functions.
    // We can't use SafeHandle here because Interop doesn't support [out] SafeHandles in structures/classes yet.
    public IntPtr hProcess = IntPtr.Zero;
    public IntPtr hThread = IntPtr.Zero;
    public int dwProcessId = 0;
    public int dwThreadId = 0;

    // Note this class makes no attempt to free the handles
    // Use InitialSetHandle to copy to handles into SafeHandles

    }
    }

    /// <devdoc>
    /// Implementation from: http://referencesource.microsoft.com/#System/compmod/microsoft/win32/UnsafeNativeMethods.cs
    /// <devdoc>
    internal static class UnsafeNativeMethods
    {

    [DllImport(ExternDll.Kernel32, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal static extern bool CloseHandle(IntPtr handle);

    }

    /// <devdoc>
    /// Implementation from: http://referencesource.microsoft.com/#System/compmod/microsoft/win32/safehandles/SafeUserTokenHandle.cs
    /// <devdoc>
    internal sealed class SafeUserTokenHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
    // Note that OpenProcess returns 0 on failure.
    internal SafeUserTokenHandle() : base (true) {}

    internal SafeUserTokenHandle(IntPtr existingHandle, bool ownsHandle) : base(ownsHandle) {
    SetHandle(existingHandle);
    }

    [DllImport(ExternDll.Advapi32, CharSet=System.Runtime.InteropServices.CharSet.Auto, SetLastError=true, BestFitMapping=false)]
    internal extern static bool DuplicateTokenEx(SafeHandle hToken, int access, NativeMethods.SECURITY_ATTRIBUTES tokenAttributes, int impersonationLevel, int tokenType, out SafeUserTokenHandle hNewToken);

    [DllImport(ExternDll.Kernel32, ExactSpelling=true, SetLastError=true)]
    private static extern bool CloseHandle(IntPtr handle);

    override protected bool ReleaseHandle()
    {
    return CloseHandle(handle);
    }
    }

    /// <devdoc>
    /// Implementation from: http://referencesource.microsoft.com/#System/misc/externdll.cs
    /// <devdoc>
    internal static class ExternDll
    {
    public const string Advapi32 = "advapi32.dll";
    public const string Kernel32 = "kernel32.dll";
    public const string Wtsapi32 = "wtsapi32.dll";
    public const string Userenv = "userenv.dll";
    }
    }