-
-
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
LocalSystem
account and have theSE_TCB_NAME
privilege.
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 ?