Programmatically elevate a .NET application on any platform
Anthony Simmon
Posted on February 6, 2024
There are times when your .NET program needs to perform an operation that requires administrative rights (elevated privileges). An example could be modifying the hosts file (C:\Windows\System32\drivers\etc\hosts
on Windows or /etc/hosts
on Linux and macOS).
Technically, it's not possible to "elevate the current process". In reality, the only thing we can do is to start a new process with administrative rights. In this article, we will explore how to do this with a console application - while obtaining the user's consent - on any platform.
Different ways to elevate a process depending on the platform
On Windows, starting a process with administrative rights involves using the User Account Control (UAC). The user is prompted to accept the elevation of privileges with a popup that should be familiar:
On Linux, it involves asking the user to enter their password to execute a command with sudo
. Of course, the user must be among the sudoers.
On macOS, it's different again. You need to ask the user to enter their password, but this time using osascript
, which will display a popup familiar to macOS users:
Detecting if the application is already elevated or not
Before elevating the current process, it's interesting to check if the application is already elevated. Here is a cross-platform method to verify this:
[DllImport("libc")]
private static extern uint geteuid();
public bool IsCurrentProcessElevated()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// https://github.com/dotnet/sdk/blob/v6.0.100/src/Cli/dotnet/Installer/Windows/WindowsUtils.cs#L38
using var identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
// https://github.com/dotnet/maintenance-packages/blob/62823150914410d43a3fd9de246d882f2a21d5ef/src/Common/tests/TestUtilities/System/PlatformDetection.Unix.cs#L58
// 0 is the ID of the root user
return geteuid() == 0;
}
I've included references to the .NET sources that inspired this method as comments.
Starting an elevated process
For the current process to start a new instance of itself with administrative rights, it needs to know where its executable is located. This can be more or less complicated depending on the platform, whether the program is executed from an executable file or a DLL (with dotnet run
or from an IDE).
In the most common scenario where the program is always compiled into an executable file thanks to the MSBuild UseAppHost property (which is true
by default), it is possible to retrieve the path of the current executable with Environment.ProcessPath
.
Then, we can start a new process with administrative rights using the Process.Start
method:
public async Task StartElevatedAsync(string[] args, CancellationToken cancellationToken)
{
var currentProcessPath = Environment.ProcessPath ?? (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? Path.ChangeExtension(typeof(Program).Assembly.Location, "exe")
: Path.ChangeExtension(typeof(Program).Assembly.Location, null));
var processStartInfo = CreateProcessStartInfo(currentProcessPath, args);
using var process = Process.Start(processStartInfo)
?? throw new InvalidOperationException("Could not start process.");
await process.WaitForExitAsync(cancellationToken);
}
private static ProcessStartInfo CreateProcessStartInfo(string processPath, string[] args)
{
var startInfo = new ProcessStartInfo
{
UseShellExecute = true,
};
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
ConfigureProcessStartInfoForWindows(startInfo, processPath, args);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
ConfigureProcessStartInfoForMacOS(startInfo, processPath, args);
}
else // Unix platforms
{
ConfigureProcessStartInfoForLinux(startInfo, processPath, args);
}
return startInfo;
}
private static void ConfigureProcessStartInfoForWindows(ProcessStartInfo startInfo, string processPath, string[] args)
{
startInfo.Verb = "runas";
startInfo.FileName = processPath;
foreach (var arg in args)
{
startInfo.ArgumentList.Add(arg);
}
}
private static void ConfigureProcessStartInfoForLinux(ProcessStartInfo startInfo, string processPath, string[] args)
{
startInfo.FileName = "sudo";
startInfo.ArgumentList.Add(processPath);
foreach (var arg in args)
{
startInfo.ArgumentList.Add(arg);
}
}
private static void ConfigureProcessStartInfoForMacOS(ProcessStartInfo startInfo, string processPath, string[] args)
{
startInfo.FileName = "osascript";
startInfo.ArgumentList.Add("-e");
startInfo.ArgumentList.Add($"do shell script \"{processPath} {string.Join(' ', args)}\" with prompt \"MyProgram\" with administrator privileges");
}
Let's analyze the code method by method. CreateProcessStartInfo
creates an instance of ProcessStartInfo
and delegates configuration to platform-specific methods.
On Windows, it is easily possible to start a process with administrative rights using the Verb
property of ProcessStartInfo
with the value runas
. There are several examples in the .NET source code, and the equivalent exists in PowerShell.
On Linux, simply make sudo
the main command to execute and provide the rest as arguments.
On macOS, osascript
is used to run a script that asks the user to enter their password to obtain administrative rights. The problem that may arise here is that the arguments, unlike the Windows and Linux methods, are not escaped. If you use arguments that contain spaces, you will need to escape them, while keeping the script valid.
Finally, we start the process and wait for it to finish.
If you need to create a communication channel between the two processes, you can use named pipes or TCP sockets.
Posted on February 6, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.