Programmatically elevate a .NET application on any platform
Tags: dotnet, csharp, programming, tutorial
Published: 2024-02-06 12:30:00

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)](https://learn.microsoft.com/en-us/windows/security/application-security/application-control/user-account-control/). The user is prompted to accept the elevation of privileges with a popup that should be familiar:

![Windows UAC prompt](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6blg9r50ll5gi3n4q0h3.png)

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](https://help.ubuntu.com/community/Sudoers).

![Linux sudo prompt](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cvzezhdusylrgrurkuht.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jz1s7cxbjigidfigu8lp.png)

## 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:

```csharp
[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](https://learn.microsoft.com/en-us/dotnet/core/project-sdk/msbuild-props#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:

```csharp
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](https://github.com/search?q=org%3Adotnet%20%22Verb%20%3D%20%5C%22runas%5C%22%22&type=code), and the equivalent exists [in PowerShell](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/start-process#example-5-start-powershell-as-an-administrator).

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](https://github.com/dotnet/runtime/blob/9a50493f9f1125fda5e2212b9d6718bc7cdbc5c0/src/libraries/System.Private.CoreLib/src/System/PasteArguments.cs#L10-L79), 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](https://learn.microsoft.com/en-us/dotnet/standard/io/how-to-use-named-pipes-for-network-interprocess-communication) or [TCP sockets](https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/sockets/socket-services).