Starting a process as normal user from a process running as Administrator
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:
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:
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.
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:
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!