Starting a process as normal user from a process running as Administrator

 
 
  • Gérald Barré

If you start a process from a process running as Administrator, the child process also runs as Administrator. Running a process as Administrator could be a security risk. So, a good practice is to use the minimum privileges required to run a process.

If you want to start a process as a normal user from a process running as Administrator, you need to use create a limited token and use CreateProcessWithUserW function. Windows provides different ways to create a limited token. In this post, I will use WinSafer APIs as it is the easiest way to do it. If you need more control on the access token, you can use the CreateRestrictedToken function.

Let's create a new console application:

Shell
dotnet new console

You'll need to use native win32 methods to create the new access token and start the process with this token. Instead of writing the [DllImports] yourself, you can use the Microsoft.Windows.CsWin32 source generator. I've already written about this package in this post: Generating PInvoke code for Win32 apis using a Source Generator. You can add the package using the following command:

Shell
dotnet add package Microsoft.Windows.CsWin32 --prerelease

The next step is to instruct the source generator to generate the methods you need. You need to create a file named NativeMethods.txt at the root of the project. This file contains the list of methods and constants you want to use.

NativeMethods.txt
SaferCreateLevel
SaferComputeTokenFromLevel
SaferCloseLevel
SAFER_SCOPEID_*
SAFER_LEVELID_*
SAFER_LEVEL_*
CreateProcessAsUser
TOKEN_MANDATORY_LABEL
SE_GROUP_INTEGRITY
ConvertStringSidToSid
SetTokenInformation
LocalFree

Finally, you can create the new access token and start the process using it. Here's the annotated code:

C#
using System.ComponentModel;
using System.Runtime.InteropServices;
using Windows.Win32;
using Windows.Win32.Security;

unsafe
{
    SAFER_LEVEL_HANDLE saferHandle = default;
    Windows.Win32.Foundation.PSID psid = default;
    try
    {
        // 1. Create a new new access token
        if (!PInvoke.SaferCreateLevel(PInvoke.SAFER_SCOPEID_USER, PInvoke.SAFER_LEVELID_NORMALUSER, PInvoke.SAFER_LEVEL_OPEN, out saferHandle, (void*)null))
            throw new Win32Exception(Marshal.GetLastWin32Error());

        if (!PInvoke.SaferComputeTokenFromLevel(saferHandle, null, out var newAccessToken, 0, null))
            throw new Win32Exception(Marshal.GetLastWin32Error());

        // Set the token to medium integrity because SaferCreateLevel doesn't reduce the
        // integrity level of the token and keep it as high.
        if (!PInvoke.ConvertStringSidToSid("S-1-16-8192", out psid))
            throw new Win32Exception(Marshal.GetLastWin32Error());

        TOKEN_MANDATORY_LABEL tml = default;
        tml.Label.Attributes = PInvoke.SE_GROUP_INTEGRITY;
        tml.Label.Sid = psid;

        var length = (uint)Marshal.SizeOf(tml);
        if (!PInvoke.SetTokenInformation(newAccessToken, TOKEN_INFORMATION_CLASS.TokenIntegrityLevel, &tml, length))
            throw new Win32Exception(Marshal.GetLastWin32Error());

        // 2. Start process using the new access token
        // Cannot use Process.Start as there is no way to set the access token to use
        var commandLine = "ChildApp.exe";
        Windows.Win32.System.Threading.STARTUPINFOW si = default;
        Span<char> span = stackalloc char[commandLine.Length + 2];
        commandLine.CopyTo(span);
        if (PInvoke.CreateProcessAsUser(newAccessToken, null, ref span, null, null, bInheritHandles: false, default, null, null, in si, out var pi))
        {
            PInvoke.CloseHandle(pi.hProcess);
            PInvoke.CloseHandle(pi.hThread);
        }
    }
    finally
    {
        if (saferHandle != default)
        {
            PInvoke.SaferCloseLevel(saferHandle);
        }

        if (psid != default)
        {
            PInvoke.LocalFree((nint)psid.Value);
        }
    }
}

You can use Process Explorer to view the security token associated to a process:

#Additional resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub