DEV Community

Cover image for Programmatically elevate a .NET application on any platform
Anthony Simmon
Anthony Simmon

Posted on • Originally published at anthonysimmon.com

Programmatically elevate a .NET application on any platform

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:

Windows UAC prompt

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.

Linux sudo prompt

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:

macOS elevated prompt

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;
}
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)