C# 如何从具有管理员权限的进程启动没有管理员权限的新进程?

C# 如何从具有管理员权限的进程启动没有管理员权限的新进程?,c#,windows,administrator,C#,Windows,Administrator,我正在为应用程序创建一个自动更新程序。应用程序由用户启动,运行时没有管理员权限。自动更新程序以管理员权限启动,并在下载新文件之前终止应用程序 当我想在自动更新程序完成后启动更新的应用程序时,问题就出现了。如果我使用常规的System.Diagnostics.Process.Start(文件),应用程序也会以管理员权限启动,并且必须在当前用户上运行才能按预期工作 那么,如何让autoupdater以当前用户而不是管理员的身份启动应用程序 我已尝试使用以下方法: var pSI = new Proc

我正在为应用程序创建一个自动更新程序。应用程序由用户启动,运行时没有管理员权限。自动更新程序以管理员权限启动,并在下载新文件之前终止应用程序

当我想在自动更新程序完成后启动更新的应用程序时,问题就出现了。如果我使用常规的System.Diagnostics.Process.Start(文件),应用程序也会以管理员权限启动,并且必须在当前用户上运行才能按预期工作

那么,如何让autoupdater以当前用户而不是管理员的身份启动应用程序

我已尝试使用以下方法:

var pSI = new ProcessStartInfo() { 
    UseShellExecute = false, 
    UserName = Environment.UserName, 
    FileName = file 
};
System.Diagnostics.Process.Start(pSI);
但这会抛出错误“无效用户名或密码”。我已经检查了用户名是否正确,并且我知道密码可能无效,因为我没有包括它。但这不是要求用户输入密码的选项,因为自动启动应用程序的全部原因是为了让用户更容易


有什么建议吗?

codeplex上有一个名为的项目

该项目提供了一个与UAC机制交互的库

在库中,您将发现一个名为
UserAccountControl
的类。班级 有一个名为
CreateProcessAsStandardUser
的静态方法来启动 来自具有标准用户权限的提升进程的进程

简而言之,这些函数打开桌面shell进程的进程令牌。 然后,它复制该令牌以获得主令牌。这个代币是 用于在登录用户下启动新进程


有关更多信息,请阅读以下博文。

您可以使用或类似的和。

假设您正在发出信号,要求应用程序完全关闭,而不是终止它,并且如果您仍然能够在发布更新程序之前对应用程序进行更改,一个简单的解决方案是让应用程序在退出之前启动一个临时流程。您可以在临时位置为临时进程创建可执行文件。更新完成后,向临时进程发出重新启动应用程序并退出的信号。这样,一切都会自然而然地发生,你不必胡闹

另一种选择是使用,并在杀死应用程序的安全令牌之前获取其副本。然后可以使用在原始上下文中重新启动应用程序


即使更新程序在不同的帐户下运行和/或在应用程序的不同会话中运行,这两种方法都应该有效。

您试图实现的目标不容易实现,也不受支持。然而,使用少量的黑客攻击是可能的。Aaron Margosis写了一篇描述一种技术的文章

要引用相关章节,您需要执行以下步骤:

  • 在当前令牌中启用SeIncreaseQuotaPrivilege
  • 获取表示桌面shell的HWND(GetShellWindow)
  • 获取与该窗口关联的进程的进程ID(PID)(GetWindowThreadProcessId)
  • 打开该进程(OpenProcess)
  • 从该进程获取访问令牌(OpenProcessToken)
  • 使用该令牌创建主令牌(DuplicateTokenEx)
  • 使用该主令牌(CreateProcessWithTokenW)启动新进程

  • 包含一些演示C++源的下载链接,从中可以将其简化为C语言。

    < P>我遇到了类似的问题。在PurrestStItnFo中有一个密码字段,捕获的是必须提供密码作为安全字符串。因此代码将类似:

    System.Security.SecureString password = new System.Security.SecureString();
    password.AppendChar('c1');
    //append the all characters of your password, you could probably use a loop and then,
    Process p =new Process();
    p.UseShellExecute = false;
    p.UserName = Environment.UserName;
    p.FileName = file ;
    p.Sassword=password;
    p.Start();
    

    正如作者所描述的,这个问题没有解决。 我现在也有同样的问题,我想描述一下我是如何解决这个问题的

    正如您所知,这并不是那么容易,但最好的解决方案是您强制计算机通过另一个非管理进程启动“*.exe”文件。我所做的是在任务调度器
    上创建一个没有最高权限的任务
    参数,这意味着创建时间或手动运行此任务

    这似乎很愚蠢,但似乎没有办法


    您可以看到描述如何在windows Task scheduler上创建新任务的实用VB.NET代码,以从提升的父进程启动具有默认shell权限的进程

    #Region "References"
    
    Imports System.Runtime.InteropServices
    
    #End Region
    
    Public Class LaunchProcess
    
    #Region "Native Methods"
    
        <DllImport("User32.dll", SetLastError:=True)> Private Shared Function GetShellWindow() As IntPtr
        End Function
    
        <DllImport("advapi32.dll", SetLastError:=True)> Private Shared Function OpenProcessToken(ByVal ProcessHandle As IntPtr, ByVal DesiredAccess As Integer, ByRef TokenHandle As IntPtr) As Boolean
        End Function
    
        <DllImport("user32.dll", SetLastError:=True)> Private Shared Function GetWindowThreadProcessId(ByVal hwnd As IntPtr, ByRef lpdwProcessId As IntPtr) As Integer
        End Function
    
        <DllImport("kernel32.dll")> Private Shared Function OpenProcess(ByVal dwDesiredAccess As UInteger, <MarshalAs(UnmanagedType.Bool)> ByVal bInheritHandle As Boolean, ByVal dwProcessId As IntPtr) As IntPtr
        End Function
    
        <DllImport("advapi32.dll", SetLastError:=True)> _
        Private Shared Function DuplicateTokenEx( _
        ByVal ExistingTokenHandle As IntPtr, _
        ByVal dwDesiredAccess As UInt32, _
        ByRef lpThreadAttributes As SECURITY_ATTRIBUTES, _
        ByVal ImpersonationLevel As Integer, _
        ByVal TokenType As Integer, _
        ByRef DuplicateTokenHandle As System.IntPtr) As Boolean
        End Function
    
        <DllImport("advapi32.dll", SetLastError:=True)> Private Shared Function LookupPrivilegeValue(lpSystemName As String, lpName As String, ByRef lpLuid As LUID) As Boolean
        End Function
    
        <DllImport("advapi32.dll", SetLastError:=True)> _
        Private Shared Function AdjustTokenPrivileges( _
        ByVal TokenHandle As IntPtr, _
        ByVal DisableAllPrivileges As Boolean, _
        ByRef NewState As TOKEN_PRIVILEGES, _
        ByVal BufferLengthInBytes As Integer, _
        ByRef PreviousState As TOKEN_PRIVILEGES, _
        ByRef ReturnLengthInBytes As Integer _
      ) As Boolean
        End Function
    
        <DllImport("advapi32", SetLastError:=True, CharSet:=CharSet.Unicode)> Private Shared Function CreateProcessWithTokenW(hToken As IntPtr, dwLogonFlags As Integer, lpApplicationName As String, lpCommandLine As String, dwCreationFlags As Integer, lpEnvironment As IntPtr, lpCurrentDirectory As IntPtr, ByRef lpStartupInfo As STARTUPINFO, ByRef lpProcessInformation As PROCESS_INFORMATION) As Boolean
        End Function
    
    #End Region
    
    #Region "Structures"
    
        <StructLayout(LayoutKind.Sequential)> Private Structure SECURITY_ATTRIBUTES
    
            Friend nLength As Integer
            Friend lpSecurityDescriptor As IntPtr
            Friend bInheritHandle As Integer
    
        End Structure
    
        Private Structure TOKEN_PRIVILEGES
    
            Friend PrivilegeCount As Integer
            Friend TheLuid As LUID
            Friend Attributes As Integer
    
        End Structure
    
        Private Structure LUID
    
            Friend LowPart As UInt32
            Friend HighPart As UInt32
    
        End Structure
    
        <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode)> Private Structure STARTUPINFO
    
            Friend cb As Integer
            Friend lpReserved As String
            Friend lpDesktop As String
            Friend lpTitle As String
            Friend dwX As Integer
            Friend dwY As Integer
            Friend dwXSize As Integer
            Friend dwYSize As Integer
            Friend dwXCountChars As Integer
            Friend dwYCountChars As Integer
            Friend dwFillAttribute As Integer
            Friend dwFlags As Integer
            Friend wShowWindow As Short
            Friend cbReserved2 As Short
            Friend lpReserved2 As Integer
            Friend hStdInput As Integer
            Friend hStdOutput As Integer
            Friend hStdError As Integer
    
        End Structure
    
        Private Structure PROCESS_INFORMATION
    
            Friend hProcess As IntPtr
            Friend hThread As IntPtr
            Friend dwProcessId As Integer
            Friend dwThreadId As Integer
    
        End Structure
    
    #End Region
    
    #Region "Enumerations"
    
        Private Enum TOKEN_INFORMATION_CLASS
    
            TokenUser = 1
            TokenGroups
            TokenPrivileges
            TokenOwner
            TokenPrimaryGroup
            TokenDefaultDacl
            TokenSource
            TokenType
            TokenImpersonationLevel
            TokenStatistics
            TokenRestrictedSids
            TokenSessionId
            TokenGroupsAndPrivileges
            TokenSessionReference
            TokenSandBoxInert
            TokenAuditPolicy
            TokenOrigin
            TokenElevationType
            TokenLinkedToken
            TokenElevation
            TokenHasRestrictions
            TokenAccessInformation
            TokenVirtualizationAllowed
            TokenVirtualizationEnabled
            TokenIntegrityLevel
            TokenUIAccess
            TokenMandatoryPolicy
            TokenLogonSid
            MaxTokenInfoClass
    
        End Enum
    
    #End Region
    
    #Region "Constants"
    
        Private Const SE_PRIVILEGE_ENABLED = &H2L
        Private Const PROCESS_QUERY_INFORMATION = &H400
        Private Const TOKEN_ASSIGN_PRIMARY = &H1
        Private Const TOKEN_DUPLICATE = &H2
        Private Const TOKEN_IMPERSONATE = &H4
        Private Const TOKEN_QUERY = &H8
        Private Const TOKEN_QUERY_SOURCE = &H10
        Private Const TOKEN_ADJUST_PRIVILEGES = &H20
        Private Const TOKEN_ADJUST_GROUPS = &H40
        Private Const TOKEN_ADJUST_DEFAULT = &H80
        Private Const TOKEN_ADJUST_SESSIONID = &H100
        Private Const SecurityImpersonation = 2
        Private Const TokenPrimary = 1
        Private Const SE_INCREASE_QUOTA_NAME = "SeIncreaseQuotaPrivilege"
    
    #End Region
    
    #Region "Methods"
    
        Friend Shared Sub LaunchFile(ByVal FilePath As String, ByVal IsWaitToFinish As Boolean)
    
            Try
    
                'Enable the SeIncreaseQuotaPrivilege in current token
                Dim HPrcsToken As IntPtr = Nothing
                OpenProcessToken(Process.GetCurrentProcess.Handle, TOKEN_ADJUST_PRIVILEGES, HPrcsToken)
    
                Dim TokenPrvlgs As TOKEN_PRIVILEGES
                TokenPrvlgs.PrivilegeCount = 1
                LookupPrivilegeValue(Nothing, SE_INCREASE_QUOTA_NAME, TokenPrvlgs.TheLuid)
                TokenPrvlgs.Attributes = SE_PRIVILEGE_ENABLED
    
                AdjustTokenPrivileges(HPrcsToken, False, TokenPrvlgs, 0, Nothing, Nothing)
    
                'Get window handle representing the desktop shell
                Dim HShellWnd As IntPtr = GetShellWindow()
    
                'Get the ID of the desktop shell process
                Dim ShellPID As IntPtr
                GetWindowThreadProcessId(HShellWnd, ShellPID)
    
                'Open the desktop shell process in order to get the process token
                Dim HShellPrcs As IntPtr = OpenProcess(PROCESS_QUERY_INFORMATION, False, ShellPID)
                Dim HShellPrcSToken As IntPtr = Nothing
                Dim HPrimaryToken As IntPtr = Nothing
    
                'Get the process token of the desktop shell
                OpenProcessToken(HShellPrcs, TOKEN_DUPLICATE, HShellPrcSToken)
    
                'Duplicate the shell's process token to get a primary token
                Dim TokenRights As UInteger = TOKEN_QUERY Or TOKEN_ASSIGN_PRIMARY Or TOKEN_DUPLICATE Or TOKEN_ADJUST_DEFAULT Or TOKEN_ADJUST_SESSIONID
                DuplicateTokenEx(HShellPrcSToken, TokenRights, Nothing, SecurityImpersonation, TokenPrimary, HPrimaryToken)
    
                Dim StartInfo As STARTUPINFO = Nothing
                Dim PrcsInfo As PROCESS_INFORMATION = Nothing
    
                StartInfo.cb = Marshal.SizeOf(StartInfo)
                Dim IsSuccessed As Boolean = CreateProcessWithTokenW(HPrimaryToken, 1, FilePath, "", 0, Nothing, Nothing, StartInfo, PrcsInfo)
    
                If IsSuccessed = True Then
    
                    If IsWaitToFinish = True Then
    
                        Try
    
                            Dim Prcs As Process = Process.GetProcessById(PrcsInfo.dwProcessId)
                            Prcs.WaitForExit()
    
                        Catch ex As Exception
                        End Try
    
                    End If
    
                Else
    
                    'Could not launch process with shell token may be the process needs admin rights to launch, we will try to launch it with default parent process permissions.
    
                    Dim Prcs As New Process
                    Prcs.StartInfo.FileName = FilePath
                    Prcs.Start()
    
                    If IsWaitToFinish = True Then Prcs.WaitForExit()
    
                End If
    
            Catch ex As Exception
            End Try
    
        End Sub
    
    #End Region
    
    End Class
    
    您可以使用Telerik代码转换器将Aaron Margosis的代码转换为C代码:


    这是我多年前看到的一个非常古老的问题,现在又重新讨论了。因为这是第一个谷歌搜索结果。。。我会把我的答案贴在这里

    我找到的所有解决方案都极其复杂和荒谬。这些年来,我偶然发现了一个解决方案,我在任何地方都没有看到它的文档,直到现在我还没有真正分享过

    代码非常简单。。。本质上,我们正在编写一个批处理文件,其中包含您想要运行的进程的可执行文件的名称/路径,以及您想要的任何参数。然后我们生成一个explorer.exe进程,其中包含批处理文件的路径

    File.WriteAllText(@"C:\test.bat", @"C:\test.exe -randomArgs");
    
    var proc = new Process
    {
        StartInfo = new ProcessStartInfo
        {
            FileName = "explorer.exe",
            Arguments = @"C:\test.bat",
            UseShellExecute = true,
            Verb = "runas",
            WindowStyle = ProcessWindowStyle.Hidden
        }
    };
    proc.Start();
    
    但是,我们生成的资源管理器进程会立即被操作系统杀死!正在运行的root explorer.exe进程运行批处理文件!您可以为explorer.exe指定可执行文件的名称,它也会执行相同的操作,但是该方法不支持参数


    据我所知,这是一个bug或未记录的特性。然而,我无法想象它如何被恶意使用,因为它允许权限的取消提升。。。这适用于windows 7/8/8.1/10

    在我的应用程序中,我最初使用的是的解决方案,效果非常好。但是,我希望提升的应用程序使用匿名管道与低权限的应用程序进行通信。为了实现这一点,我需要一个允许继承管道句柄的解决方案

    我想出了解决办法
            private static void RunAsDesktopUser(string fileName)
        {
            if (string.IsNullOrWhiteSpace(fileName))
                throw new ArgumentException("Value cannot be null or whitespace.", nameof(fileName));
    
            // To start process as shell user you will need to carry out these steps:
            // 1. Enable the SeIncreaseQuotaPrivilege in your current token
            // 2. Get an HWND representing the desktop shell (GetShellWindow)
            // 3. Get the Process ID(PID) of the process associated with that window(GetWindowThreadProcessId)
            // 4. Open that process(OpenProcess)
            // 5. Get the access token from that process (OpenProcessToken)
            // 6. Make a primary token with that token(DuplicateTokenEx)
            // 7. Start the new process with that primary token(CreateProcessWithTokenW)
    
            var hProcessToken = IntPtr.Zero;
            // Enable SeIncreaseQuotaPrivilege in this process.  (This won't work if current process is not elevated.)
            try
            {
                var process = GetCurrentProcess();
                if (!OpenProcessToken(process, 0x0020, ref hProcessToken))
                    return;
    
                var tkp = new TOKEN_PRIVILEGES
                {
                    PrivilegeCount = 1,
                    Privileges = new LUID_AND_ATTRIBUTES[1]
                };
    
                if (!LookupPrivilegeValue(null, "SeIncreaseQuotaPrivilege", ref tkp.Privileges[0].Luid))
                    return;
    
                tkp.Privileges[0].Attributes = 0x00000002;
    
                if (!AdjustTokenPrivileges(hProcessToken, false, ref tkp, 0, IntPtr.Zero, IntPtr.Zero))
                    return;
            }
            finally
            {
                CloseHandle(hProcessToken);
            }
    
            // Get an HWND representing the desktop shell.
            // CAVEATS:  This will fail if the shell is not running (crashed or terminated), or the default shell has been
            // replaced with a custom shell.  This also won't return what you probably want if Explorer has been terminated and
            // restarted elevated.
            var hwnd = GetShellWindow();
            if (hwnd == IntPtr.Zero)
                return;
    
            var hShellProcess = IntPtr.Zero;
            var hShellProcessToken = IntPtr.Zero;
            var hPrimaryToken = IntPtr.Zero;
            try
            {
                // Get the PID of the desktop shell process.
                uint dwPID;
                if (GetWindowThreadProcessId(hwnd, out dwPID) == 0)
                    return;
    
                // Open the desktop shell process in order to query it (get the token)
                hShellProcess = OpenProcess(ProcessAccessFlags.QueryInformation, false, dwPID);
                if (hShellProcess == IntPtr.Zero)
                    return;
    
                // Get the process token of the desktop shell.
                if (!OpenProcessToken(hShellProcess, 0x0002, ref hShellProcessToken))
                    return;
    
                var dwTokenRights = 395U;
    
                // Duplicate the shell's process token to get a primary token.
                // Based on experimentation, this is the minimal set of rights required for CreateProcessWithTokenW (contrary to current documentation).
                if (!DuplicateTokenEx(hShellProcessToken, dwTokenRights, IntPtr.Zero, SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, TOKEN_TYPE.TokenPrimary, out hPrimaryToken))
                    return;
    
                // Start the target process with the new token.
                var si = new STARTUPINFO();
                var pi = new PROCESS_INFORMATION();
                if (!CreateProcessWithTokenW(hPrimaryToken, 0, fileName, "", 0, IntPtr.Zero, Path.GetDirectoryName(fileName), ref si, out pi))
                    return;
            }
            finally
            {
                CloseHandle(hShellProcessToken);
                CloseHandle(hPrimaryToken);
                CloseHandle(hShellProcess);
            }
    
        }
    
        #region Interop
    
        private struct TOKEN_PRIVILEGES
        {
            public UInt32 PrivilegeCount;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
            public LUID_AND_ATTRIBUTES[] Privileges;
        }
    
        [StructLayout(LayoutKind.Sequential, Pack = 4)]
        private struct LUID_AND_ATTRIBUTES
        {
            public LUID Luid;
            public UInt32 Attributes;
        }
    
        [StructLayout(LayoutKind.Sequential)]
        private struct LUID
        {
            public uint LowPart;
            public int HighPart;
        }
    
        [Flags]
        private enum ProcessAccessFlags : uint
        {
            All = 0x001F0FFF,
            Terminate = 0x00000001,
            CreateThread = 0x00000002,
            VirtualMemoryOperation = 0x00000008,
            VirtualMemoryRead = 0x00000010,
            VirtualMemoryWrite = 0x00000020,
            DuplicateHandle = 0x00000040,
            CreateProcess = 0x000000080,
            SetQuota = 0x00000100,
            SetInformation = 0x00000200,
            QueryInformation = 0x00000400,
            QueryLimitedInformation = 0x00001000,
            Synchronize = 0x00100000
        }
    
        private enum SECURITY_IMPERSONATION_LEVEL
        {
            SecurityAnonymous,
            SecurityIdentification,
            SecurityImpersonation,
            SecurityDelegation
        }
    
        private enum TOKEN_TYPE
        {
            TokenPrimary = 1,
            TokenImpersonation
        }
    
        [StructLayout(LayoutKind.Sequential)]
        private struct PROCESS_INFORMATION
        {
            public IntPtr hProcess;
            public IntPtr hThread;
            public int dwProcessId;
            public int dwThreadId;
        }
    
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        private struct STARTUPINFO
        {
            public Int32 cb;
            public string lpReserved;
            public string lpDesktop;
            public string lpTitle;
            public Int32 dwX;
            public Int32 dwY;
            public Int32 dwXSize;
            public Int32 dwYSize;
            public Int32 dwXCountChars;
            public Int32 dwYCountChars;
            public Int32 dwFillAttribute;
            public Int32 dwFlags;
            public Int16 wShowWindow;
            public Int16 cbReserved2;
            public IntPtr lpReserved2;
            public IntPtr hStdInput;
            public IntPtr hStdOutput;
            public IntPtr hStdError;
        }
    
        [DllImport("kernel32.dll", ExactSpelling = true)]
        private static extern IntPtr GetCurrentProcess();
    
        [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
        private static extern bool OpenProcessToken(IntPtr h, int acc, ref IntPtr phtok);
    
        [DllImport("advapi32.dll", SetLastError = true)]
        private static extern bool LookupPrivilegeValue(string host, string name, ref LUID pluid);
    
        [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
        private static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall, ref TOKEN_PRIVILEGES newst, int len, IntPtr prev, IntPtr relen);
    
        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool CloseHandle(IntPtr hObject);
    
    
        [DllImport("user32.dll")]
        private static extern IntPtr GetShellWindow();
    
        [DllImport("user32.dll", SetLastError = true)]
        private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
    
        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern IntPtr OpenProcess(ProcessAccessFlags processAccess, bool bInheritHandle, uint processId);
    
        [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern bool DuplicateTokenEx(IntPtr hExistingToken, uint dwDesiredAccess, IntPtr lpTokenAttributes, SECURITY_IMPERSONATION_LEVEL impersonationLevel, TOKEN_TYPE tokenType, out IntPtr phNewToken);
    
        [DllImport("advapi32", SetLastError = true, CharSet = CharSet.Unicode)]
        private static extern bool CreateProcessWithTokenW(IntPtr hToken, int dwLogonFlags, string lpApplicationName, string lpCommandLine, int dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, [In] ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
    
        #endregion
    
    File.WriteAllText(@"C:\test.bat", @"C:\test.exe -randomArgs");
    
    var proc = new Process
    {
        StartInfo = new ProcessStartInfo
        {
            FileName = "explorer.exe",
            Arguments = @"C:\test.bat",
            UseShellExecute = true,
            Verb = "runas",
            WindowStyle = ProcessWindowStyle.Hidden
        }
    };
    proc.Start();
    
        public static class ProcessHelper
        {
            /// Runs a process as a non-elevated version of the current user.
            public static Process? RunAsRestrictedUser(string fileName, string? args = null)
            {
                if (string.IsNullOrWhiteSpace(fileName))
                    throw new ArgumentException("Value cannot be null or whitespace.", nameof(fileName));
    
                if (!GetRestrictedSessionUserToken(out var hRestrictedToken))
                {
                    return null;
                }
    
                try
                {
                    var si = new STARTUPINFO();
                    var pi = new PROCESS_INFORMATION();
                    var cmd = new StringBuilder();
                    cmd.Append('"').Append(fileName).Append('"');
                    if (!string.IsNullOrWhiteSpace(args))
                    {
                        cmd.Append(' ').Append(args);
                    }
    
                    if (!CreateProcessAsUser(
                        hRestrictedToken,
                        null,
                        cmd,
                        IntPtr.Zero,
                        IntPtr.Zero,
                        true, // inherit handle
                        0,
                        IntPtr.Zero,
                        Path.GetDirectoryName(fileName),
                        ref si,
                        out pi))
                    {
                        return null;
                    }
    
                    return Process.GetProcessById(pi.dwProcessId);
                }
                finally
                {
                    CloseHandle(hRestrictedToken);
                }
            }
    
            // based on https://stackoverflow.com/a/16110126/862099
            private static bool GetRestrictedSessionUserToken(out IntPtr token)
            {
                token = IntPtr.Zero;
                if (!SaferCreateLevel(SaferScope.User, SaferLevel.NormalUser, SaferOpenFlags.Open, out var hLevel, IntPtr.Zero))
                {
                    return false;
                }
    
                IntPtr hRestrictedToken = IntPtr.Zero;
                TOKEN_MANDATORY_LABEL tml = default;
                tml.Label.Sid = IntPtr.Zero;
                IntPtr tmlPtr = IntPtr.Zero;
    
                try
                {
                    if (!SaferComputeTokenFromLevel(hLevel, IntPtr.Zero, out hRestrictedToken, 0, IntPtr.Zero))
                    {
                        return false;
                    }
    
                    // Set the token to medium integrity.
                    tml.Label.Attributes = SE_GROUP_INTEGRITY;
                    tml.Label.Sid = IntPtr.Zero;
                    if (!ConvertStringSidToSid("S-1-16-8192", out tml.Label.Sid))
                    {
                        return false;
                    }
    
                    tmlPtr = Marshal.AllocHGlobal(Marshal.SizeOf(tml));
                    Marshal.StructureToPtr(tml, tmlPtr, false);
                    if (!SetTokenInformation(hRestrictedToken,
                        TOKEN_INFORMATION_CLASS.TokenIntegrityLevel,
                        tmlPtr, (uint)Marshal.SizeOf(tml)))
                    {
                        return false;
                    }
    
                    token = hRestrictedToken;
                    hRestrictedToken = IntPtr.Zero; // make sure finally() doesn't close the handle
                }
                finally
                {
                    SaferCloseLevel(hLevel);
                    SafeCloseHandle(hRestrictedToken);
                    if (tml.Label.Sid != IntPtr.Zero)
                    {
                        LocalFree(tml.Label.Sid);
                    }
                    if (tmlPtr != IntPtr.Zero)
                    {
                        Marshal.FreeHGlobal(tmlPtr);
                    }
                }
    
                return true;
            }
    
            [StructLayout(LayoutKind.Sequential)]
            private struct PROCESS_INFORMATION
            {
                public IntPtr hProcess;
                public IntPtr hThread;
                public int dwProcessId;
                public int dwThreadId;
            }
    
            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
            private struct STARTUPINFO
            {
                public Int32 cb;
                public string lpReserved;
                public string lpDesktop;
                public string lpTitle;
                public Int32 dwX;
                public Int32 dwY;
                public Int32 dwXSize;
                public Int32 dwYSize;
                public Int32 dwXCountChars;
                public Int32 dwYCountChars;
                public Int32 dwFillAttribute;
                public Int32 dwFlags;
                public Int16 wShowWindow;
                public Int16 cbReserved2;
                public IntPtr lpReserved2;
                public IntPtr hStdInput;
                public IntPtr hStdOutput;
                public IntPtr hStdError;
            }
    
            [StructLayout(LayoutKind.Sequential)]
            private struct SID_AND_ATTRIBUTES
            {
                public IntPtr Sid;
                public uint Attributes;
            }
    
            [StructLayout(LayoutKind.Sequential)]
            private struct TOKEN_MANDATORY_LABEL
            {
                public SID_AND_ATTRIBUTES Label;
            }
    
            public enum SaferLevel : uint
            {
                Disallowed = 0,
                Untrusted = 0x1000,
                Constrained = 0x10000,
                NormalUser = 0x20000,
                FullyTrusted = 0x40000
            }
    
            public enum SaferScope : uint
            {
                Machine = 1,
                User = 2
            }
    
            [Flags]
            public enum SaferOpenFlags : uint
            {
                Open = 1
            }
    
            [DllImport("advapi32", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
            private static extern bool SaferCreateLevel(SaferScope scope, SaferLevel level, SaferOpenFlags openFlags, out IntPtr pLevelHandle, IntPtr lpReserved);
    
            [DllImport("advapi32", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
            private static extern bool SaferComputeTokenFromLevel(IntPtr LevelHandle, IntPtr InAccessToken, out IntPtr OutAccessToken, int dwFlags, IntPtr lpReserved);
    
            [DllImport("advapi32", SetLastError = true)]
            private static extern bool SaferCloseLevel(IntPtr hLevelHandle);
    
            [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
            private static extern bool ConvertStringSidToSid(string StringSid, out IntPtr ptrSid);
    
            [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            [return: MarshalAs(UnmanagedType.Bool)]
            private static extern bool CloseHandle(IntPtr hObject);
    
            private static bool SafeCloseHandle(IntPtr hObject)
            {
                return (hObject == IntPtr.Zero) ? true : CloseHandle(hObject);
            }
    
            [DllImport("kernel32.dll", SetLastError = true)]
            static extern IntPtr LocalFree(IntPtr hMem);
    
            enum TOKEN_INFORMATION_CLASS
            {
                /// <summary>
                /// The buffer receives a TOKEN_USER structure that contains the user account of the token.
                /// </summary>
                TokenUser = 1,
    
                /// <summary>
                /// The buffer receives a TOKEN_GROUPS structure that contains the group accounts associated with the token.
                /// </summary>
                TokenGroups,
    
                /// <summary>
                /// The buffer receives a TOKEN_PRIVILEGES structure that contains the privileges of the token.
                /// </summary>
                TokenPrivileges,
    
                /// <summary>
                /// The buffer receives a TOKEN_OWNER structure that contains the default owner security identifier (SID) for newly created objects.
                /// </summary>
                TokenOwner,
    
                /// <summary>
                /// The buffer receives a TOKEN_PRIMARY_GROUP structure that contains the default primary group SID for newly created objects.
                /// </summary>
                TokenPrimaryGroup,
    
                /// <summary>
                /// The buffer receives a TOKEN_DEFAULT_DACL structure that contains the default DACL for newly created objects.
                /// </summary>
                TokenDefaultDacl,
    
                /// <summary>
                /// The buffer receives a TOKEN_SOURCE structure that contains the source of the token. TOKEN_QUERY_SOURCE access is needed to retrieve this information.
                /// </summary>
                TokenSource,
    
                /// <summary>
                /// The buffer receives a TOKEN_TYPE value that indicates whether the token is a primary or impersonation token.
                /// </summary>
                TokenType,
    
                /// <summary>
                /// The buffer receives a SECURITY_IMPERSONATION_LEVEL value that indicates the impersonation level of the token. If the access token is not an impersonation token, the function fails.
                /// </summary>
                TokenImpersonationLevel,
    
                /// <summary>
                /// The buffer receives a TOKEN_STATISTICS structure that contains various token statistics.
                /// </summary>
                TokenStatistics,
    
                /// <summary>
                /// The buffer receives a TOKEN_GROUPS structure that contains the list of restricting SIDs in a restricted token.
                /// </summary>
                TokenRestrictedSids,
    
                /// <summary>
                /// The buffer receives a DWORD value that indicates the Terminal Services session identifier that is associated with the token.
                /// </summary>
                TokenSessionId,
    
                /// <summary>
                /// The buffer receives a TOKEN_GROUPS_AND_PRIVILEGES structure that contains the user SID, the group accounts, the restricted SIDs, and the authentication ID associated with the token.
                /// </summary>
                TokenGroupsAndPrivileges,
    
                /// <summary>
                /// Reserved.
                /// </summary>
                TokenSessionReference,
    
                /// <summary>
                /// The buffer receives a DWORD value that is nonzero if the token includes the SANDBOX_INERT flag.
                /// </summary>
                TokenSandBoxInert,
    
                /// <summary>
                /// Reserved.
                /// </summary>
                TokenAuditPolicy,
    
                /// <summary>
                /// The buffer receives a TOKEN_ORIGIN value.
                /// </summary>
                TokenOrigin,
    
                /// <summary>
                /// The buffer receives a TOKEN_ELEVATION_TYPE value that specifies the elevation level of the token.
                /// </summary>
                TokenElevationType,
    
                /// <summary>
                /// The buffer receives a TOKEN_LINKED_TOKEN structure that contains a handle to another token that is linked to this token.
                /// </summary>
                TokenLinkedToken,
    
                /// <summary>
                /// The buffer receives a TOKEN_ELEVATION structure that specifies whether the token is elevated.
                /// </summary>
                TokenElevation,
    
                /// <summary>
                /// The buffer receives a DWORD value that is nonzero if the token has ever been filtered.
                /// </summary>
                TokenHasRestrictions,
    
                /// <summary>
                /// The buffer receives a TOKEN_ACCESS_INFORMATION structure that specifies security information contained in the token.
                /// </summary>
                TokenAccessInformation,
    
                /// <summary>
                /// The buffer receives a DWORD value that is nonzero if virtualization is allowed for the token.
                /// </summary>
                TokenVirtualizationAllowed,
    
                /// <summary>
                /// The buffer receives a DWORD value that is nonzero if virtualization is enabled for the token.
                /// </summary>
                TokenVirtualizationEnabled,
    
                /// <summary>
                /// The buffer receives a TOKEN_MANDATORY_LABEL structure that specifies the token's integrity level.
                /// </summary>
                TokenIntegrityLevel,
    
                /// <summary>
                /// The buffer receives a DWORD value that is nonzero if the token has the UIAccess flag set.
                /// </summary>
                TokenUIAccess,
    
                /// <summary>
                /// The buffer receives a TOKEN_MANDATORY_POLICY structure that specifies the token's mandatory integrity policy.
                /// </summary>
                TokenMandatoryPolicy,
    
                /// <summary>
                /// The buffer receives the token's logon security identifier (SID).
                /// </summary>
                TokenLogonSid,
    
                /// <summary>
                /// The maximum value for this enumeration
                /// </summary>
                MaxTokenInfoClass
            }
    
            [DllImport("advapi32.dll", SetLastError = true)]
            static extern Boolean SetTokenInformation(
                IntPtr TokenHandle,
                TOKEN_INFORMATION_CLASS TokenInformationClass,
                IntPtr TokenInformation,
                UInt32 TokenInformationLength);
    
            const uint SE_GROUP_INTEGRITY = 0x00000020;
    
            [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
            static extern bool CreateProcessAsUser(
                IntPtr hToken,
                string? lpApplicationName,
                StringBuilder? lpCommandLine,
                IntPtr lpProcessAttributes,
                IntPtr lpThreadAttributes,
                bool bInheritHandles,
                uint dwCreationFlags,
                IntPtr lpEnvironment,
                string? lpCurrentDirectory,
                ref STARTUPINFO lpStartupInfo,
                out PROCESS_INFORMATION lpProcessInformation);
        }