-
-
Save heri16/8f69aa919ee1d87f3bb53255ef78f188 to your computer and use it in GitHub Desktop.
| 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"; | |
| } | |
| } |
Congratulations Heri,
This class resolves one problem that I was research a lot: start one Process from "Layer 0" (as Windows Service or Local System) to Domain Logged User.
Thanks a lot!
Very nice and works great :) (tested using win-service and console app developed in .net 5)
Hi, I'm trying to use this code to find an active RDC user session for a given user, and then eventually launch an application for their session.
I can find the user, and get down to line 169, but that line always evaluates to false, and then the method "GetSessionUserToken" returns false and I can't get any further... I know very little about Win32 programming and could use some help here please.
@KarlRhodes https://learn.microsoft.com/en-us/windows/win32/api/wtsapi32/nf-wtsapi32-wtsqueryusertoken
To call this function successfully, the calling application must be running within the context of the
LocalSystemaccount and have theSE_TCB_NAMEprivilege.
This privilege is usually present in LocalSystem token, but disabled, you can use AdjustTokenPrivileges to enable it. Since SE_TCB_NAME is a potentially very dangerous privilege to have, you should disable it again immediately after you use it.
minor improvement - use Win32Exception on the errorCode to provide human readable messaging
replace line 257 with throw new Exception("StartProcessAsCurrentUser: CreateProcessAsUser failed.\n", new Win32Exception(iResultOfCreateProcessAsUser));
Thanks for the suggestions!
Please tell me the license used by this gist ? can i use it commercially ?