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; } } }
效果验证
- 控制台标题:启动的CMD窗口标题不再显示“Administrator:”前缀,和普通用户启动的窗口一致。
- UAC触发:在该CMD中执行
powershell -C Start-Process -Verb RunAs -FilePath CMD.EXE,会正常弹出UAC确认窗口,确认后启动的CMD是完全提权的管理员进程。 - 权限状态:执行
whoami /groups会看到BUILTIN\Administrators组不存在(或仅作为拒绝组),进程处于中等完整性级别,是真正的非提权状态。
关键说明
TokenLinkedToken:管理员用户提升后,其进程的令牌会关联一个非提升的普通用户令牌,通过获取这个令牌启动进程,才能得到真正的非提权上下文。- 避免使用Safer API:Safer API主要用于应用程序限制(如软件限制策略),生成的令牌是管理员的受限版本,而非普通用户令牌,会导致UAC机制异常。
内容的提问来源于stack exchange,提问作者Gerardo Grignoli
相关产品推荐
相关产品推荐

