You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SaferToken创建非提权进程的异常问题及解决方案咨询

问题分析与解决方案

你遇到的三个问题本质上是因为SaferComputeTokenFromLevel生成的令牌属于管理员受限令牌——虽然移除了管理员权限,但仍保留了管理员身份的上下文标记,而非真正的普通用户令牌。系统会识别到这个令牌的管理员属性,导致窗口标题异常、UAC机制失效。

问题原因拆解

  • 控制台标题显示“Administrator:”:系统根据令牌的TokenElevationType和关联的用户SID判断是否为管理员上下文,Safer生成的令牌仍保留了管理员用户的SID,只是移除了权限组,所以窗口会显示管理员前缀。
  • RunAs无UAC弹窗且无法提权:当从管理员受限令牌启动RunAs时,系统认为当前上下文已经是管理员的一部分,会尝试复用现有令牌的权限,而不会触发UAC提升流程,最终启动的进程还是受限状态。

正确的实现方式

要生成真正的非提权中等完整性进程,应该获取当前用户的非提升链接令牌(Linked Token),而非通过Safer API生成受限令牌。管理员用户登录后,系统会维护两个令牌:提升的管理员令牌和非提升的普通用户令牌,我们需要获取后者来启动进程。

修正后的完整代码

using Microsoft.Win32.SafeHandles;
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;

namespace NonElevatedLauncher
{
    class Program
    {
        static void Main(string[] args)
        {
            string appToRun = "CMD.EXE";
            string arguments = string.Empty;
            bool newWindow = true;
            string startupFolder = Environment.CurrentDirectory;

            using (var nonElevatedToken = GetNonElevatedLinkedToken())
            {
                // 确保令牌是中等完整性(默认非提升令牌已经是中等,此步骤可选)
                SetTokenIntegrityLevel(nonElevatedToken, 8192);
                var process = StartProcessWithToken(nonElevatedToken, appToRun, arguments, startupFolder, newWindow);
                process.WaitForExit();
            }
        }

        /// <summary>
        /// 获取当前用户的非提升链接令牌
        /// </summary>
        private static SafeTokenHandle GetNonElevatedLinkedToken()
        {
            // 获取当前进程的令牌
            if (!NativeMethods.OpenProcessToken(NativeMethods.GetCurrentProcess(), 
                NativeMethods.TOKEN_QUERY | NativeMethods.TOKEN_DUPLICATE | NativeMethods.TOKEN_ASSIGN_PRIMARY, 
                out SafeTokenHandle hCurrentToken))
            {
                throw new Win32Exception();
            }

            try
            {
                // 查询令牌的提升类型,确认当前是提升状态
                if (!NativeMethods.GetTokenInformation(hCurrentToken, NativeMethods.TOKEN_INFORMATION_CLASS.TokenElevationType, 
                    out NativeMethods.TOKEN_ELEVATION_TYPE elevationType, 
                    (uint)Marshal.SizeOf<NativeMethods.TOKEN_ELEVATION_TYPE>(), out _))
                {
                    throw new Win32Exception();
                }

                if (elevationType != NativeMethods.TOKEN_ELEVATION_TYPE.TokenElevationTypeFull)
                {
                    // 当前进程未提升,直接返回当前令牌
                    return hCurrentToken.DuplicateToken(NativeMethods.TOKEN_IMPERSONATION_LEVEL.SecurityImpersonation);
                }

                // 获取链接的非提升令牌
                if (!NativeMethods.GetTokenInformation(hCurrentToken, NativeMethods.TOKEN_INFORMATION_CLASS.TokenLinkedToken, 
                    out SafeTokenHandle hLinkedToken, 
                    (uint)Marshal.SizeOf<SafeTokenHandle>(), out _))
                {
                    throw new Win32Exception();
                }

                // 复制链接令牌作为主令牌使用
                if (!NativeMethods.DuplicateTokenEx(hLinkedToken, 
                    NativeMethods.TOKEN_ALL_ACCESS, 
                    IntPtr.Zero, 
                    NativeMethods.TOKEN_IMPERSONATION_LEVEL.SecurityImpersonation, 
                    NativeMethods.TOKEN_TYPE.TokenPrimary, 
                    out SafeTokenHandle hPrimaryToken))
                {
                    throw new Win32Exception();
                }

                hLinkedToken.Dispose();
                return hPrimaryToken;
            }
            finally
            {
                hCurrentToken.Dispose();
            }
        }

        /// <summary>
        /// 设置令牌的完整性级别(可选,非提升令牌默认已是中等)
        /// </summary>
        private static void SetTokenIntegrityLevel(SafeTokenHandle token, int integrityLevel)
        {
            string integritySid = $"S-1-16-{integrityLevel}";
            if (!NativeMethods.ConvertStringSidToSid(integritySid, out IntPtr pSid))
            {
                throw new Win32Exception();
            }

            try
            {
                var mandatoryLabel = new NativeMethods.TOKEN_MANDATORY_LABEL
                {
                    Label = new NativeMethods.SID_AND_ATTRIBUTES
                    {
                        Sid = pSid,
                        Attributes = NativeMethods.SE_GROUP_INTEGRITY
                    }
                };

                IntPtr pLabel = Marshal.AllocHGlobal(Marshal.SizeOf(mandatoryLabel) + NativeMethods.GetLengthSid(pSid));
                try
                {
                    Marshal.StructureToPtr(mandatoryLabel, pLabel, false);
                    if (!NativeMethods.SetTokenInformation(token, NativeMethods.TOKEN_INFORMATION_CLASS.TokenIntegrityLevel, 
                        pLabel, (uint)(Marshal.SizeOf(mandatoryLabel) + NativeMethods.GetLengthSid(pSid))))
                    {
                        throw new Win32Exception();
                    }
                }
                finally
                {
                    Marshal.FreeHGlobal(pLabel);
                }
            }
            finally
            {
                NativeMethods.LocalFree(pSid);
            }
        }

        /// <summary>
        /// 使用指定令牌启动进程
        /// </summary>
        private static System.Diagnostics.Process StartProcessWithToken(SafeTokenHandle token, string appPath, string arguments, string workingDir, bool newWindow)
        {
            var si = new NativeMethods.STARTUPINFO();
            si.cb = Marshal.SizeOf(si);
            if (newWindow)
            {
                si.dwFlags = NativeMethods.STARTF_USESHOWWINDOW;
                si.wShowWindow = NativeMethods.SW_SHOWNORMAL;
            }

            var pi = new NativeMethods.PROCESS_INFORMATION();
            uint creationFlags = newWindow ? NativeMethods.CREATE_NEW_CONSOLE : 0;

            string commandLine = string.IsNullOrEmpty(arguments) ? appPath : $"{appPath} {arguments}";
            if (!NativeMethods.CreateProcessAsUser(token, null, commandLine, IntPtr.Zero, IntPtr.Zero, 
                false, creationFlags, IntPtr.Zero, workingDir, ref si, out pi))
            {
                throw new Win32Exception();
            }

            try
            {
                NativeMethods.CloseHandle(pi.hThread);
                return System.Diagnostics.Process.GetProcessById(pi.dwProcessId);
            }
            finally
            {
                NativeMethods.CloseHandle(pi.hProcess);
            }
        }
    }

    internal class SafeTokenHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
        public SafeTokenHandle(IntPtr handle) : base(true) => SetHandle(handle);
        private SafeTokenHandle() : base(true) { }

        protected override bool ReleaseHandle() => NativeMethods.CloseHandle(handle);

        public SafeTokenHandle DuplicateToken(NativeMethods.TOKEN_IMPERSONATION_LEVEL impersonationLevel)
        {
            if (!NativeMethods.DuplicateToken(this, (int)impersonationLevel, out SafeTokenHandle duplicatedToken))
            {
                throw new Win32Exception();
            }
            return duplicatedToken;
        }
    }

    static class NativeMethods
    {
        public const uint TOKEN_QUERY = 0x0008;
        public const uint TOKEN_DUPLICATE = 0x0002;
        public const uint TOKEN_ASSIGN_PRIMARY = 0x0001;
        public const uint TOKEN_ALL_ACCESS = 0xF01FF;

        public const int STARTF_USESHOWWINDOW = 0x00000001;
        public const int SW_SHOWNORMAL = 1;
        public const uint CREATE_NEW_CONSOLE = 0x00000010;
        public const uint SE_GROUP_INTEGRITY = 0x00000020;

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr GetCurrentProcess();

        [DllImport("advapi32.dll", SetLastError = true)]
        public static extern bool OpenProcessToken(IntPtr processHandle, uint desiredAccess, out SafeTokenHandle tokenHandle);

        [DllImport("advapi32.dll", SetLastError = true)]
        public static extern bool GetTokenInformation(SafeTokenHandle tokenHandle, TOKEN_INFORMATION_CLASS informationClass, 
            out TOKEN_ELEVATION_TYPE elevationType, uint informationLength, out uint returnLength);

        [DllImport("advapi32.dll", SetLastError = true)]
        public static extern bool GetTokenInformation(SafeTokenHandle tokenHandle, TOKEN_INFORMATION_CLASS informationClass, 
            out SafeTokenHandle linkedToken, uint informationLength, out uint returnLength);

        [DllImport("advapi32.dll", SetLastError = true)]
        public static extern bool DuplicateTokenEx(SafeTokenHandle existingToken, uint desiredAccess, 
            IntPtr tokenAttributes, TOKEN_IMPERSONATION_LEVEL impersonationLevel, 
            TOKEN_TYPE tokenType, out SafeTokenHandle newToken);

        [DllImport("advapi32.dll", SetLastError = true)]
        public static extern bool DuplicateToken(SafeTokenHandle existingToken, int impersonationLevel, out SafeTokenHandle newToken);

        [DllImport("advapi32.dll", SetLastError = true)]
        public static extern bool CreateProcessAsUser(SafeTokenHandle tokenHandle, string applicationName, string commandLine, 
            IntPtr processAttributes, IntPtr threadAttributes, bool inheritHandles, uint creationFlags, 
            IntPtr environment, string currentDirectory, ref STARTUPINFO startupInfo, out PROCESS_INFORMATION processInfo);

        [DllImport("advapi32.dll", SetLastError = true)]
        public static extern bool ConvertStringSidToSid(string stringSid, out IntPtr sid);

        [DllImport("advapi32.dll")]
        public static extern int GetLengthSid(IntPtr sid);

        [DllImport("advapi32.dll", SetLastError = true)]
        public static extern bool SetTokenInformation(SafeTokenHandle tokenHandle, TOKEN_INFORMATION_CLASS informationClass, 
            IntPtr information, uint informationLength);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool CloseHandle(IntPtr handle);

        [DllImport("kernel32.dll")]
        public static extern IntPtr LocalFree(IntPtr ptr);

        public enum TOKEN_INFORMATION_CLASS
        {
            TokenElevationType = 18,
            TokenLinkedToken = 19,
            TokenIntegrityLevel = 25
        }

        public enum TOKEN_ELEVATION_TYPE
        {
            TokenElevationTypeDefault = 1,
            TokenElevationTypeFull = 2,
            TokenElevationTypeLimited = 3
        }

        public enum TOKEN_IMPERSONATION_LEVEL
        {
            SecurityAnonymous = 0,
            SecurityIdentification = 1,
            SecurityImpersonation = 2,
            SecurityDelegation = 3
        }

        public enum TOKEN_TYPE
        {
            TokenPrimary = 1,
            TokenImpersonation = 2
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct STARTUPINFO
        {
            public int cb;
            public string lpReserved;
            public string lpDesktop;
            public string lpTitle;
            public int dwX;
            public int dwY;
            public int dwXSize;
            public int dwYSize;
            public int dwXCountChars;
            public int dwYCountChars;
            public int dwFillAttribute;
            public int dwFlags;
            public short wShowWindow;
            public short cbReserved2;
            public IntPtr lpReserved2;
            public IntPtr hStdInput;
            public IntPtr hStdOutput;
            public IntPtr hStdError;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct PROCESS_INFORMATION
        {
            public IntPtr hProcess;
            public IntPtr hThread;
            public int dwProcessId;
            public int dwThreadId;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct TOKEN_MANDATORY_LABEL
        {
            public SID_AND_ATTRIBUTES Label;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct SID_AND_ATTRIBUTES
        {
            public IntPtr Sid;
            public uint Attributes;
        }
    }
}

效果验证

  1. 控制台标题:启动的CMD窗口标题不再显示“Administrator:”前缀,和普通用户启动的窗口一致。
  2. UAC触发:在该CMD中执行powershell -C Start-Process -Verb RunAs -FilePath CMD.EXE,会正常弹出UAC确认窗口,确认后启动的CMD是完全提权的管理员进程。
  3. 权限状态:执行whoami /groups会看到BUILTIN\Administrators组不存在(或仅作为拒绝组),进程处于中等完整性级别,是真正的非提权状态。

关键说明

  • TokenLinkedToken:管理员用户提升后,其进程的令牌会关联一个非提升的普通用户令牌,通过获取这个令牌启动进程,才能得到真正的非提权上下文。
  • 避免使用Safer API:Safer API主要用于应用程序限制(如软件限制策略),生成的令牌是管理员的受限版本,而非普通用户令牌,会导致UAC机制异常。

内容的提问来源于stack exchange,提问作者Gerardo Grignoli

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 18:29:06