Skip to content

Instantly share code, notes, and snippets.

@heri16
Created July 13, 2016 16:17
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save heri16/8f69aa919ee1d87f3bb53255ef78f188 to your computer and use it in GitHub Desktop.
Save heri16/8f69aa919ee1d87f3bb53255ef78f188 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";
}
}
@ExtremeGTX
Copy link

Please tell me the license used by this gist ? can i use it commercially ?

@antonio-leonardo
Copy link

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!

@bymeitalbd
Copy link

Very nice and works great :) (tested using win-service and console app developed in .net 5)

@KarlRhodes
Copy link

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.

@OwnageIsMagic
Copy link

@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 the SE_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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment