-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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