Skip to content

Instantly share code, notes, and snippets.

@rmdavy
Forked from heri16/ProcessExtensions.cs
Created September 7, 2023 17:11
Show Gist options
  • Save rmdavy/d936fc5d7b173d2804ec92c6cc10d00c to your computer and use it in GitHub Desktop.
Save rmdavy/d936fc5d7b173d2804ec92c6cc10d00c to your computer and use it in GitHub Desktop.
Powershell / C# class to start a GUI Windows Process on the desktop/session of any logged-in RDP/TS user.
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";
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment