From 66f67b452fe91206858e4077dbc545214d02450c Mon Sep 17 00:00:00 2001 From: Gerardo Grignoli Date: Sun, 26 May 2024 15:29:13 -0300 Subject: [PATCH 1/7] Force dotnet 7 sdk for builds to fix build errors after installing 8.0 --- global.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 global.json diff --git a/global.json b/global.json new file mode 100644 index 0000000..f0858b8 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "7.0.0", + "rollForward": "latestFeature" + } +} \ No newline at end of file From 6ebb465ac7fea1f858e79abdd7cdcbcedf77e81d Mon Sep 17 00:00:00 2001 From: Gerardo Grignoli Date: Sun, 26 May 2024 15:46:30 -0300 Subject: [PATCH 2/7] - Accept `--chdir {directory}` or uppercase `-D {directory}` argument for compat with MS-SUDO. - Included new ms-sudo compatible options aliases (`--preserve-env`, `--new-window`). --- src/gsudo/Commands/RunCommand.cs | 4 ++-- src/gsudo/Helpers/CommandLineParser.cs | 12 +++++++++++- src/gsudo/InputParameters.cs | 3 +++ src/gsudo/Native/FileApi.cs | 3 +++ src/gsudo/ProcessHosts/AttachedConsoleHost.cs | 2 +- src/gsudo/ProcessRenderers/TokenSwitchRenderer.cs | 10 ++++++++++ 6 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/gsudo/Commands/RunCommand.cs b/src/gsudo/Commands/RunCommand.cs index a58af99..6c7f6ac 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, diff --git a/src/gsudo/Helpers/CommandLineParser.cs b/src/gsudo/Helpers/CommandLineParser.cs index cb405d2..f052f38 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,15 @@ 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; } + else if (argChar == "D" && argWord == "-D" && FileApi.PathExists(args.FirstOrDefault())) { InputArguments.StartingDirectory = DeQueueArg(); } + else if (match(null, "--chdir")) { InputArguments.StartingDirectory = DeQueueArg(); } + + // rest + else if (match("d", "--direct")) { InputArguments.Direct = true; } else if (argWord.StartsWith("-", StringComparison.Ordinal)) { if (argChar != null) diff --git a/src/gsudo/InputParameters.cs b/src/gsudo/InputParameters.cs index e117f7b..f8835f0 100644 --- a/src/gsudo/InputParameters.cs +++ b/src/gsudo/InputParameters.cs @@ -42,6 +42,9 @@ 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 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 7bcaba0..e3107c6 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 d18b02a..bcf3453 100644 --- a/src/gsudo/ProcessHosts/AttachedConsoleHost.cs +++ b/src/gsudo/ProcessHosts/AttachedConsoleHost.cs @@ -40,7 +40,7 @@ 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); diff --git a/src/gsudo/ProcessRenderers/TokenSwitchRenderer.cs b/src/gsudo/ProcessRenderers/TokenSwitchRenderer.cs index 8a6214d..951cca4 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; @@ -55,6 +56,15 @@ 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); From 687c099005a1f5fc0797935fa6c821b44172be61 Mon Sep 17 00:00:00 2001 From: Gerardo Grignoli Date: Sun, 26 May 2024 16:28:17 -0300 Subject: [PATCH 3/7] Accept --inline arg --- src/gsudo/Helpers/CommandLineParser.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gsudo/Helpers/CommandLineParser.cs b/src/gsudo/Helpers/CommandLineParser.cs index f052f38..73c4685 100644 --- a/src/gsudo/Helpers/CommandLineParser.cs +++ b/src/gsudo/Helpers/CommandLineParser.cs @@ -152,6 +152,7 @@ ICommand ParseOption(string argChar, string argWord, out bool skipRemainingChars else if (match(null, "--new-window")) { InputArguments.NewWindow = true; } else if (argChar == "D" && argWord == "-D" && FileApi.PathExists(args.FirstOrDefault())) { InputArguments.StartingDirectory = DeQueueArg(); } else if (match(null, "--chdir")) { InputArguments.StartingDirectory = DeQueueArg(); } + else if (match(null, "--inline")) { InputArguments.NewWindow = false; } // rest else if (match("d", "--direct")) { InputArguments.Direct = true; } From 00f4fd608882d717c47e08bb33c3b65f4ea47571 Mon Sep 17 00:00:00 2001 From: Gerardo Grignoli Date: Sun, 26 May 2024 17:41:09 -0300 Subject: [PATCH 4/7] Fixes for `--chdir {dir}`, like path validation. Use Pushd when {dir} is a network path. Updated help, README.md and docs. --- README.md | 3 ++- docs/docs/usage/usage.md | 1 + src/gsudo/Commands/HelpCommand.cs | 1 + src/gsudo/Helpers/CommandLineParser.cs | 9 ++++++++- src/gsudo/Helpers/CommandToRunAdapter.cs | 7 ++++--- 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d1379bc..03e9072 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 69e67da..4200d73 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/src/gsudo/Commands/HelpCommand.cs b/src/gsudo/Commands/HelpCommand.cs index 490ac77..6cf723f 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/Helpers/CommandLineParser.cs b/src/gsudo/Helpers/CommandLineParser.cs index 73c4685..588e093 100644 --- a/src/gsudo/Helpers/CommandLineParser.cs +++ b/src/gsudo/Helpers/CommandLineParser.cs @@ -151,7 +151,14 @@ ICommand ParseOption(string argChar, string argWord, out bool skipRemainingChars else if (match(null, "--preserve-env")) { Settings.CopyEnvironmentVariables.Value = true; } else if (match(null, "--new-window")) { InputArguments.NewWindow = true; } else if (argChar == "D" && argWord == "-D" && FileApi.PathExists(args.FirstOrDefault())) { InputArguments.StartingDirectory = DeQueueArg(); } - else if (match(null, "--chdir")) { 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; } // rest diff --git a/src/gsudo/Helpers/CommandToRunAdapter.cs b/src/gsudo/Helpers/CommandToRunAdapter.cs index 99968b0..209b756 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"); From 3e00c6b3492cd019da91295eab587a1710e22d10 Mon Sep 17 00:00:00 2001 From: Gerardo Grignoli Date: Sun, 26 May 2024 19:01:31 -0300 Subject: [PATCH 5/7] Added --disable-input to activate `gsudo config SecurityEnforceUacIsolation true` but per call, for compat with ms-sudo. - Incorporated `DisableInput` handling in Attach and TokenSwitch Modes besides piped mode. - Updated Program.cs: Reduced shutdown delay message from 15 to 10 seconds for ServiceCommand, to reduce msbuild errors while building. --- src/gsudo/AppSettings/Settings.cs | 2 +- src/gsudo/Commands/RunCommand.cs | 7 +- src/gsudo/ElevationRequest.cs | 5 +- src/gsudo/Helpers/CommandLineParser.cs | 2 + src/gsudo/Helpers/ProcessFactory.cs | 74 +++++++++++++++++-- src/gsudo/InputParameters.cs | 1 + src/gsudo/ProcessHosts/AttachedConsoleHost.cs | 5 +- src/gsudo/ProcessHosts/PipedProcessHost.cs | 4 +- src/gsudo/ProcessHosts/TokenSwitchHost.cs | 3 - .../ProcessRenderers/TokenSwitchRenderer.cs | 7 +- .../ProcessRenderers/VTClientRenderer.cs | 2 +- src/gsudo/Program.cs | 4 +- 12 files changed, 86 insertions(+), 30 deletions(-) diff --git a/src/gsudo/AppSettings/Settings.cs b/src/gsudo/AppSettings/Settings.cs index e3be1cc..f7ebf41 100644 --- a/src/gsudo/AppSettings/Settings.cs +++ b/src/gsudo/AppSettings/Settings.cs @@ -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; } = diff --git a/src/gsudo/Commands/RunCommand.cs b/src/gsudo/Commands/RunCommand.cs index 6c7f6ac..9304fe2 100644 --- a/src/gsudo/Commands/RunCommand.cs +++ b/src/gsudo/Commands/RunCommand.cs @@ -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 37fc55e..43650c9 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 588e093..ec21536 100644 --- a/src/gsudo/Helpers/CommandLineParser.cs +++ b/src/gsudo/Helpers/CommandLineParser.cs @@ -150,6 +150,7 @@ ICommand ParseOption(string argChar, string argWord, out bool skipRemainingChars // 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")) { @@ -160,6 +161,7 @@ ICommand ParseOption(string argChar, string argWord, out bool skipRemainingChars } } 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; } diff --git a/src/gsudo/Helpers/ProcessFactory.cs b/src/gsudo/Helpers/ProcessFactory.cs index ebb10e4..28923cb 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/InputParameters.cs b/src/gsudo/InputParameters.cs index f8835f0..4f011a5 100644 --- a/src/gsudo/InputParameters.cs +++ b/src/gsudo/InputParameters.cs @@ -44,6 +44,7 @@ public static class InputArguments // 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); diff --git a/src/gsudo/ProcessHosts/AttachedConsoleHost.cs b/src/gsudo/ProcessHosts/AttachedConsoleHost.cs index bcf3453..20e9378 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(); @@ -43,7 +40,7 @@ public async Task Start(Connection connection, ElevationRequest elevationRequest 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 818b98b..904f377 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 c1bdc0c..03c8a6c 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 951cca4..2ac4af4 100644 --- a/src/gsudo/ProcessRenderers/TokenSwitchRenderer.cs +++ b/src/gsudo/ProcessRenderers/TokenSwitchRenderer.cs @@ -28,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); @@ -67,7 +66,7 @@ internal TokenSwitchRenderer(Connection connection, ElevationRequest elevationRe 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 13739f5..05afacd 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 63b5f1b..9e443bd 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); } } } From 87226a33fb23e38cc5b42c5e122db77d1f77b891 Mon Sep 17 00:00:00 2001 From: Gerardo Grignoli Date: Mon, 27 May 2024 22:57:05 -0300 Subject: [PATCH 6/7] Verbiage fixes --- src/gsudo/AppSettings/PathPrecedenceSetting.cs | 2 +- src/gsudo/AppSettings/Settings.cs | 12 ++++++------ src/gsudo/Commands/ConfigCommand.cs | 5 +++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/gsudo/AppSettings/PathPrecedenceSetting.cs b/src/gsudo/AppSettings/PathPrecedenceSetting.cs index 59f2f0d..95bbe21 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 f7ebf41..f2728d7 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; } @@ -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 509ed3c..3c23378 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) From 46a158712abf5fa06401c8288fa8e68e1e3a3348 Mon Sep 17 00:00:00 2001 From: Gerardo Grignoli Date: Mon, 27 May 2024 23:16:51 -0300 Subject: [PATCH 7/7] feat: Improve UAC prompt handling by focusing the UAC window, useful when UAC set as Do Not Dim. Fixes #85 --- src/gsudo/Helpers/ServiceHelper.cs | 9 ++++++ src/gsudo/Helpers/UACWindowFocusHelper.cs | 38 +++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 src/gsudo/Helpers/UACWindowFocusHelper.cs diff --git a/src/gsudo/Helpers/ServiceHelper.cs b/src/gsudo/Helpers/ServiceHelper.cs index bff39a0..c95eed9 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 0000000..f2a9851 --- /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); + } + } + } +}