diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index c25eb1f9..8b5104f0 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -28,6 +28,6 @@ Before creating one, please search https://github.com/gerardog/gsudo/issues?q=
## Context:
- Windows version:
- gsudo version:
diff --git a/README.md b/README.md
index 65705da1..9c85012d 100644
--- a/README.md
+++ b/README.md
@@ -282,7 +282,7 @@ Here are the `Credentials Cache` Modes:
- This is the default mode. (Use `gsudo config CacheMode Explicit` to revert to this value).
- `Auto:` Similar to unix-sudo. The first elevation shows a UAC popup and **starts a cache session automatically**.
- Run `gsudo config CacheMode Auto` to use this mode.
-- `Disabled:` Every elevation shows a UAC popup, and tryoing to start a cache session will throw an error..
+- `Disabled:` Every elevation shows a UAC popup, and trying to start a cache session will throw an error..
- Run `gsudo config CacheMode Disabled` to use this mode.
In any case, you can stop all cache sessions with `gsudo -k`.
diff --git a/docs/docs/gsudo-vs-sudo.md b/docs/docs/gsudo-vs-sudo.md
new file mode 100644
index 00000000..c45aa4d9
--- /dev/null
+++ b/docs/docs/gsudo-vs-sudo.md
@@ -0,0 +1,70 @@
+---
+sidebar_position: 7
+title: Comparison with Microsoft sudo
+#hide_title: true
+---
+
+# Comparison between gsudo and Microsoft sudo
+
+`gsudo` was born in 2019 as a productivity tool and is open-source. It has been downloaded over 700k times so far and has enjoyed a warm reception from users.
+
+Microsoft initially declined to create a similar tool, citing security concerns. However `gsudo`'s documentation [challenged this view](security.md) arguing that absolute security is unattainable without altering Windows itself, and that the default same-desktop UAC is not completely secure either.
+
+In February 2024, Microsoft reversed its stance and released [Sudo for Windows](https://devblogs.microsoft.com/commandline/introducing-sudo-for-windows/).
+
+Surprisingly, Microsoft's sudo does not leverage new OS features to enhance security. Its mechanisms are akin to `gsudo`, making their security models comparable. The question of which one is more secure depends on which version of each are you comparing, and how many open bugs it has. The initial release of Microsoft's sudo presented some [critical issues](https://www.tiraniddo.dev/2024/02/sudo-on-windows-quick-rundown.html) that they will hopefully address very soon. `gsudo` has fixed similar issues in the past and may in the future.
+
+## Sudo Tools Feature Comparison
+
+### Is it a proper Sudo tool?
+| Feature | `gsudo` | Sudo for Windows |
+| ------- | ------- | ------------------ |
+| Executes command with elevated permissions | Yes | Yes |
+| Supports output redirection (`sudo dir > file.txt`) | Yes | Yes |
+| Supports input redirection (`echo md folder \| sudo cmd`) | Yes | Partial (Only with output redirection) |
+| Returns the command exit code | Yes | No |
+| Source code available | [Yes](https://github.com/gerardog/gsudo) | Not for `sudo.exe`, but [promised](https://github.com/microsoft/sudo/blob/f8f1d05/README.md#contributing) |
+
+### Security Impersonation Features
+
+| Feature | `gsudo` | Sudo for Windows |
+| ------- | ------- | ------------------ |
+| Run with custom Integrity | Yes (`-i`, `--integrity`) | No |
+| Run as System | Yes (`-s`, `--system`) | No |
+| Run as TrustedInstaller | Yes (`--ti`)| No |
+| Run as user | Yes (`-u user`) | No |
+
+### User Experience
+
+| Feature | `gsudo` | Sudo for Windows |
+| ------- | ------- | ------------------ |
+| Easy to install and update | Yes (winget, choco, scoop) | No (Windows Insider build required) |
+| See less UAC Pop-ups | Yes ([Credentials Cache](credentials-cache.md)) | No |
+| Elevate current shell | Yes | No |
+| Elevate commands using current shell | Yes | No |
+| Red # indicator for elevation on CMD | Yes | No |
+
+### Additional Features
+
+| Feature | `gsudo` | Sudo for Windows |
+| ------- | ------- | ------------------ |
+| Run in new window | Yes | Yes |
+| Option to keep new window open until a key is pressed | [Yes](tips/elevation-in-new-window.md) | No |
+| Option to keep new window's shell open | [Yes](tips/elevation-in-new-window.md) | No |
+| Run with Input Disabled | [Yes](https://gerardog.github.io/gsudo/docs/security#what-are-the-risks-of-running-gsudo) | Yes |
+| Elevate last command with `sudo !!` | Yes | No |
+
+### PowerShell
+
+| Feature | `gsudo` | Sudo for Windows |
+| ------- | ------- | ------------------ |
+| Elevation syntax | `gsudo { Script } -args $a,$b` [syntax](usage/powershell.md#using-gsudo-scriptblock-syntax) | Unknown, possibly: `sudo pwsh { script }` |
+| Auto-complete of last 3 commands | Yes (with [gsudoModule](usage/powershell.md#gsudo-powershell-module)) | No |
+| Auto-complete of options | Yes (with [gsudoModule](usage/powershell.md#gsudo-powershell-module)) | No |
+| Red # indicator for elevation | Yes (with [gsudoModule](usage/powershell.md#gsudo-powershell-module)) | No |
+
+## What if I install both?
+
+If you have both Microsoft Sudo and `gsudo` installed, they both should work independently.
+
+The `sudo` keyword will run Microsoft's sudo instead of `gsudo` because the typical install of `Sudo for Windows` (which is via a Windows Insider build) puts it in `c:\Windows\System32\sudo.exe`. This folder appears first in the `PATH` environment variable, therefore when running `sudo`, the Microsoft `sudo.exe` will take precedence over gsudo's `sudo` alias.
diff --git a/docs/docs/security.md b/docs/docs/security.md
index de72aa32..aee1c141 100644
--- a/docs/docs/security.md
+++ b/docs/docs/security.md
@@ -4,17 +4,17 @@ title: Security Considerations
hide_title: true
---
-## Why Windows doesn't have a `sudo` command? Should it have one?
+## Why doesn't Windows have a `sudo` command? Should it have one?
To answer this question, we first have to take a look back at the history.
In August 2002, Chris Paget released a white paper describing a form of attack against event-driven systems that he termed Shatter Attack. It allowed processes in the same session to bypass security restrictions by abusing Windows Message loop.
-Microsoft response was to add "User Interface Privilege Isolation" (UIPI) and the "User Access Control" (UAC) popup to the next major release: Windows Vista. Privileged processes would then run "elevated" at high integrity level, out of reach of processes at lower, non-admin, levels.
+Microsoft's response was to add "User Interface Privilege Isolation" (UIPI) and the "User Access Control" (UAC) popup to the next major release: Windows Vista. Privileged processes would then run "elevated" at high integrity level, out of reach of processes at lower, non-admin, levels.
-I assume that at this point Microsoft decided not to make a `sudo` tool for windows. It would be an unwanted bridge between two worlds that intended to isolate.
+I assume that at this point, Microsoft decided not to make a `sudo` tool for windows. It would be an unwanted bridge between two worlds that intended to isolate.
-But that insulation quickly proved weak. Let me [quote Raymond Chen from Microsoft](https://devblogs.microsoft.com/oldnewthing/20160816-00/?p=94105):
+However, that insulation quickly proved to be weak. Let me [quote Raymond Chen from Microsoft](https://devblogs.microsoft.com/oldnewthing/20160816-00/?p=94105):
> There’s a setting that lets you specify how often you want to be prompted by UAC. You can set any of four levels:
>
@@ -28,9 +28,9 @@ But that insulation quickly proved weak. Let me [quote Raymond Chen from Microso
> - Always notify
> - Meh
>
-> The reason why all the other options collapse into Meh is that the `Notify only when apps try to change settings` option can be subverted by any app simply by injecting a thread into (...)
+> The reason why all the other options collapse into Meh is that the `Notify only when apps try to change settings` option can be subverted by any app simply by injecting a thread (... a hacking technique explained ...)
-And, up to this day, Windows 10 & 11, defaults to "Meh". Microsoft default is: user convenience at the expense of lesser security.
+And, up to this day, Windows 10 & 11, defaults corresponds to that "Meh". Microsoft's default stance is user convenience at the expense of lesser security.
---
![Vista UAC](../static/img/Vista-UAC.png)
@@ -38,22 +38,26 @@ And, up to this day, Windows 10 & 11, defaults to "Meh". Microsoft default is: u
---
-Over time, no less than a hundred UAC-bypass techniques were disclosed. Many of them still work in an updated Windows 11. Windows Defender, or your antivirus of choice, will stop them (hopefully). But if you disable your AV, UAC bypass is easy.
+Over time, more than a hundred UAC-bypass techniques have been disclosed. Many of them still work in an updated Windows 11. It is Windows Defender, or your antivirus of choice, who may stop them (hopefully). But if you disable your AV, UAC bypass is easy.
-Microsoft did not patch them. Instead, they assumed publicly that `UAC/UIPI` in default mode is not a security boundary. I will [quote Microsoft documentation](https://docs.microsoft.com/en-us/troubleshoot/windows-server/windows-security/disable-user-account-control#:~:text=More%20important%2C%20Same%2Ddesktop%20Elevation,be%20considered%20a%20convenience%20feature.) :
+And Microsoft can't just patch them all without loosing 'convenience' features. They instead, they assumed publicly that `UAC/UIPI` in default mode is not a security boundary. I will [quote Microsoft documentation](https://docs.microsoft.com/en-us/troubleshoot/windows-server/windows-security/disable-user-account-control#:~:text=More%20important%2C%20Same%2Ddesktop%20Elevation,be%20considered%20a%20convenience%20feature.) :
> Same-desktop Elevation in UAC isn't a security boundary. It can be hijacked by unprivileged software that runs on the same desktop. Same-desktop Elevation should be considered a convenience feature.
-Which means: UAC does not protect you from threats. It's a convenience tool that protects you from shooting yourself in the foot.
+Which to me, it means: UAC is a convenience tool. It does not protect you from threats, it works as a warning that protects you from shooting yourself in the foot.
-And, in my opinion, it's not doing the best possible job:
+And if UAC is a convenience tool, in my opinion it's not doing the best possible job:
- You waste important time by switching between elevated and unelevated windows. You must manually carry your command and context to the elevated window, each time, back and forth.
- ... or you suffer from "elevation fatigue". For example, you elevate a whole console beforehand, and you do all your stuff there, likely running non-admin or untrusted stuff as admin.
-**In conclusion:**
+**In conclusion:**
-- Same-desktop UAC is a vulnerable convenience feature, and so is `gsudo`. In any case, **the only thing that protects you from malware is you and your Antivirus.**.
+- Same-desktop UAC is a vulnerable convenience feature.
+- `gsudo` is also a vulnerable convenience feature.
+- Whether you use `gsudo` or not, **the only thing that protects you from malware is not UAC, it is you and your Antivirus.**.
+
+So, if you decide to use `gsudo`, here are the risks:
## What are the risks of running gsudo?
@@ -61,7 +65,7 @@ gsudo could be used as an attack vector for escalation of privileges. Using anti
- **Abusing an elevation made with gsudo:**
- A medium integrity process could drive the gsudo-elevated process.
+ A medium integrity process could drive the gsudo-elevated process.
When gsudo elevates **in the same console**, it creates a connection between a medium and a high integrity process. A malicious process (at medium integrity) can then drive the medium integrity console: sending keystrokes to the high integrity app, or scrapping its screen.
diff --git a/docs/docs/troubleshooting.md b/docs/docs/troubleshooting.md
index c9c4b249..bd6f0456 100644
--- a/docs/docs/troubleshooting.md
+++ b/docs/docs/troubleshooting.md
@@ -1,5 +1,5 @@
---
-sidebar_position: 7
+sidebar_position: 10
hide_title: true
title: Troubleshooting
---
diff --git a/docs/docs/usage/powershell.md b/docs/docs/usage/powershell.md
index d0974bae..b2ee06bc 100644
--- a/docs/docs/usage/powershell.md
+++ b/docs/docs/usage/powershell.md
@@ -22,7 +22,8 @@ When the current shell is `PowerShell`, gsudo can be used in the following ways:
3. [`gsudo 'string command'`](#using-gsudo-command-syntax) => Old, legacy syntax.
- You can [add `gsudo` PowerShell Module](#powershell-profile-config) to your `$PROFILE`
- - This enables to use `gsudo !!` to elevate last command.
+ - This allows tab-key auto-completition. For example, write 'gsudo` then `space` and then `tab` and your last executed command will be suggested.
+ - Also enables to use `gsudo !!` to elevate last command.
- In a pipeline of commands, `gsudo` only elevates one command.
diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js
index 899e1620..4298e4cc 100644
--- a/docs/docusaurus.config.js
+++ b/docs/docusaurus.config.js
@@ -6,7 +6,7 @@ const darkCodeTheme = require('prism-react-renderer/themes/dracula');
/** @type {import('@docusaurus/types').Config} */
const config = {
- title: 'gsudo (sudo for windows)',
+ title: 'gsudo Documentation',
tagline: 'The missing piece in Windows. Cherry-pick which commands to elevate with just one keyword.',
url: 'https://gerardog.github.io',
baseUrl: '/gsudo/',
diff --git a/docs/src/pages/index.js b/docs/src/pages/index.js
index 0da93f48..3834e72a 100644
--- a/docs/src/pages/index.js
+++ b/docs/src/pages/index.js
@@ -12,7 +12,7 @@ function HomepageHeader() {
return (
-
{siteConfig.title}
+
gsudo (sudo for Windows)
{siteConfig.tagline}
ExceptionList { get; } =
new RegistrySetting(nameof(ExceptionList),
- defaultValue: "notepad.exe;powershell.exe;whoami.exe;",
+ defaultValue: "notepad.exe;powershell.exe;whoami.exe;vim.exe;nano.exe;",
deserializer: (string s)=>s,
scope: RegistrySettingScope.GlobalOnly);
@@ -127,8 +127,14 @@ internal static TimeSpan TimeSpanParseWithInfinite(string value)
{
if (value.In("-1", "Infinite"))
return TimeSpan.MaxValue;
- else
- return TimeSpan.Parse(value, CultureInfo.InvariantCulture);
+
+ var timeSpan = TimeSpan.Parse(value, CultureInfo.InvariantCulture);
+
+ // Cap at 24 days.
+ if (timeSpan.TotalDays > 24)
+ return TimeSpan.MaxValue;
+
+ return timeSpan;
}
internal static string TimeSpanWithInfiniteToString(TimeSpan value)
diff --git a/src/gsudo/Commands/RunCommand.cs b/src/gsudo/Commands/RunCommand.cs
index 58dc5667..a58af99e 100644
--- a/src/gsudo/Commands/RunCommand.cs
+++ b/src/gsudo/Commands/RunCommand.cs
@@ -231,15 +231,18 @@ private void AdjustUacIsolationRequest(ElevationRequest elevationRequest, bool i
}
}
- internal static bool IsRunningAsDesiredUser()
+ internal static bool IsRunningAsDesiredUser(bool allowHigherIntegrity = false)
{
if (InputArguments.TrustedInstaller && !WindowsIdentity.GetCurrent().Claims.Any(c => c.Value == Constants.TI_SID))
return false;
if (InputArguments.RunAsSystem && !WindowsIdentity.GetCurrent().IsSystem)
- return false;
+ return false;
- if ((int)InputArguments.GetIntegrityLevel() != SecurityHelper.GetCurrentIntegrityLevel())
+ if ((int)InputArguments.GetIntegrityLevel() != SecurityHelper.GetCurrentIntegrityLevel() && !allowHigherIntegrity)
+ return false;
+
+ if ((int)InputArguments.GetIntegrityLevel() > SecurityHelper.GetCurrentIntegrityLevel())
return false;
if (InputArguments.UserName != null && InputArguments.UserName != WindowsIdentity.GetCurrent().Name)
diff --git a/src/gsudo/Commands/ServiceCommand.cs b/src/gsudo/Commands/ServiceCommand.cs
index ad7bdf63..15e362ce 100644
--- a/src/gsudo/Commands/ServiceCommand.cs
+++ b/src/gsudo/Commands/ServiceCommand.cs
@@ -28,6 +28,8 @@ class ServiceCommand : ICommand, IDisposable
void EnableTimer()
{
+ if (CacheDuration > TimeSpan.FromDays(24)) CacheDuration = TimeSpan.FromDays(24);
+
if (CacheDuration != TimeSpan.MaxValue)
ShutdownTimer.Change((int)CacheDuration.TotalMilliseconds, Timeout.Infinite);
}
@@ -55,7 +57,7 @@ public async Task Execute()
|| (InputArguments.RunAsSystem && !System.Security.Principal.WindowsIdentity.GetCurrent().IsSystem)
|| (InputArguments.UserName != null && !SecurityHelper.IsAdministrator() && SecurityHelper.IsMemberOfLocalAdmins())
)*/
- if (!RunCommand.IsRunningAsDesiredUser())
+ if (!RunCommand.IsRunningAsDesiredUser(allowHigherIntegrity: true))
{
Logger.Instance.Log("This service is not running with desired credentials. Starting a new service instance.", LogLevel.Info);
#if DEBUG
diff --git a/src/gsudo/Commands/StatusCommand.cs b/src/gsudo/Commands/StatusCommand.cs
index 651104ab..f80f7ae8 100644
--- a/src/gsudo/Commands/StatusCommand.cs
+++ b/src/gsudo/Commands/StatusCommand.cs
@@ -152,7 +152,7 @@ private static void PrintToConsole(Dictionary result)
foreach (string s in result["CacheSessions"] as string[])
{
- Console.WriteLine($" {s},");
+ Console.WriteLine($" {s}");
}
if ((bool)result["IsRedirected"])
diff --git a/src/gsudo/Helpers/ProcessFactory.cs b/src/gsudo/Helpers/ProcessFactory.cs
index 1f41068a..ebb10e47 100644
--- a/src/gsudo/Helpers/ProcessFactory.cs
+++ b/src/gsudo/Helpers/ProcessFactory.cs
@@ -67,7 +67,7 @@ public static Process StartRedirected(string fileName, string arguments, string
public static Process StartAttached(string filename, string arguments)
{
- Logger.Instance.Log($"Process Start: {filename} {arguments}", LogLevel.Debug );
+ Logger.Instance.Log($"Process Start: {filename} {arguments}", LogLevel.Debug);
var process = new Process();
process.StartInfo = new ProcessStartInfo(filename)
{
@@ -128,7 +128,7 @@ public static Process StartWithCredentials(string filename, string arguments, st
CreateNoWindow = !InputArguments.Debug,
});
}
- catch(Win32Exception ex)
+ catch (Win32Exception ex)
{
if (ex.NativeErrorCode == 1326)
throw new ApplicationException("The user name or password is incorrect.");
@@ -332,7 +332,7 @@ private static SafeProcessHandle CreateProcessWithToken(IntPtr newToken, string
return new SafeProcessHandle(processInformation.hProcess, true);
}
- internal static SafeProcessHandle CreateProcessAsUserWithFlags(string lpApplicationName, string args, ProcessApi.CreateProcessFlags dwCreationFlags, out PROCESS_INFORMATION pInfo)
+ internal static void CreateProcessForTokenReplacement(string lpApplicationName, string args, ProcessApi.CreateProcessFlags dwCreationFlags, out SafeProcessHandle processHandle, out SafeHandle threadHandle, out int processId)
{
var sInfoEx = new ProcessApi.STARTUPINFOEX();
sInfoEx.StartupInfo.cb = Marshal.SizeOf(sInfoEx);
@@ -342,7 +342,9 @@ internal static SafeProcessHandle CreateProcessAsUserWithFlags(string lpApplicat
pSec.nLength = Marshal.SizeOf(pSec);
tSec.nLength = Marshal.SizeOf(tSec);
- // Set more restrictive Security Descriptor
+ // 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.
string sddl = "D:(D;;GAFAWD;;;S-1-1-0)"; // Deny Generic-All, File-All, and Write-Dac to everyone.
IntPtr sd_ptr = new IntPtr();
@@ -354,15 +356,43 @@ internal static SafeProcessHandle CreateProcessAsUserWithFlags(string lpApplicat
var command = $"{lpApplicationName} {args}";
- Logger.Instance.Log($"{nameof(CreateProcessAsUserWithFlags)}: {lpApplicationName} {args}", LogLevel.Debug);
+ 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))
{
throw new Win32Exception((int)ConsoleApi.GetLastError());
}
- return new SafeProcessHandle(pInfo.hProcess, true);
- }
+ var currentProcessHandle = ProcessApi.GetCurrentProcess();
+
+ if (!DuplicateHandle(
+ currentProcessHandle, // Source process handle is the current process
+ pInfo.hProcess, // The handle to duplicate
+ currentProcessHandle, // Target process handle is also the current process
+ out var restrictedProcessHandle, // The duplicated handle with desired access rights
+ 0x1000 | 0x00100000 | 0x0001, // Desired access: PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE | PROCESS_TERMINATE
+ false, // The handle is not inheritable
+ 1)) // dwOptions: auto close pInfo.hProcess.
+ {
+ throw new Win32Exception(Marshal.GetLastWin32Error());
+ }
+ if (!DuplicateHandle(
+ currentProcessHandle, // Source process handle is the current process
+ pInfo.hThread, // The thread handle to duplicate
+ currentProcessHandle, // Target process handle is also the current process
+ out var restrictedThreadHandle, // The duplicated handle with desired access rights
+ 0x0002, // Desired access: THREAD_SUSPEND_RESUME
+ false, // The handle is not inheritable
+ 1)) // dwOptions: auto close pInfo.hThread.
+ {
+ throw new Win32Exception(Marshal.GetLastWin32Error());
+ }
+
+ processHandle = new SafeProcessHandle(restrictedProcessHandle, true);
+ threadHandle = new Native.SafeThreadHandle(restrictedThreadHandle);
+
+ processId = pInfo.dwProcessId;
+ }
}
}
-
diff --git a/src/gsudo/Helpers/ServiceHelper.cs b/src/gsudo/Helpers/ServiceHelper.cs
index c615c0be..bff39a0d 100644
--- a/src/gsudo/Helpers/ServiceHelper.cs
+++ b/src/gsudo/Helpers/ServiceHelper.cs
@@ -69,8 +69,8 @@ public static async Task FindAnyServiceFast()
private static ServiceLocation FindServiceByIntegrity(int? clientPid, string user)
{
var anyIntegrity = InputArguments.UserName != null;
- var tryHighIntegrity = !InputArguments.IntegrityLevel.HasValue || InputArguments.IntegrityLevel.Value >= IntegrityLevel.High;
- var tryLowIntegrity = !InputArguments.IntegrityLevel.HasValue || InputArguments.IntegrityLevel.Value < IntegrityLevel.High;
+ var tryHighIntegrity = !InputArguments.IntegrityLevel.HasValue || InputArguments.IntegrityLevel.Value > IntegrityLevel.Medium;
+ var tryLowIntegrity = !InputArguments.IntegrityLevel.HasValue || InputArguments.IntegrityLevel.Value <= IntegrityLevel.Medium;
var targetUserSid = InputArguments.RunAsSystem ? "S-1-5-18" : InputArguments.UserSid;
diff --git a/src/gsudo/Tokens/NativeMethods.cs b/src/gsudo/Native/NativeMethods.cs
similarity index 64%
rename from src/gsudo/Tokens/NativeMethods.cs
rename to src/gsudo/Native/NativeMethods.cs
index e71ae796..2f0c4018 100644
--- a/src/gsudo/Tokens/NativeMethods.cs
+++ b/src/gsudo/Native/NativeMethods.cs
@@ -1,11 +1,10 @@
-using gsudo.Native;
-using System;
+using System;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using System.Security.Principal;
using static gsudo.Native.TokensApi;
-namespace gsudo.Tokens
+namespace gsudo.Native
{
internal static partial class NativeMethods
{
@@ -21,19 +20,19 @@ internal static partial class NativeMethods
internal const int SE_PRIVILEGE_DISABLED = 0x00000000;
internal const int ERROR_NOT_ALL_ASSIGNED = 0x00000514;
- internal const UInt32 STANDARD_RIGHTS_REQUIRED = 0x000F0000;
- internal const UInt32 STANDARD_RIGHTS_READ = 0x00020000;
- internal const UInt32 TOKEN_ASSIGN_PRIMARY = 0x0001;
- internal const UInt32 TOKEN_DUPLICATE = 0x0002;
- internal const UInt32 TOKEN_IMPERSONATE = 0x0004;
- internal const UInt32 TOKEN_QUERY = 0x0008;
- internal const UInt32 TOKEN_QUERY_SOURCE = 0x0010;
- internal const UInt32 TOKEN_ADJUST_PRIVILEGES = 0x0020;
- internal const UInt32 TOKEN_ADJUST_GROUPS = 0x0040;
- internal const UInt32 TOKEN_ADJUST_DEFAULT = 0x0080;
- internal const UInt32 TOKEN_ADJUST_SESSIONID = 0x0100;
- internal const UInt32 TOKEN_READ = (STANDARD_RIGHTS_READ | TOKEN_QUERY);
- internal const UInt32 TOKEN_ALL_ACCESS = (STANDARD_RIGHTS_REQUIRED |
+ internal const uint STANDARD_RIGHTS_REQUIRED = 0x000F0000;
+ internal const uint STANDARD_RIGHTS_READ = 0x00020000;
+ internal const uint TOKEN_ASSIGN_PRIMARY = 0x0001;
+ internal const uint TOKEN_DUPLICATE = 0x0002;
+ internal const uint TOKEN_IMPERSONATE = 0x0004;
+ internal const uint TOKEN_QUERY = 0x0008;
+ internal const uint TOKEN_QUERY_SOURCE = 0x0010;
+ internal const uint TOKEN_ADJUST_PRIVILEGES = 0x0020;
+ internal const uint TOKEN_ADJUST_GROUPS = 0x0040;
+ internal const uint TOKEN_ADJUST_DEFAULT = 0x0080;
+ internal const uint TOKEN_ADJUST_SESSIONID = 0x0100;
+ internal const uint TOKEN_READ = STANDARD_RIGHTS_READ | TOKEN_QUERY;
+ internal const uint TOKEN_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED |
TOKEN_ASSIGN_PRIMARY |
TOKEN_DUPLICATE |
TOKEN_IMPERSONATE |
@@ -42,7 +41,7 @@ internal static partial class NativeMethods
TOKEN_ADJUST_PRIVILEGES |
TOKEN_ADJUST_GROUPS |
TOKEN_ADJUST_DEFAULT |
- TOKEN_ADJUST_SESSIONID);
+ TOKEN_ADJUST_SESSIONID;
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern IntPtr GetCurrentProcess();
@@ -65,7 +64,13 @@ internal static extern bool OpenProcessToken(IntPtr processHandle,
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
- internal static extern Boolean CloseHandle(IntPtr hObject);
+ internal static extern bool CloseHandle(IntPtr hObject);
+
+ [DllImport("advapi32.dll", SetLastError = true)]
+ internal static extern bool GetKernelObjectSecurity(IntPtr Handle, uint securityInformation, IntPtr pSecurityDescriptor, uint nLength, out uint lpnLengthNeeded);
+
+ [DllImport("advapi32.dll", SetLastError = true)]
+ internal static extern bool SetKernelObjectSecurity(IntPtr Handle, uint securityInformation, IntPtr pSecurityDescriptor);
[StructLayout(LayoutKind.Sequential)]
public struct LUID
@@ -100,5 +105,20 @@ public struct TOKEN_PRIVILEGES
public LUID_AND_ATTRIBUTES[] Privileges { get => privileges; set => privileges = value; }
}
+
}
+
+ [Flags]
+ internal enum SECURITY_INFORMATION : uint
+ {
+ OWNER_SECURITY_INFORMATION = 0x00000001,
+ GROUP_SECURITY_INFORMATION = 0x00000002,
+ DACL_SECURITY_INFORMATION = 0x00000004,
+ SACL_SECURITY_INFORMATION = 0x00000008,
+ UNPROTECTED_SACL_SECURITY_INFORMATION = 0x10000000,
+ UNPROTECTED_DACL_SECURITY_INFORMATION = 0x20000000,
+ PROTECTED_SACL_SECURITY_INFORMATION = 0x40000000,
+ PROTECTED_DACL_SECURITY_INFORMATION = 0x80000000
+ }
+
}
diff --git a/src/gsudo/Native/ProcessApi.cs b/src/gsudo/Native/ProcessApi.cs
index 77bb7406..c024c818 100644
--- a/src/gsudo/Native/ProcessApi.cs
+++ b/src/gsudo/Native/ProcessApi.cs
@@ -120,7 +120,10 @@ public enum CreateProcessFlags : uint
internal static extern bool DeleteProcThreadAttributeList(IntPtr lpAttributeList);
[DllImport("kernel32.dll", SetLastError = true)]
- internal static extern bool CloseHandle(IntPtr hObject);
+ internal static extern bool CloseHandle(IntPtr hObject);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ public static extern IntPtr LocalFree(IntPtr hMem);
[DllImport("kernel32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto, SetLastError = true)]
internal static extern bool GetExitCodeProcess(Microsoft.Win32.SafeHandles.SafeProcessHandle processHandle, out int exitCode);
@@ -190,7 +193,9 @@ protected override bool ReleaseHandle()
#region Query Process Info
public const UInt32 PROCESS_QUERY_INFORMATION = 0x0400;
public const UInt32 PROCESS_SET_INFORMATION = 0x0200;
-
+ public const UInt32 READ_CONTROL = 0x00020000;
+ public const UInt32 WRITE_DAC = 0x40000;
+
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern IntPtr OpenProcess(UInt32 dwDesiredAccess, Boolean bInheritHandle, UInt32 dwProcessId);
@@ -225,7 +230,7 @@ internal static extern bool CheckRemoteDebuggerPresent(
internal static extern bool CreatePipe(out SafeFileHandle hReadPipe, out SafeFileHandle hWritePipe, SECURITY_ATTRIBUTES lpPipeAttributes, int nSize);
[DllImport("kernel32.dll", SetLastError = true)]
- internal static extern uint ResumeThread(IntPtr hThread);
+ internal static extern int ResumeThread(IntPtr hThread);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
@@ -233,5 +238,16 @@ internal static extern bool CheckRemoteDebuggerPresent(
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool DuplicateHandle(
+ IntPtr hSourceProcessHandle,
+ IntPtr hSourceHandle,
+ IntPtr hTargetProcessHandle,
+ out IntPtr lpTargetHandle,
+ uint dwDesiredAccess,
+ [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle,
+ uint dwOptions);
}
}
diff --git a/src/gsudo/Native/SafeTokenHandle.cs b/src/gsudo/Native/SafeTokenHandle.cs
index ce5fdea3..0328b5c1 100644
--- a/src/gsudo/Native/SafeTokenHandle.cs
+++ b/src/gsudo/Native/SafeTokenHandle.cs
@@ -21,4 +21,19 @@ protected override bool ReleaseHandle()
return Native.ProcessApi.CloseHandle(base.handle);
}
}
+
+ internal sealed class SafeThreadHandle : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ internal SafeThreadHandle(IntPtr handle)
+ : base(true)
+ {
+ base.SetHandle(handle);
+ }
+
+ override protected bool ReleaseHandle()
+ {
+ return Native.ProcessApi.CloseHandle(handle);
+ }
+
+ }
}
diff --git a/src/gsudo/Native/TokensApi.cs b/src/gsudo/Native/TokensApi.cs
index 7b86315d..5f6bad6d 100644
--- a/src/gsudo/Native/TokensApi.cs
+++ b/src/gsudo/Native/TokensApi.cs
@@ -414,7 +414,19 @@ public struct LUID
#endregion
[DllImport("Advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
- internal static extern bool ConvertStringSecurityDescriptorToSecurityDescriptor(string StringSecurityDescriptor, uint StringSDRevision, out IntPtr SecurityDescriptor, out UIntPtr SecurityDescriptorSize);
-
+ internal static extern bool ConvertStringSecurityDescriptorToSecurityDescriptor(
+ string StringSecurityDescriptor,
+ uint StringSDRevision,
+ out IntPtr SecurityDescriptor,
+ out UIntPtr SecurityDescriptorSize);
+
+ // Additional imports for DACL manipulation
+ [DllImport("advapi32.dll", SetLastError = true)]
+ internal static extern bool ConvertSecurityDescriptorToStringSecurityDescriptor(
+ IntPtr SecurityDescriptor,
+ uint StringSDRevision,
+ SECURITY_INFORMATION SecurityInformation,
+ out IntPtr StringSecurityDescriptor,
+ out uint StringSecurityDescriptorLen);
}
}
diff --git a/src/gsudo/ProcessRenderers/TokenSwitchRenderer.cs b/src/gsudo/ProcessRenderers/TokenSwitchRenderer.cs
index faad4a5c..8a6214dd 100644
--- a/src/gsudo/ProcessRenderers/TokenSwitchRenderer.cs
+++ b/src/gsudo/ProcessRenderers/TokenSwitchRenderer.cs
@@ -4,8 +4,10 @@
using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.IO;
using System.Linq;
+using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@@ -19,8 +21,8 @@ class TokenSwitchRenderer : IProcessRenderer
{
private readonly Connection _connection;
private readonly ElevationRequest _elevationRequest;
- private readonly SafeProcessHandle _process;
- private readonly ProcessApi.PROCESS_INFORMATION _processInformation;
+ private readonly SafeProcessHandle _processHandle;
+ private readonly SafeHandle _threadHandle;
private readonly ManualResetEventSlim tokenSwitchSuccessEvent = new ManualResetEventSlim(false);
internal TokenSwitchRenderer(Connection connection, ElevationRequest elevationRequest)
@@ -55,9 +57,9 @@ internal TokenSwitchRenderer(Connection connection, ElevationRequest elevationRe
args = elevationRequest.Arguments;
}
- _process = ProcessFactory.CreateProcessAsUserWithFlags(exeName, args, dwCreationFlags, out _processInformation);
+ ProcessFactory.CreateProcessForTokenReplacement(exeName, args, dwCreationFlags, out _processHandle, out _threadHandle, out int processId);
- elevationRequest.TargetProcessId = _processInformation.dwProcessId;
+ elevationRequest.TargetProcessId = processId;
if (!elevationRequest.NewWindow)
ConsoleApi.SetConsoleCtrlHandler(ConsoleHelper.IgnoreConsoleCancelKeyPress, true);
}
@@ -68,7 +70,7 @@ public Task Start()
{
var t1 = new StreamReader(_connection.ControlStream).ConsumeOutput(HandleControlStream);
- WaitHandle.WaitAny(new WaitHandle[] { tokenSwitchSuccessEvent.WaitHandle, _process.GetProcessWaitHandle(), _connection.DisconnectedWaitHandle });
+ WaitHandle.WaitAny(new WaitHandle[] { tokenSwitchSuccessEvent.WaitHandle, _processHandle.GetProcessWaitHandle(), _connection.DisconnectedWaitHandle });
if (!tokenSwitchSuccessEvent.IsSet)
{
@@ -87,43 +89,34 @@ public Task Start()
_connection.DataStream.Close();
_connection.ControlStream.Close();
- return GetResult();
- }
- finally
- {
- ConsoleApi.SetConsoleCtrlHandler(ConsoleHelper.IgnoreConsoleCancelKeyPress, false);
- }
- }
-
- public void TerminateProcess()
- {
- ProcessApi.TerminateProcess(_process.DangerousGetHandle(), 0);
- }
+ if (ProcessApi.ResumeThread(_threadHandle.DangerousGetHandle()) < 0)
+ throw new Win32Exception();
- public Task GetResult()
- {
- try
- {
- _ = ProcessApi.ResumeThread(_processInformation.hThread);
- Native.FileApi.CloseHandle(_processInformation.hThread);
+ _threadHandle.Close();
if (_elevationRequest.Wait)
{
- _process.GetProcessWaitHandle().WaitOne();
- if (ProcessApi.GetExitCodeProcess(_process, out int exitCode))
+ _processHandle.GetProcessWaitHandle().WaitOne();
+ if (ProcessApi.GetExitCodeProcess(_processHandle, out int exitCode))
return Task.FromResult(exitCode);
- Native.FileApi.CloseHandle(_processInformation.hProcess);
+ _processHandle.Close();
}
return Task.FromResult(0);
}
finally
{
+ _processHandle?.Close();
+ _threadHandle?.Close();
ConsoleApi.SetConsoleCtrlHandler(ConsoleHelper.IgnoreConsoleCancelKeyPress, false);
}
}
+ public void TerminateProcess()
+ {
+ ProcessApi.TerminateProcess(_processHandle.DangerousGetHandle(), 0);
+ }
enum Mode { Normal, Error};
Mode CurrentMode = Mode.Normal;
diff --git a/src/gsudo/Rpc/NamedPipeNameFactory.cs b/src/gsudo/Rpc/NamedPipeNameFactory.cs
index d0484279..24417ea0 100644
--- a/src/gsudo/Rpc/NamedPipeNameFactory.cs
+++ b/src/gsudo/Rpc/NamedPipeNameFactory.cs
@@ -16,7 +16,8 @@ public static string GetPipeName(string allowedSid, int allowedPid, string targe
var s = InputArguments.RunAsSystem ? "_S" : string.Empty;
var admin = !isAdmin ? "_NonAdmin" : string.Empty;
- var data = $"allowedSid-{allowedSid}_targetSid-{targetSid}{allowedPid}{s}{ti}{admin}";
+ var ownExe = GetHash(ProcessHelper.GetOwnExeName());
+ var data = $"allowedSid-{allowedSid}_targetSid-{targetSid}{allowedPid}{s}{ti}{admin}_{ownExe}";
#if !DEBUG
data = GetHash(data);
#endif
diff --git a/src/gsudo/Tokens/PrivilegeManager.cs b/src/gsudo/Tokens/PrivilegeManager.cs
index a6334ec9..d29d38cf 100644
--- a/src/gsudo/Tokens/PrivilegeManager.cs
+++ b/src/gsudo/Tokens/PrivilegeManager.cs
@@ -3,7 +3,8 @@
using System.Runtime.InteropServices;
using System.Security.Principal;
using static gsudo.Native.TokensApi;
-using static gsudo.Tokens.NativeMethods;
+using static gsudo.Native.NativeMethods;
+using gsudo.Native;
namespace gsudo.Tokens
{
diff --git a/src/gsudo/Tokens/TokenSwitcher.cs b/src/gsudo/Tokens/TokenSwitcher.cs
index 2510bc6d..8555b8cf 100644
--- a/src/gsudo/Tokens/TokenSwitcher.cs
+++ b/src/gsudo/Tokens/TokenSwitcher.cs
@@ -3,6 +3,8 @@
using System.Runtime.InteropServices;
using gsudo.Helpers;
using gsudo.Native;
+using static gsudo.Native.TokensApi;
+using static gsudo.Native.NativeMethods;
namespace gsudo.Tokens
{
@@ -27,28 +29,51 @@ public static void ReplaceProcessToken(ElevationRequest elevationRequest)
.EnablePrivilege(Privilege.SeTcbPrivilege, false)
.EnablePrivilege(Privilege.SeIncreaseQuotaPrivilege, false)
.Impersonate(() =>
- {
- IntPtr hProcess = ProcessApi.OpenProcess(ProcessApi.PROCESS_SET_INFORMATION, true,
- (uint)elevationRequest.TargetProcessId);
- NtDllApi.PROCESS_INFORMATION_CLASS processInformationClass =
- NtDllApi.PROCESS_INFORMATION_CLASS.ProcessAccessToken;
-
- int res = NtDllApi.NativeMethods.NtSetInformationProcess(hProcess, processInformationClass,
- ref tokenInfo,
- Marshal.SizeOf());
- Logger.Instance.Log($"NtSetInformationProcess returned {res}", LogLevel.Debug);
- if (res < 0)
- throw new Win32Exception();
-
- ProcessApi.CloseHandle(hProcess);
+ {
+ IntPtr securityDescriptor = IntPtr.Zero;
+ UIntPtr securityDescriptorSize = UIntPtr.Zero;
+ IntPtr hProcess = IntPtr.Zero;
+ try
+ {
+ hProcess = ProcessApi.OpenProcess(ProcessApi.PROCESS_SET_INFORMATION | ProcessApi.PROCESS_QUERY_INFORMATION | ProcessApi.READ_CONTROL | ProcessApi.WRITE_DAC, true,
+ (uint)elevationRequest.TargetProcessId);
+
+ // Tighten the security descriptor of the process to elevate, before elevating its process token
+ string sddl = "D:(D;;GAFAWD;;;S-1-1-0)S:(ML;;NW;;;HI)"; // Deny all to everyone. SACL requires High Integrity.
+ Native.TokensApi.ConvertStringSecurityDescriptorToSecurityDescriptor(sddl, StringSDRevision: 1, out securityDescriptor, out securityDescriptorSize);
+
+ // https://learn.microsoft.com/en-us/windows/win32/secauthz/low-level-security-descriptor-functions
+ if (!SetKernelObjectSecurity(hProcess, (uint)SECURITY_INFORMATION.DACL_SECURITY_INFORMATION, securityDescriptor))
+ throw new InvalidOperationException("Failed to tighten security descriptor.", new Win32Exception());
+
+ // Replace the Process access token with an elevated one.
+ var processInformationClass = NtDllApi.PROCESS_INFORMATION_CLASS.ProcessAccessToken;
+
+ int res = NtDllApi.NativeMethods.NtSetInformationProcess(hProcess, processInformationClass,
+ ref tokenInfo, Marshal.SizeOf());
+
+ if (res < 0)
+ throw new Win32Exception();
+
+ Logger.Instance.Log($"Process token replaced", LogLevel.Debug);
+ }
+ finally
+ {
+ if (hProcess != IntPtr.Zero)
+ ProcessApi.CloseHandle(hProcess);
+
+ // Cleanup: Free the security descriptor pointer if it's not null
+ if (securityDescriptor != IntPtr.Zero)
+ ProcessApi.LocalFree(securityDescriptor);
+ }
});
}
finally
{
desiredToken?.Close();
}
- }
-
+ }
+
private static SafeTokenHandle GetDesiredToken(ElevationRequest elevationRequest)
{
TokenProvider tm = null;
diff --git a/src/gsudo/gsudo.csproj b/src/gsudo/gsudo.csproj
index feb8993b..a8c08df3 100644
--- a/src/gsudo/gsudo.csproj
+++ b/src/gsudo/gsudo.csproj
@@ -41,6 +41,12 @@
+
+
+
+
+
+ all
diff --git a/yarn.lock b/yarn.lock
index 54ed0584..d9f9ca1a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1555,7 +1555,7 @@
"@docusaurus/utils-validation" "2.4.1"
tslib "^2.4.0"
-"@docusaurus/plugin-sitemap@2.4.1", "@docusaurus/plugin-sitemap@^2.4.1":
+"@docusaurus/plugin-sitemap@2.4.1":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.4.1.tgz#8a7a76ed69dc3e6b4474b6abb10bb03336a9de6d"
integrity sha512-lZx+ijt/+atQ3FVE8FOHV/+X3kuok688OydDXrqKRJyXBJZKgGjA2Qa8RjQ4f27V2woaXhtnyrdPop/+OjVMRg==
@@ -1570,6 +1570,21 @@
sitemap "^7.1.1"
tslib "^2.4.0"
+"@docusaurus/plugin-sitemap@^2.4.3":
+ version "2.4.3"
+ resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.4.3.tgz#1b3930900a8f89670ce7e8f83fb4730cd3298c32"
+ integrity sha512-LRQYrK1oH1rNfr4YvWBmRzTL0LN9UAPxBbghgeFRBm5yloF6P+zv1tm2pe2hQTX/QP5bSKdnajCvfnScgKXMZQ==
+ dependencies:
+ "@docusaurus/core" "2.4.3"
+ "@docusaurus/logger" "2.4.3"
+ "@docusaurus/types" "2.4.3"
+ "@docusaurus/utils" "2.4.3"
+ "@docusaurus/utils-common" "2.4.3"
+ "@docusaurus/utils-validation" "2.4.3"
+ fs-extra "^10.1.0"
+ sitemap "^7.1.1"
+ tslib "^2.4.0"
+
"@docusaurus/preset-classic@^2.4.1":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-2.4.1.tgz#072f22d0332588e9c5f512d4bded8d7c99f91497"
@@ -4063,9 +4078,9 @@ flux@^4.0.1:
fbjs "^3.0.1"
follow-redirects@^1.0.0, follow-redirects@^1.14.7:
- version "1.15.2"
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
- integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
+ version "1.15.4"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
+ integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
fork-ts-checker-webpack-plugin@^6.5.0:
version "6.5.2"