Skip to content

Commit

Permalink
Merge pull request #355 from gerardog/feature/ch-dir
Browse files Browse the repository at this point in the history
Added --chdir and other ms-sudo compatibilities.
  • Loading branch information
gerardog committed May 28, 2024
2 parents bae3da5 + 46a1587 commit 7e7dc33
Show file tree
Hide file tree
Showing 22 changed files with 195 additions and 47 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions docs/docs/usage/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
```

Expand Down
6 changes: 6 additions & 0 deletions global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"sdk": {
"version": "7.0.0",
"rollForward": "latestFeature"
}
}
2 changes: 1 addition & 1 deletion src/gsudo/AppSettings/PathPrecedenceSetting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ internal class PathPrecedenceSetting : RegistrySetting<bool>
{
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")
{

}
Expand Down
14 changes: 7 additions & 7 deletions src/gsudo/AppSettings/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Settings
= new RegistrySetting<CacheMode>(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<TimeSpan> CacheDuration { get; }
= new RegistrySetting<TimeSpan>(nameof(CacheDuration),
Expand All @@ -33,14 +33,14 @@ class Settings
= new RegistrySetting<string>(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<string> Prompt { get; }
= new RegistrySetting<string>(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> LogLevel { get; }
Expand All @@ -54,7 +54,7 @@ class Settings
= new RegistrySetting<bool>(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<bool> ForceAttachedConsole { get; }
Expand Down Expand Up @@ -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<string> ExceptionList { get; } =
Expand All @@ -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<CloseBehaviour> NewWindow_CloseBehaviour { get; } =
new RegistrySetting<CloseBehaviour>(nameof(NewWindow_CloseBehaviour),
defaultValue: CloseBehaviour.OsDefault,
deserializer: ExtensionMethods.ParseEnum<CloseBehaviour>,
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<bool> PathOverrideSetting = new PathPrecedenceSetting();
Expand Down
5 changes: 3 additions & 2 deletions src/gsudo/Commands/ConfigCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,17 @@ public Task<int> 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)
Expand Down
1 change: 1 addition & 0 deletions src/gsudo/Commands/HelpCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ internal static void ShowHelp()
--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.
Expand Down
11 changes: 5 additions & 6 deletions src/gsudo/Commands/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public async Task<int> 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();
Expand All @@ -58,7 +58,7 @@ public async Task<int> 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,
Expand All @@ -69,7 +69,7 @@ public async Task<int> Execute()
IsInputRedirected = Console.IsInputRedirected
};

if (isElevationRequired && Settings.SecurityEnforceUacIsolation)
if (isElevationRequired && (Settings.SecurityEnforceUacIsolation || InputArguments.DisableInput))
AdjustUacIsolationRequest(elevationRequest, isShellElevation);

SetRequestPrompt(elevationRequest);
Expand Down Expand Up @@ -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;
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/gsudo/ElevationRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/// <summary>
Expand Down
22 changes: 21 additions & 1 deletion src/gsudo/Helpers/CommandLineParser.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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; }
Expand All @@ -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)
Expand Down
7 changes: 4 additions & 3 deletions src/gsudo/Helpers/CommandToRunAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
74 changes: 67 additions & 7 deletions src/gsudo/Helpers/ProcessFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
}

Expand Down Expand Up @@ -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);

Expand All @@ -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.
Expand All @@ -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
Expand Down

0 comments on commit 7e7dc33

Please sign in to comment.