Indago.psm1
|
#region C# Source — RunAsUser Process Extensions # Embedded C# type for Win32 CreateProcessAsUser. # Compiled once per session via Add-Type. Provides: # [RunAsUser.ProcessExtensions]::StartProcessAsCurrentUser() # [RunAsUser.ProcessExtensions]::GetTokenPrivileges() $script:CSharpSource = @" using Microsoft.Win32.SafeHandles; using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Security.Principal; using System.Text; using System.Threading; namespace RunAsUser { internal class NativeHelpers { [StructLayout(LayoutKind.Sequential)] public struct LUID { public int LowPart; public int HighPart; } [StructLayout(LayoutKind.Sequential)] public struct LUID_AND_ATTRIBUTES { public LUID Luid; public PrivilegeAttributes Attributes; } [StructLayout(LayoutKind.Sequential)] public struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public int dwProcessId; public int dwThreadId; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct STARTUPINFO { public int cb; public String lpReserved; public String lpDesktop; public String lpTitle; public uint dwX; public uint dwY; public uint dwXSize; public uint dwYSize; public uint dwXCountChars; public uint dwYCountChars; public uint dwFillAttribute; public uint dwFlags; public short wShowWindow; public short cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } [StructLayout(LayoutKind.Sequential)] public struct TOKEN_PRIVILEGES { public int PrivilegeCount; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] public LUID_AND_ATTRIBUTES[] Privileges; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct WTS_SESSION_INFO { public readonly UInt32 SessionID; [MarshalAs(UnmanagedType.LPWStr)] public readonly String pWinStationName; public readonly WTS_CONNECTSTATE_CLASS State; } public struct SECURITY_ATTRIBUTES { public Int32 nLength; public IntPtr lpSecurityDescriptor; public int bInheritHandle; } } internal class NativeMethods { [DllImport("kernel32", SetLastError = true)] public static extern int WaitForSingleObject( IntPtr hHandle, int dwMilliseconds); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool CloseHandle( IntPtr hSnapshot); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool TerminateProcess( IntPtr hProcess, uint uExitCode); [DllImport("userenv.dll", SetLastError = true)] public static extern bool CreateEnvironmentBlock( ref IntPtr lpEnvironment, SafeHandle hToken, bool bInherit); [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern bool CreateProcessAsUserW( SafeHandle hToken, String lpApplicationName, StringBuilder lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandle, uint dwCreationFlags, IntPtr lpEnvironment, String lpCurrentDirectory, ref NativeHelpers.STARTUPINFO lpStartupInfo, out NativeHelpers.PROCESS_INFORMATION lpProcessInformation); [DllImport("userenv.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool DestroyEnvironmentBlock( IntPtr lpEnvironment); [DllImport("advapi32.dll", SetLastError = true)] public static extern bool DuplicateTokenEx( SafeHandle ExistingTokenHandle, uint dwDesiredAccess, IntPtr lpThreadAttributes, SECURITY_IMPERSONATION_LEVEL ImpersonationLevel, TOKEN_TYPE TokenType, out SafeNativeHandle DuplicateTokenHandle); [DllImport("kernel32")] public static extern IntPtr GetCurrentProcess(); // Bug #4 fix: SafeHandle overload for actual data retrieval [DllImport("advapi32.dll", SetLastError = true)] public static extern bool GetTokenInformation( SafeHandle TokenHandle, uint TokenInformationClass, SafeMemoryBuffer TokenInformation, int TokenInformationLength, out int ReturnLength); // Bug #4 fix: IntPtr overload for buffer-size query (avoids SafeHandle invalid-zero crash) [DllImport("advapi32.dll", EntryPoint = "GetTokenInformation", SetLastError = true)] public static extern bool GetTokenInformationRaw( SafeHandle TokenHandle, uint TokenInformationClass, IntPtr TokenInformation, int TokenInformationLength, out int ReturnLength); [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern bool LookupPrivilegeName( string lpSystemName, ref NativeHelpers.LUID lpLuid, StringBuilder lpName, ref Int32 cchName); [DllImport("advapi32.dll", SetLastError = true)] public static extern bool OpenProcessToken( IntPtr ProcessHandle, TokenAccessLevels DesiredAccess, out SafeNativeHandle TokenHandle); [DllImport("wtsapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern bool WTSEnumerateSessions( IntPtr hServer, int Reserved, int Version, ref IntPtr ppSessionInfo, ref int pCount); [DllImport("wtsapi32.dll")] public static extern void WTSFreeMemory( IntPtr pMemory); [DllImport("kernel32.dll")] public static extern uint WTSGetActiveConsoleSessionId(); [DllImport("Wtsapi32.dll", SetLastError = true)] public static extern bool WTSQueryUserToken( uint SessionId, out SafeNativeHandle phToken); [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr CreatePipe( ref IntPtr hReadPipe, ref IntPtr hWritePipe, ref NativeHelpers.SECURITY_ATTRIBUTES lpPipeAttributes, Int32 nSize); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool SetHandleInformation( IntPtr hObject, int dwMask, int dwFlags); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool ReadFile( IntPtr hFile, byte[] lpBuffer, int nNumberOfBytesToRead, ref int lpNumberOfBytesRead, IntPtr lpOverlapped); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool PeekNamedPipe( IntPtr handle, byte[] buffer, uint nBufferSize, ref uint bytesRead, ref uint bytesAvail, ref uint BytesLeftThisMessage); } internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid { public SafeMemoryBuffer(int cb) : base(true) { base.SetHandle(Marshal.AllocHGlobal(cb)); } public SafeMemoryBuffer(IntPtr handle) : base(true) { base.SetHandle(handle); } protected override bool ReleaseHandle() { Marshal.FreeHGlobal(handle); return true; } } internal class SafeNativeHandle : SafeHandleZeroOrMinusOneIsInvalid { public SafeNativeHandle() : base(true) { } public SafeNativeHandle(IntPtr handle) : base(true) { this.handle = handle; } protected override bool ReleaseHandle() { return NativeMethods.CloseHandle(handle); } } internal enum SECURITY_IMPERSONATION_LEVEL { SecurityAnonymous = 0, SecurityIdentification = 1, SecurityImpersonation = 2, SecurityDelegation = 3, } internal enum SW { SW_HIDE = 0, SW_SHOWNORMAL = 1, SW_NORMAL = 1, SW_SHOWMINIMIZED = 2, SW_SHOWMAXIMIZED = 3, SW_MAXIMIZE = 3, SW_SHOWNOACTIVATE = 4, SW_SHOW = 5, SW_MINIMIZE = 6, SW_SHOWMINNOACTIVE = 7, SW_SHOWNA = 8, SW_RESTORE = 9, SW_SHOWDEFAULT = 10, SW_MAX = 10 } internal enum TokenElevationType { TokenElevationTypeDefault = 1, TokenElevationTypeFull, TokenElevationTypeLimited, } internal enum TOKEN_TYPE { TokenPrimary = 1, TokenImpersonation = 2 } internal enum WTS_CONNECTSTATE_CLASS { WTSActive, WTSConnected, WTSConnectQuery, WTSShadow, WTSDisconnected, WTSIdle, WTSListen, WTSReset, WTSDown, WTSInit } [Flags] public enum PrivilegeAttributes : uint { Disabled = 0x00000000, EnabledByDefault = 0x00000001, Enabled = 0x00000002, Removed = 0x00000004, UsedForAccess = 0x80000000, } public class Win32Exception : System.ComponentModel.Win32Exception { private string _msg; public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { } public Win32Exception(int errorCode, string message) : base(errorCode) { _msg = String.Format("{0} ({1}, Win32ErrorCode {2} - 0x{2:X8})", message, base.Message, errorCode); } public override string Message { get { return _msg; } } public static explicit operator Win32Exception(string message) { return new Win32Exception(message); } } public static class ProcessExtensions { #region Win32 Constants private const int CREATE_UNICODE_ENVIRONMENT = 0x00000400; private const int CREATE_NO_WINDOW = 0x08000000; private const int CREATE_NEW_CONSOLE = 0x00000010; private const uint INVALID_SESSION_ID = 0xFFFFFFFF; private static readonly IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero; private const int HANDLE_FLAG_INHERIT = 0x00000001; private const int STARTF_USESTDHANDLES = 0x00000100; private const int CREATE_BREAKAWAY_FROM_JOB = 0x01000000; private const int BUFSIZE = 4096; private const int WAIT_TIMEOUT = 0x00000102; #endregion private static SafeNativeHandle GetSessionUserToken(bool elevated) { var activeSessionId = INVALID_SESSION_ID; var pSessionInfo = IntPtr.Zero; var sessionCount = 0; if (NativeMethods.WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, ref pSessionInfo, ref sessionCount)) { try { var arrayElementSize = Marshal.SizeOf(typeof(NativeHelpers.WTS_SESSION_INFO)); var current = pSessionInfo; for (var i = 0; i < sessionCount; i++) { var si = (NativeHelpers.WTS_SESSION_INFO)Marshal.PtrToStructure( current, typeof(NativeHelpers.WTS_SESSION_INFO)); current = IntPtr.Add(current, arrayElementSize); if (si.State == WTS_CONNECTSTATE_CLASS.WTSActive) { activeSessionId = si.SessionID; break; } } } finally { NativeMethods.WTSFreeMemory(pSessionInfo); } } if (activeSessionId == INVALID_SESSION_ID) { activeSessionId = NativeMethods.WTSGetActiveConsoleSessionId(); } SafeNativeHandle hImpersonationToken; if (!NativeMethods.WTSQueryUserToken(activeSessionId, out hImpersonationToken)) { throw new Win32Exception("WTSQueryUserToken failed to get access token."); } using (hImpersonationToken) { TokenElevationType elevationType = GetTokenElevationType(hImpersonationToken); if (elevationType == TokenElevationType.TokenElevationTypeLimited && elevated == true) { using (var linkedToken = GetTokenLinkedToken(hImpersonationToken)) return DuplicateTokenAsPrimary(linkedToken); } else { return DuplicateTokenAsPrimary(hImpersonationToken); } } } // Pipe handles are method-local (thread-safe), stderr merged into stdout. public static string StartProcessAsCurrentUser(string appPath, string cmdLine = null, string workDir = null, bool visible = true, int wait = -1, bool elevated = true, bool redirectOutput = true, bool breakaway = false) { IntPtr out_read = IntPtr.Zero; IntPtr out_write = IntPtr.Zero; // R2-Bug #3 fix: master try/finally so pipe handles are cleaned up // even if GetSessionUserToken or CreateEnvironmentBlock throws try { NativeHelpers.SECURITY_ATTRIBUTES saAttr = new NativeHelpers.SECURITY_ATTRIBUTES(); saAttr.nLength = Marshal.SizeOf(typeof(NativeHelpers.SECURITY_ATTRIBUTES)); saAttr.bInheritHandle = 0x1; saAttr.lpSecurityDescriptor = IntPtr.Zero; if (redirectOutput) { NativeMethods.CreatePipe(ref out_read, ref out_write, ref saAttr, 0); NativeMethods.SetHandleInformation(out_read, HANDLE_FLAG_INHERIT, 0); } var startInfo = new NativeHelpers.STARTUPINFO(); startInfo.cb = Marshal.SizeOf(startInfo); // Map process to the interactive user's desktop. // Previous ERROR_INVALID_NAME was caused by ANSI/Unicode marshaling // mismatch (STARTUPINFO lacked CharSet.Unicode). Now fixed. startInfo.lpDesktop = @"winsta0\default"; uint dwCreationFlags = CREATE_UNICODE_ENVIRONMENT | (uint)(breakaway ? CREATE_BREAKAWAY_FROM_JOB : 0) | (uint)(visible ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW); // STARTF_USESHOWWINDOW so wShowWindow is respected by the API startInfo.dwFlags = 0x00000001; startInfo.wShowWindow = (short)(visible ? SW.SW_SHOW : SW.SW_HIDE); if (redirectOutput) { startInfo.hStdOutput = out_write; startInfo.hStdError = out_write; startInfo.dwFlags |= (uint)STARTF_USESTDHANDLES; } StringBuilder commandLine = new StringBuilder(cmdLine); var procInfo = new NativeHelpers.PROCESS_INFORMATION(); using (var hUserToken = GetSessionUserToken(elevated)) { IntPtr pEnv = IntPtr.Zero; if (!NativeMethods.CreateEnvironmentBlock(ref pEnv, hUserToken, false)) { throw new Win32Exception("CreateEnvironmentBlock failed."); } try { if (!NativeMethods.CreateProcessAsUserW(hUserToken, appPath, commandLine, IntPtr.Zero, IntPtr.Zero, redirectOutput, dwCreationFlags, pEnv, workDir, ref startInfo, out procInfo)) { throw new Win32Exception("CreateProcessAsUser failed."); } try { if (redirectOutput) { // Close parent's write handle so ReadFile sees EOF NativeMethods.CloseHandle(out_write); out_write = IntPtr.Zero; // R2-Bug #2 fix: read pipe on a background thread so the // main thread can enforce the timeout via WaitForSingleObject. // If the child hangs, ReadFile blocks the reader thread but // WaitForSingleObject returns WAIT_TIMEOUT on the main thread, // which then kills the child, breaking the pipe and unblocking // the reader. var sb = new StringBuilder(); var readDone = new ManualResetEvent(false); // Capture out_read in a local for the closure IntPtr pipeHandle = out_read; ThreadPool.QueueUserWorkItem(delegate { try { byte[] buf = new byte[BUFSIZE]; // R2-Bug #5 fix: Decoder maintains state across chunks // so multi-byte UTF-8 chars split at buffer boundaries // are decoded correctly instead of producing \uFFFD. Decoder decoder = Encoding.UTF8.GetDecoder(); int dwRead = 0; while (true) { bool bSuccess = NativeMethods.ReadFile(pipeHandle, buf, BUFSIZE, ref dwRead, IntPtr.Zero); if (!bSuccess || dwRead == 0) { int flushCount = decoder.GetCharCount(new byte[0], 0, 0, true); if (flushCount > 0) { char[] flushChars = new char[flushCount]; decoder.GetChars(new byte[0], 0, 0, flushChars, 0, true); sb.Append(flushChars, 0, flushCount); } break; } int charCount = decoder.GetCharCount(buf, 0, dwRead); char[] chars = new char[charCount]; decoder.GetChars(buf, 0, dwRead, chars, 0); sb.Append(chars, 0, charCount); } } catch { /* pipe broken = expected on timeout kill */ } finally { readDone.Set(); } }); // Main thread: enforce the timeout int waitResult = NativeMethods.WaitForSingleObject(procInfo.hProcess, wait); if (waitResult == WAIT_TIMEOUT) { // Kill the hung process — this breaks the pipe, // unblocking the reader thread's ReadFile call NativeMethods.TerminateProcess(procInfo.hProcess, 1); } // Wait for the reader thread to finish (give it 5s after process exit) readDone.WaitOne(5000); NativeMethods.CloseHandle(out_read); out_read = IntPtr.Zero; return sb.ToString(); } else { int waitResult = NativeMethods.WaitForSingleObject(procInfo.hProcess, wait); if (waitResult == WAIT_TIMEOUT) { NativeMethods.TerminateProcess(procInfo.hProcess, 1); } return procInfo.dwProcessId.ToString(); } } finally { NativeMethods.CloseHandle(procInfo.hThread); NativeMethods.CloseHandle(procInfo.hProcess); } } finally { NativeMethods.DestroyEnvironmentBlock(pEnv); } } } finally { // Master cleanup: handles are closed regardless of where the exception was thrown if (out_read != IntPtr.Zero) NativeMethods.CloseHandle(out_read); if (out_write != IntPtr.Zero) NativeMethods.CloseHandle(out_write); } } private static SafeNativeHandle DuplicateTokenAsPrimary(SafeHandle hToken) { SafeNativeHandle pDupToken; if (!NativeMethods.DuplicateTokenEx(hToken, 0, IntPtr.Zero, SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, TOKEN_TYPE.TokenPrimary, out pDupToken)) { throw new Win32Exception("DuplicateTokenEx failed."); } return pDupToken; } public static Dictionary<String, PrivilegeAttributes> GetTokenPrivileges() { Dictionary<string, PrivilegeAttributes> privileges = new Dictionary<string, PrivilegeAttributes>(); using (SafeNativeHandle hToken = OpenProcessToken(NativeMethods.GetCurrentProcess(), TokenAccessLevels.Query)) using (SafeMemoryBuffer tokenInfo = GetTokenInformation(hToken, 3)) { NativeHelpers.TOKEN_PRIVILEGES privilegeInfo = (NativeHelpers.TOKEN_PRIVILEGES)Marshal.PtrToStructure( tokenInfo.DangerousGetHandle(), typeof(NativeHelpers.TOKEN_PRIVILEGES)); IntPtr ptrOffset = IntPtr.Add(tokenInfo.DangerousGetHandle(), Marshal.SizeOf(privilegeInfo.PrivilegeCount)); for (int i = 0; i < privilegeInfo.PrivilegeCount; i++) { NativeHelpers.LUID_AND_ATTRIBUTES info = (NativeHelpers.LUID_AND_ATTRIBUTES)Marshal.PtrToStructure(ptrOffset, typeof(NativeHelpers.LUID_AND_ATTRIBUTES)); int nameLen = 0; NativeHelpers.LUID privLuid = info.Luid; NativeMethods.LookupPrivilegeName(null, ref privLuid, null, ref nameLen); StringBuilder name = new StringBuilder(nameLen + 1); if (!NativeMethods.LookupPrivilegeName(null, ref privLuid, name, ref nameLen)) { throw new Win32Exception("LookupPrivilegeName() failed"); } privileges[name.ToString()] = info.Attributes; ptrOffset = IntPtr.Add(ptrOffset, Marshal.SizeOf(typeof(NativeHelpers.LUID_AND_ATTRIBUTES))); } } return privileges; } private static TokenElevationType GetTokenElevationType(SafeHandle hToken) { using (SafeMemoryBuffer tokenInfo = GetTokenInformation(hToken, 18)) { return (TokenElevationType)Marshal.ReadInt32(tokenInfo.DangerousGetHandle()); } } private static SafeNativeHandle GetTokenLinkedToken(SafeHandle hToken) { using (SafeMemoryBuffer tokenInfo = GetTokenInformation(hToken, 19)) { return new SafeNativeHandle(Marshal.ReadIntPtr(tokenInfo.DangerousGetHandle())); } } // Bug #4 fix: use IntPtr.Zero directly for buffer-size probe instead of // new SafeMemoryBuffer(IntPtr.Zero) which throws ArgumentException private static SafeMemoryBuffer GetTokenInformation(SafeHandle hToken, uint infoClass) { int returnLength; bool res = NativeMethods.GetTokenInformationRaw(hToken, infoClass, IntPtr.Zero, 0, out returnLength); int errCode = Marshal.GetLastWin32Error(); if (!res && errCode != 24 && errCode != 122) { throw new Win32Exception(errCode, String.Format("GetTokenInformation({0}) failed to get buffer length", infoClass)); } SafeMemoryBuffer tokenInfo = new SafeMemoryBuffer(returnLength); if (!NativeMethods.GetTokenInformation(hToken, infoClass, tokenInfo, returnLength, out returnLength)) throw new Win32Exception(String.Format("GetTokenInformation({0}) failed", infoClass)); return tokenInfo; } private static SafeNativeHandle OpenProcessToken(IntPtr process, TokenAccessLevels access) { SafeNativeHandle hToken = null; if (!NativeMethods.OpenProcessToken(process, access, out hToken)) { throw new Win32Exception("OpenProcessToken() failed"); } return hToken; } } } "@ #endregion #region Module State $script:IndagoState = @{ ModuleRoot = $PSScriptRoot ScriptletCatalog = $null LogPath = $null LoggedOnUser = $null TypeLoaded = $false } #endregion #region C# Type Compilation if (-not ('RunAsUser.ProcessExtensions' -as [type])) { try { Add-Type -TypeDefinition $script:CSharpSource -Language CSharp -ErrorAction Stop $script:IndagoState.TypeLoaded = $true Write-Verbose 'Indago: C# ProcessExtensions type compiled successfully.' } catch { Write-Warning "Indago: Failed to compile C# type. User-context tasks will not be available. Error: $($_.Exception.Message)" } } else { $script:IndagoState.TypeLoaded = $true Write-Verbose 'Indago: C# ProcessExtensions type already loaded.' } #endregion #region Resolve Log Path $logDir = Join-Path -Path 'C:\ProgramData\Indago' -ChildPath 'Logs' if (-not (Test-Path -Path $logDir)) { try { $null = New-Item -Path $logDir -ItemType Directory -Force -ErrorAction Stop } catch { # Fall back to Windows temp if ProgramData is somehow unavailable $logDir = Join-Path -Path $env:SystemRoot -ChildPath 'Temp' } } $script:IndagoState.LogPath = $logDir #endregion #region Dot-Source Private and Public Functions $privatePath = Join-Path -Path $PSScriptRoot -ChildPath 'Private' if (Test-Path -Path $privatePath) { foreach ($file in Get-ChildItem -Path $privatePath -Filter '*.ps1') { . $file.FullName } } $publicPath = Join-Path -Path $PSScriptRoot -ChildPath 'Public' if (Test-Path -Path $publicPath) { foreach ($file in Get-ChildItem -Path $publicPath -Filter '*.ps1') { . $file.FullName } } #endregion #region Load Scriptlet Catalog on Import # Bug #10 fix: use the validation function instead of raw ConvertFrom-Json $script:IndagoState.ScriptletCatalog = Import-ScriptletCatalog if (@($script:IndagoState.ScriptletCatalog).Count -gt 0) { Write-Verbose "Indago: Loaded $(@($script:IndagoState.ScriptletCatalog).Count) validated scriptlets from catalog." } else { Write-Warning 'Indago: No valid scriptlets loaded. Check the catalog file and run Invoke-SelfTest.' } #endregion Export-ModuleMember -Function 'Invoke-Indago', 'Get-IndagoList', 'Get-IndagoHelp', 'Get-LoggedOnUser' |