diff --git a/README.md b/README.md index d1379bc7..03e9072e 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ Note: `gsudo.exe` is portable. No windows service is required or system change i ## Usage ``` powershell -gsudo [options] # Elevates your current shell +gsudo [options] # Starts your current shell elevated gsudo [options] {command} [args] # Runs {command} with elevated permissions gsudo cache [on | off | help] # Starts/Stops a credentials cache session. (less UAC popups) gsudo status [--json | filter ] # Shows current user, cache and console status. @@ -106,6 +106,7 @@ Other options: --debug # Enable debug mode. --copyns # Connect network drives to the elevated user. Warning: Verbose, interactive asks for credentials --copyev # (deprecated) Copy environment variables to the elevated process. (not needed on default console mode) + --chdir {dir} # Change the current directory to {dir} before running the command. ``` **Note:** You can use anywhere **the `sudo` alias** created by the installers. diff --git a/docs/docs/usage/usage.md b/docs/docs/usage/usage.md index 69e67daf..4200d738 100644 --- a/docs/docs/usage/usage.md +++ b/docs/docs/usage/usage.md @@ -39,6 +39,7 @@ Other options: --debug # Enable debug mode. --copyns # Connect network drives to the elevated user. Warning: Verbose, interactive asks for credentials --copyev # (deprecated) Copy environment variables to the elevated process. (not needed on default console mode) + --chdir {dir} # Change the current directory to {dir} before running the command. ``` diff --git a/global.json b/global.json new file mode 100644 index 00000000..f0858b83 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "7.0.0", + "rollForward": "latestFeature" + } +} \ No newline at end of file diff --git a/src/gsudo/AppSettings/PathPrecedenceSetting.cs b/src/gsudo/AppSettings/PathPrecedenceSetting.cs index 59f2f0d1..95bbe21d 100644 --- a/src/gsudo/AppSettings/PathPrecedenceSetting.cs +++ b/src/gsudo/AppSettings/PathPrecedenceSetting.cs @@ -14,7 +14,7 @@ internal class PathPrecedenceSetting : RegistrySetting { public PathPrecedenceSetting(): base("PathPrecedence", false, bool.Parse, RegistrySettingScope.GlobalOnly, - description: "Prioritize gsudo over Microsoft Sudo in the PATH environment variable.") + description: "Prioritize gsudo over Microsoft Sudo in the PATH environment variable") { } diff --git a/src/gsudo/AppSettings/Settings.cs b/src/gsudo/AppSettings/Settings.cs index e3be1cc5..f2728d7a 100644 --- a/src/gsudo/AppSettings/Settings.cs +++ b/src/gsudo/AppSettings/Settings.cs @@ -19,7 +19,7 @@ class Settings = new RegistrySetting(nameof(CacheMode), CredentialsCache.CacheMode.Explicit, deserializer: ExtensionMethods.ParseEnum< CacheMode>, scope: RegistrySettingScope.GlobalOnly, - description: "Defines how gsudo credentials cache works: Auto, Explicit (Manual), Disabled" ); + description: "Defines how gsudo credentials cache works: Auto, Explicit (default), Disabled" ); public static RegistrySetting CacheDuration { get; } = new RegistrySetting(nameof(CacheDuration), @@ -33,14 +33,14 @@ class Settings = new RegistrySetting(nameof(PipedPrompt), defaultValue: DefaultAsciiPrompt, deserializer: (s) => s, - description: "Prompt to be used when gsudo uses piped mode." + description: "CMD Prompt to be used when gsudo uses piped mode" ); public static RegistrySetting Prompt { get; } = new RegistrySetting(nameof(Prompt), defaultValue: GetPromptDefaultValue, deserializer: (s) => s, - description: "Prompt to be used when gsudo uses standard mode." + description: "CMD Prompt to be used when gsudo uses standard mode" ); public static RegistrySetting LogLevel { get; } @@ -54,7 +54,7 @@ class Settings = new RegistrySetting(nameof(ForcePipedConsole), defaultValue: false, deserializer: bool.Parse, - description: "Forces gsudo to use legacy piped mode. Not recommended." + description: "Forces gsudo to use legacy piped mode. Not recommended" ); public static RegistrySetting ForceAttachedConsole { get; } @@ -97,7 +97,7 @@ class Settings defaultValue: false, deserializer: bool.Parse, scope: RegistrySettingScope.GlobalOnly, - description: "Elevates but with the input handle closed. More secure, less convenient. To be implemented soon also as --disableInput" + description: "Elevates but with the input handle closed. More secure, but less convenient. Same as --disableInput" ); public static RegistrySetting ExceptionList { get; } = @@ -113,14 +113,14 @@ class Settings defaultValue: false, deserializer: bool.Parse, scope: RegistrySettingScope.Any, - description: "Always elevate in new window. Same as --new"); + description: "Always elevate in new window. (Equivalent to --new)"); public static RegistrySetting NewWindow_CloseBehaviour { get; } = new RegistrySetting(nameof(NewWindow_CloseBehaviour), defaultValue: CloseBehaviour.OsDefault, deserializer: ExtensionMethods.ParseEnum, scope: RegistrySettingScope.Any, - description: "When elevating in new window, let the window auto-close (OsDefault), KeepShellOpen or PressKeyToClose" + description: "When elevating in a new window, defines what happens when the process ends: OsDefault (let the window auto-close), KeepShellOpen or PressKeyToClose" ); public static RegistrySetting PathOverrideSetting = new PathPrecedenceSetting(); diff --git a/src/gsudo/Commands/ConfigCommand.cs b/src/gsudo/Commands/ConfigCommand.cs index 509ed3c4..3c233783 100644 --- a/src/gsudo/Commands/ConfigCommand.cs +++ b/src/gsudo/Commands/ConfigCommand.cs @@ -32,16 +32,17 @@ public Task Execute() if (key == null) { + Console.ForegroundColor = ConsoleColor. Yellow; // print all configs Descriptions foreach (var k in Settings.AllKeys) { - Console.ForegroundColor = ConsoleColor.Yellow; if (Settings.LogLevel <= LogLevel.Info) { Console.WriteLine($"# {k.Value.Name}: {k.Value.Description}"); } - Console.ResetColor(); } + Console.WriteLine(); + Console.ResetColor(); // print all config values foreach (var k in Settings.AllKeys) diff --git a/src/gsudo/Commands/HelpCommand.cs b/src/gsudo/Commands/HelpCommand.cs index 490ac771..6cf723fc 100644 --- a/src/gsudo/Commands/HelpCommand.cs +++ b/src/gsudo/Commands/HelpCommand.cs @@ -63,6 +63,7 @@ gsudo status {key} [--no-output]\tShows status filtered by json {key}. Boolean k --debug Enable debug mode. --copyns Connect network drives to the elevated user. Warning: Interactive asks for credentials --copyev (deprecated) Copy all environment variables to the elevated process. + --chdir {dir} Change the current directory to {dir} before running the command. Configuration: gsudo config\t\t\t\tShow current config settings & values. diff --git a/src/gsudo/Commands/RunCommand.cs b/src/gsudo/Commands/RunCommand.cs index a58af99e..9304fe29 100644 --- a/src/gsudo/Commands/RunCommand.cs +++ b/src/gsudo/Commands/RunCommand.cs @@ -41,7 +41,7 @@ public async Task Execute() if (isElevationRequired & SecurityHelper.GetCurrentIntegrityLevel() < (int)IntegrityLevel.Medium) throw new ApplicationException("Sorry, gsudo doesn't allow to elevate from low integrity level."); // This message is not a security feature, but a nicer error message. It would have failed anyway since the named pipe's ACL restricts it. - if (isRunningAsDesiredUser && isShellElevation && !InputArguments.NewWindow) + if (isRunningAsDesiredUser && isShellElevation && !InputArguments.NewWindow && !InputArguments.Direct && InputArguments.StartingDirectory == null) throw new ApplicationException("Already running as the specified user/permission-level (and no command specified). Exiting..."); var elevationMode = GetElevationMode(); @@ -58,7 +58,7 @@ public async Task Execute() { FileName = commandBuilder.GetExeName(), Arguments = commandBuilder.GetArgumentsAsString(), - StartFolder = Environment.CurrentDirectory, + StartFolder = InputArguments.StartingDirectory ?? Environment.CurrentDirectory, NewWindow = InputArguments.NewWindow, Wait = (!commandBuilder.IsWindowsApp && !InputArguments.NewWindow) || InputArguments.Wait, Mode = elevationMode, @@ -69,7 +69,7 @@ public async Task Execute() IsInputRedirected = Console.IsInputRedirected }; - if (isElevationRequired && Settings.SecurityEnforceUacIsolation) + if (isElevationRequired && (Settings.SecurityEnforceUacIsolation || InputArguments.DisableInput)) AdjustUacIsolationRequest(elevationRequest, isShellElevation); SetRequestPrompt(elevationRequest); @@ -223,9 +223,8 @@ private void AdjustUacIsolationRequest(ElevationRequest elevationRequest, bool i } else { - // force raw mode (that disables user input with SecurityEnforceUacIsolation) - elevationRequest.Mode = ElevationRequest.ConsoleMode.Piped; - Logger.Instance.Log("User Input disabled because of SecurityEnforceUacIsolation. Press Ctrl-C three times to abort. Or use -n argument to elevate in new window.", LogLevel.Info); + // Disables user input with SecurityEnforceUacIsolation + elevationRequest.DisableInput = true; } } } diff --git a/src/gsudo/ElevationRequest.cs b/src/gsudo/ElevationRequest.cs index 37fc55ed..43650c9c 100644 --- a/src/gsudo/ElevationRequest.cs +++ b/src/gsudo/ElevationRequest.cs @@ -20,8 +20,9 @@ class ElevationRequest public bool KillCache { get; set; } public IntegrityLevel IntegrityLevel { get; set; } - public bool IsInputRedirected { get; set; } - + public bool IsInputRedirected { get; set; } + public bool DisableInput { get; set; } + [Serializable] internal enum ConsoleMode { /// diff --git a/src/gsudo/Helpers/CommandLineParser.cs b/src/gsudo/Helpers/CommandLineParser.cs index cb405d2f..ec215367 100644 --- a/src/gsudo/Helpers/CommandLineParser.cs +++ b/src/gsudo/Helpers/CommandLineParser.cs @@ -1,8 +1,10 @@ using gsudo.AppSettings; using gsudo.Commands; +using gsudo.Native; using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Security.Principal; @@ -132,7 +134,6 @@ ICommand ParseOption(string argChar, string argWord, out bool skipRemainingChars else if (match(null, "--close")) { InputArguments.CloseNewWindow = true; InputArguments.KeepWindowOpen = false; InputArguments.KeepShellOpen = false; } else if (match("s", "--system")) { InputArguments.RunAsSystem = true; } - else if (match("d", "--direct")) { InputArguments.Direct = true; } else if (match("k", "--reset-timestamp")) { InputArguments.KillCache = true; } else if (match(null, "--global")) { InputArguments.Global = true; } else if (match(null, "--ti")) { InputArguments.TrustedInstaller = InputArguments.RunAsSystem = true; } @@ -145,6 +146,25 @@ ICommand ParseOption(string argChar, string argWord, out bool skipRemainingChars else if (match(null, "--debug")) { Settings.LogLevel.Value = LogLevel.All; InputArguments.Debug = true; } else if (match("v", "--version")) { return new ShowVersionHelpCommand(); } else if (match("h", "--help")) return new HelpCommand(); + + // ms-sudo compat: + else if (match(null, "--preserve-env")) { Settings.CopyEnvironmentVariables.Value = true; } + else if (match(null, "--new-window")) { InputArguments.NewWindow = true; } + // case sensitive -D {dir} + else if (argChar == "D" && argWord == "-D" && FileApi.PathExists(args.FirstOrDefault())) { InputArguments.StartingDirectory = DeQueueArg(); } + else if (match(null, "--chdir")) + { + InputArguments.StartingDirectory = DeQueueArg(); + if (!FileApi.PathExists(InputArguments.StartingDirectory)) + { + throw new ApplicationException($"Invalid directory: {InputArguments.StartingDirectory}"); + } + } + else if (match(null, "--inline")) { InputArguments.NewWindow = false; } + else if (argWord.In("--disable-input", "--disableInput")) { InputArguments.DisableInput = true; } + + // rest + else if (match("d", "--direct")) { InputArguments.Direct = true; } else if (argWord.StartsWith("-", StringComparison.Ordinal)) { if (argChar != null) diff --git a/src/gsudo/Helpers/CommandToRunAdapter.cs b/src/gsudo/Helpers/CommandToRunAdapter.cs index 99968b03..209b756b 100644 --- a/src/gsudo/Helpers/CommandToRunAdapter.cs +++ b/src/gsudo/Helpers/CommandToRunAdapter.cs @@ -403,14 +403,15 @@ internal void Build() postCommands.Add("exit /b !errl!"); } - bool bNetworkfolder = Environment.CurrentDirectory.StartsWith(@"\\", StringComparison.Ordinal); + string startupFolder = InputArguments.StartingDirectory ?? Environment.CurrentDirectory; + bool bNetworkfolder = startupFolder.StartsWith(@"\\", StringComparison.Ordinal); bool bIsCmdExe = ArgumentsHelper.UnQuote(command.First()).EndsWith("cmd.exe", StringComparison.OrdinalIgnoreCase); if (bNetworkfolder && (bIsCmdExe || mustWrap)) { - Logger.Instance.Log($"The current directory '{Environment.CurrentDirectory}' is a network folder. Mapping as a network drive.", LogLevel.Debug); + Logger.Instance.Log($"The path '{startupFolder}' is a network folder. Mapping as a network drive.", LogLevel.Debug); // Prepending PUSHD command. It maps network folders magically! - preCommands.Insert(0, $"pushd \"{Environment.CurrentDirectory}\""); + preCommands.Insert(0, $"pushd \"{startupFolder}\""); postCommands.Add("popd"); // And set current directory to local folder to avoid CMD warning message Environment.CurrentDirectory = Environment.GetEnvironmentVariable("SystemRoot"); diff --git a/src/gsudo/Helpers/ProcessFactory.cs b/src/gsudo/Helpers/ProcessFactory.cs index ebb10e47..28923cbd 100644 --- a/src/gsudo/Helpers/ProcessFactory.cs +++ b/src/gsudo/Helpers/ProcessFactory.cs @@ -65,7 +65,7 @@ public static Process StartRedirected(string fileName, string arguments, string return process; } - public static Process StartAttached(string filename, string arguments) + public static Process StartAttached(string filename, string arguments, bool disableInput = false) { Logger.Instance.Log($"Process Start: {filename} {arguments}", LogLevel.Debug); var process = new Process(); @@ -74,7 +74,17 @@ public static Process StartAttached(string filename, string arguments) Arguments = arguments, UseShellExecute = false, }; + + if (disableInput) + { + process.StartInfo.RedirectStandardInput = true; + } + process.Start(); + + if (disableInput) + process.StandardInput.Close(); + return process; } @@ -332,8 +342,10 @@ private static SafeProcessHandle CreateProcessWithToken(IntPtr newToken, string return new SafeProcessHandle(processInformation.hProcess, true); } - internal static void CreateProcessForTokenReplacement(string lpApplicationName, string args, ProcessApi.CreateProcessFlags dwCreationFlags, out SafeProcessHandle processHandle, out SafeHandle threadHandle, out int processId) - { + internal static void CreateProcessForTokenReplacement(string lpApplicationName, string args, ProcessApi.CreateProcessFlags dwCreationFlags, out SafeProcessHandle processHandle, out SafeHandle threadHandle, out int processId, bool bDisableInput) + { + var currentProcessHandle = ProcessApi.GetCurrentProcess(); + var sInfoEx = new ProcessApi.STARTUPINFOEX(); sInfoEx.StartupInfo.cb = Marshal.SizeOf(sInfoEx); @@ -342,6 +354,56 @@ internal static void CreateProcessForTokenReplacement(string lpApplicationName, pSec.nLength = Marshal.SizeOf(pSec); tSec.nLength = Marshal.SizeOf(tSec); + if (bDisableInput) + { + dwCreationFlags |= CreateProcessFlags.EXTENDED_STARTUPINFO_PRESENT; + var STARTF_USESTDHANDLES = 0x00000100; + + sInfoEx.StartupInfo.dwFlags = STARTF_USESTDHANDLES; + + uint DUPLICATE_SAME_ACCESS = 0x00000002; + + if (!DuplicateHandle( + currentProcessHandle, // Source process handle is the current process + ConsoleApi.GetStdHandle(ConsoleApi.STD_INPUT_HANDLE), // The handle to duplicate + currentProcessHandle, // Target process handle is also the current process + out var inputHandle, // The duplicated handle with desired access rights + DUPLICATE_SAME_ACCESS, // Desired access: PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE | PROCESS_TERMINATE + true, // The handle is not inheritable + 0)) // dwOptions: auto close pInfo.hProcess. + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + if (!DuplicateHandle( + currentProcessHandle, // Source process handle is the current process + ConsoleApi.GetStdHandle(ConsoleApi.STD_OUTPUT_HANDLE), // The handle to duplicate + currentProcessHandle, // Target process handle is also the current process + out var outputHandle, // The duplicated handle with desired access rights + DUPLICATE_SAME_ACCESS, // Desired access: PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE | PROCESS_TERMINATE + true, // The handle is not inheritable + 0)) // dwOptions: auto close pInfo.hProcess. + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + if (!DuplicateHandle( + currentProcessHandle, // Source process handle is the current process + ConsoleApi.GetStdHandle(ConsoleApi.STD_ERROR_HANDLE), // The handle to duplicate + currentProcessHandle, // Target process handle is also the current process + out var errorHandle, // The duplicated handle with desired access rights + DUPLICATE_SAME_ACCESS, // Desired access: PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE | PROCESS_TERMINATE + true, // The handle is not inheritable + 0)) // dwOptions: auto close pInfo.hProcess. + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + sInfoEx.StartupInfo.hStdInput = IntPtr.Zero; + sInfoEx.StartupInfo.hStdOutput = outputHandle; + sInfoEx.StartupInfo.hStdError = errorHandle; + } + // Set a more restrictive Security Descriptor: // - This code runs at medium integrity, so we dont have permissions to change the SDACL to High integrity level. // - We will do that in TokenSwitcher.ReplaceProcessToken. @@ -358,12 +420,10 @@ internal static void CreateProcessForTokenReplacement(string lpApplicationName, PROCESS_INFORMATION pInfo; Logger.Instance.Log($"Creating target process: {lpApplicationName} {args}", LogLevel.Debug); - if (!ProcessApi.CreateProcess(null, command, ref pSec, ref tSec, false, dwCreationFlags, IntPtr.Zero, null, ref sInfoEx, out pInfo)) + if (!ProcessApi.CreateProcess(null, command, ref pSec, ref tSec, true, dwCreationFlags, IntPtr.Zero, null, ref sInfoEx, out pInfo)) { throw new Win32Exception((int)ConsoleApi.GetLastError()); - } - - var currentProcessHandle = ProcessApi.GetCurrentProcess(); + } if (!DuplicateHandle( currentProcessHandle, // Source process handle is the current process diff --git a/src/gsudo/Helpers/ServiceHelper.cs b/src/gsudo/Helpers/ServiceHelper.cs index bff39a0d..c95eed90 100644 --- a/src/gsudo/Helpers/ServiceHelper.cs +++ b/src/gsudo/Helpers/ServiceHelper.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Security; using System.Security.Principal; +using System.Threading; using System.Threading.Tasks; namespace gsudo.Helpers @@ -175,13 +176,21 @@ internal static SafeProcessHandle StartService(int? allowedPid, TimeSpan? cacheD else { if (SecurityHelper.IsMemberOfLocalAdmins() && InputArguments.GetIntegrityLevel() >= IntegrityLevel.High) + { + // UAC Popup doesnt always have focus, so we try to bring it to the front. + new Thread(UACWindowFocusHelper.FocusUacWindow).Start(); + ret = ProcessFactory.StartElevatedDetached(ownExe, commandLine, !InputArguments.Debug).GetSafeProcessHandle(); + } else ret = ProcessFactory.StartDetached(ownExe, commandLine, null, !InputArguments.Debug).GetSafeProcessHandle(); } } else { + // UAC Popup doesnt always have focus, so we try to bring it to the front. + new Thread(UACWindowFocusHelper.FocusUacWindow).Start(); + ret = ProcessFactory.StartElevatedDetached(ownExe, commandLine, !InputArguments.Debug).GetSafeProcessHandle(); } diff --git a/src/gsudo/Helpers/UACWindowFocusHelper.cs b/src/gsudo/Helpers/UACWindowFocusHelper.cs new file mode 100644 index 00000000..f2a98519 --- /dev/null +++ b/src/gsudo/Helpers/UACWindowFocusHelper.cs @@ -0,0 +1,38 @@ +using gsudo.Native; +using System; +using System.Runtime.InteropServices; + +namespace gsudo.Helpers +{ + internal class UACWindowFocusHelper + { + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + + internal static void FocusUacWindow() + { + try + { + for (int i = 0; i < 10; i++) + { + // Wait a moment to allow the UAC prompt to appear + System.Threading.Thread.Sleep(100); + + // Find the UAC window + string classname = "Credential Dialog Xaml Host"; // Found using Visual Studio spyxx_amd64.exe, this is the value for Windows 10 & 11. + IntPtr uacWindow = FindWindow(classname, null); + if (uacWindow != IntPtr.Zero) + { + // Set focus to the UAC window + WindowApi.SetForegroundWindow(uacWindow); + return; + } + } + } + catch (Exception ex) + { + Logger.Instance.Log("Error searching for UAC Window: " + ex.ToString(), LogLevel.Debug); + } + } + } +} diff --git a/src/gsudo/InputParameters.cs b/src/gsudo/InputParameters.cs index e117f7b4..4f011a57 100644 --- a/src/gsudo/InputParameters.cs +++ b/src/gsudo/InputParameters.cs @@ -42,6 +42,10 @@ public static class InputArguments // SID of User to Impersonate public static string UserSid { get; private set; } + // Starting Directory for the new process + public static string StartingDirectory { get; internal set; } + public static bool DisableInput { get; internal set; } + public static IntegrityLevel GetIntegrityLevel() => (RunAsSystem ? gsudo.IntegrityLevel.System : IntegrityLevel ?? gsudo.IntegrityLevel.High); internal static void Clear() // added for tests repeatability diff --git a/src/gsudo/Native/FileApi.cs b/src/gsudo/Native/FileApi.cs index 7bcaba01..e3107c69 100644 --- a/src/gsudo/Native/FileApi.cs +++ b/src/gsudo/Native/FileApi.cs @@ -78,6 +78,9 @@ public static extern bool FindNextFile(IntPtr hFindFile, out WIN32_FIND_DATA [DllImport("kernel32.dll", SetLastError = true)] public static extern bool FindClose(IntPtr hFindFile); + [DllImport("shlwapi", EntryPoint = "PathFileExists", CharSet = CharSet.Unicode)] + public static extern bool PathExists(string path); + #endregion #region Network Drives diff --git a/src/gsudo/ProcessHosts/AttachedConsoleHost.cs b/src/gsudo/ProcessHosts/AttachedConsoleHost.cs index d18b02aa..20e9378d 100644 --- a/src/gsudo/ProcessHosts/AttachedConsoleHost.cs +++ b/src/gsudo/ProcessHosts/AttachedConsoleHost.cs @@ -21,9 +21,6 @@ public async Task Start(Connection connection, ElevationRequest elevationRequest { var exitCode = 0; - if (Settings.SecurityEnforceUacIsolation) - throw new Exception("Attached mode not supported when SecurityEnforceUacIsolation is set."); - try { Native.ConsoleApi.FreeConsole(); @@ -40,10 +37,10 @@ public async Task Start(Connection connection, ElevationRequest elevationRequest } catch (UnauthorizedAccessException ex) { - throw new ApplicationException($"User \"{WindowsIdentity.GetCurrent().Name}\" can not access current directory \"{elevationRequest.StartFolder}\""); + throw new ApplicationException($"User \"{WindowsIdentity.GetCurrent().Name}\" can not access directory \"{elevationRequest.StartFolder}\""); } - var process = Helpers.ProcessFactory.StartAttached(elevationRequest.FileName, elevationRequest.Arguments); + var process = Helpers.ProcessFactory.StartAttached(elevationRequest.FileName, elevationRequest.Arguments, elevationRequest.DisableInput); WaitHandle.WaitAny(new WaitHandle[] { process.GetProcessWaitHandle(), connection.DisconnectedWaitHandle }); if (process.HasExited) diff --git a/src/gsudo/ProcessHosts/PipedProcessHost.cs b/src/gsudo/ProcessHosts/PipedProcessHost.cs index 818b98b4..904f3777 100644 --- a/src/gsudo/ProcessHosts/PipedProcessHost.cs +++ b/src/gsudo/ProcessHosts/PipedProcessHost.cs @@ -43,7 +43,7 @@ public async Task Start(Connection connection, ElevationRequest request) var t3 = new StreamReader(connection.DataStream, Settings.Encoding).ConsumeOutput((s) => WriteToProcessStdIn(s, process)); var t4 = new StreamReader(connection.ControlStream, Settings.Encoding).ConsumeOutput((s) => HandleControl(s, process)); - if (Settings.SecurityEnforceUacIsolation) + if (Settings.SecurityEnforceUacIsolation || request.DisableInput) process.StandardInput.Close(); WaitHandle.WaitAny(new WaitHandle[] { process.GetProcessWaitHandle(), connection.DisconnectedWaitHandle }); @@ -104,7 +104,7 @@ private async Task WriteToProcessStdIn(string s, Process process) else lastInboundMessage += s; - if (!Settings.SecurityEnforceUacIsolation) + if (!Settings.SecurityEnforceUacIsolation && !_request.DisableInput) { await process.StandardInput.WriteAsync(s).ConfigureAwait(false); } diff --git a/src/gsudo/ProcessHosts/TokenSwitchHost.cs b/src/gsudo/ProcessHosts/TokenSwitchHost.cs index c1bdc0c6..03c8a6c8 100644 --- a/src/gsudo/ProcessHosts/TokenSwitchHost.cs +++ b/src/gsudo/ProcessHosts/TokenSwitchHost.cs @@ -16,9 +16,6 @@ class TokenSwitchHost : IProcessHost public async Task Start(Connection connection, ElevationRequest elevationRequest) { - if (Settings.SecurityEnforceUacIsolation && !elevationRequest.NewWindow) - throw new Exception("TokenSwitch mode not supported when SecurityEnforceUacIsolation is set."); - try { TokenSwitcher.ReplaceProcessToken(elevationRequest); diff --git a/src/gsudo/ProcessRenderers/TokenSwitchRenderer.cs b/src/gsudo/ProcessRenderers/TokenSwitchRenderer.cs index 8a6214dd..2ac4af48 100644 --- a/src/gsudo/ProcessRenderers/TokenSwitchRenderer.cs +++ b/src/gsudo/ProcessRenderers/TokenSwitchRenderer.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Security.Principal; using System.Threading; using System.Threading.Tasks; @@ -27,9 +28,8 @@ class TokenSwitchRenderer : IProcessRenderer internal TokenSwitchRenderer(Connection connection, ElevationRequest elevationRequest) { - if (Settings.SecurityEnforceUacIsolation && !elevationRequest.NewWindow) - throw new Exception("TokenSwitch mode not supported when SecurityEnforceUacIsolation is set."); - + bool disableInput = elevationRequest.DisableInput; + _connection = connection; _elevationRequest = elevationRequest; ConsoleHelper.SetPrompt(elevationRequest); @@ -55,9 +55,18 @@ internal TokenSwitchRenderer(Connection connection, ElevationRequest elevationRe // Hack not needed if we are already calling CMD exeName = elevationRequest.FileName; args = elevationRequest.Arguments; + } + + try + { + System.Environment.CurrentDirectory = elevationRequest.StartFolder; + } + catch (UnauthorizedAccessException ex) + { + throw new ApplicationException($"User \"{WindowsIdentity.GetCurrent().Name}\" can not access directory \"{elevationRequest.StartFolder}\""); } - ProcessFactory.CreateProcessForTokenReplacement(exeName, args, dwCreationFlags, out _processHandle, out _threadHandle, out int processId); + ProcessFactory.CreateProcessForTokenReplacement(exeName, args, dwCreationFlags, out _processHandle, out _threadHandle, out int processId, disableInput); elevationRequest.TargetProcessId = processId; if (!elevationRequest.NewWindow) diff --git a/src/gsudo/ProcessRenderers/VTClientRenderer.cs b/src/gsudo/ProcessRenderers/VTClientRenderer.cs index 13739f57..05afacdf 100644 --- a/src/gsudo/ProcessRenderers/VTClientRenderer.cs +++ b/src/gsudo/ProcessRenderers/VTClientRenderer.cs @@ -33,7 +33,7 @@ public VTClientRenderer(Connection connection, ElevationRequest elevationRequest public async Task Start() { - if (Settings.SecurityEnforceUacIsolation) + if (Settings.SecurityEnforceUacIsolation || InputArguments.DisableInput) throw new NotSupportedException("VT Mode not supported when SecurityEnforceUacIsolation=true"); Console.OutputEncoding = System.Text.Encoding.UTF8; diff --git a/src/gsudo/Program.cs b/src/gsudo/Program.cs index 63b5f1bd..9e443bdd 100644 --- a/src/gsudo/Program.cs +++ b/src/gsudo/Program.cs @@ -74,8 +74,8 @@ private static async Task Start() { if (cmd.GetType() == typeof(ServiceCommand)) { - Console.WriteLine("Service shutdown. This window will close in 15 seconds"); - System.Threading.Thread.Sleep(15000); + Console.WriteLine("Service shutdown. This window will close in 10 seconds"); + System.Threading.Thread.Sleep(10000); } } }