improve:UIA置顶

This commit is contained in:
2026-04-30 22:49:05 +08:00
parent 73f27a9423
commit 329e9bd933
2 changed files with 577 additions and 12 deletions
+557
View File
@@ -0,0 +1,557 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 通过 Winlogon 令牌模拟实现 UIAccess 提权重启。
/// 1. 找到当前会话中 winlogon.exe 的令牌,复制为模拟令牌;
/// 2. SetThreadToken 暂时模拟 winlogon(拥有 TCB 权限);
/// 3. 在自身令牌副本上 SetTokenInformation(TokenUIAccess, TRUE)
/// 4. RevertToSelf 后用 CreateProcessWithTokenW 启动新进程;
/// 5. 新进程具有 UIAccess 权限,可置顶于 UAC 提示之上。
/// </summary>
public static class UIAccessHelper
{
#region Constants
private const uint TOKEN_QUERY = 0x0008;
private const uint TOKEN_DUPLICATE = 0x0002;
private const uint TOKEN_IMPERSONATE = 0x0004;
private const uint TOKEN_ASSIGN_PRIMARY = 0x0001;
private const uint TOKEN_ADJUST_DEFAULT = 0x0080;
private const uint TOKEN_ADJUST_SESSIONID = 0x0100;
private const uint TOKEN_ADJUST_PRIVILEGES = 0x0020;
private const int SecurityAnonymous = 0;
private const int SecurityImpersonation = 2;
private const int TokenPrimary = 1;
private const int TokenImpersonation = 2;
// TOKEN_INFORMATION_CLASS
private const int TokenSessionId = 12;
private const int TokenUIAccess = 26;
private const uint PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
private const uint TH32CS_SNAPPROCESS = 0x00000002;
private const uint LOGON_WITH_PROFILE = 0x00000001;
private const uint CREATE_NEW_CONSOLE = 0x00000010;
private const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400;
private const uint SE_PRIVILEGE_ENABLED = 0x00000002;
private const string SE_ASSIGNPRIMARYTOKEN_NAME = "SeAssignPrimaryTokenPrivilege";
private static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
#endregion
#region Structs
[StructLayout(LayoutKind.Sequential)]
private struct LUID
{
public uint LowPart;
public int HighPart;
}
[StructLayout(LayoutKind.Sequential)]
private struct LUID_AND_ATTRIBUTES
{
public LUID Luid;
public uint Attributes;
}
[StructLayout(LayoutKind.Sequential)]
private struct TOKEN_PRIVILEGES
{
public uint PrivilegeCount;
public LUID_AND_ATTRIBUTES Privilege;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct PROCESSENTRY32W
{
public uint dwSize;
public uint cntUsage;
public uint th32ProcessID;
public IntPtr th32DefaultHeapID;
public uint th32ModuleID;
public uint cntThreads;
public uint th32ParentProcessID;
public int pcPriClassBase;
public uint dwFlags;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string szExeFile;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct STARTUPINFOW
{
public uint cb;
public IntPtr lpReserved;
public IntPtr lpDesktop;
public IntPtr lpTitle;
public uint dwX, dwY, dwXSize, dwYSize;
public uint dwXCountChars, dwYCountChars;
public uint dwFillAttribute;
public uint dwFlags;
public ushort wShowWindow;
public ushort cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput, hStdOutput, hStdError;
}
[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public uint dwProcessId;
public uint dwThreadId;
}
#endregion
#region P/Invoke
[DllImport("kernel32.dll")]
private static extern IntPtr GetCurrentProcess();
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(IntPtr hObject);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DuplicateTokenEx(
IntPtr hExistingToken,
uint dwDesiredAccess,
IntPtr lpTokenAttributes,
int ImpersonationLevel,
int TokenType,
out IntPtr phNewToken);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetTokenInformation(
IntPtr TokenHandle,
int TokenInformationClass,
IntPtr TokenInformation,
uint TokenInformationLength,
out uint ReturnLength);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetTokenInformation(
IntPtr TokenHandle,
int TokenInformationClass,
IntPtr TokenInformation,
uint TokenInformationLength);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetThreadToken(IntPtr Thread, IntPtr Token);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool RevertToSelf();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool Process32FirstW(IntPtr hSnapshot, ref PROCESSENTRY32W lppe);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool Process32NextW(IntPtr hSnapshot, ref PROCESSENTRY32W lppe);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool LookupPrivilegeValueW(string lpSystemName, string lpName, out LUID lpLuid);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool AdjustTokenPrivileges(
IntPtr TokenHandle,
[MarshalAs(UnmanagedType.Bool)] bool DisableAllPrivileges,
ref TOKEN_PRIVILEGES NewState,
uint BufferLength,
IntPtr PreviousState,
IntPtr ReturnLength);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CreateProcessWithTokenW(
IntPtr hToken,
uint dwLogonFlags,
string lpApplicationName,
StringBuilder lpCommandLine,
uint dwCreationFlags,
IntPtr lpEnvironment,
string lpCurrentDirectory,
ref STARTUPINFOW lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern void GetStartupInfoW(ref STARTUPINFOW lpStartupInfo);
#endregion
#region Public API
/// <summary>
/// 检查当前进程是否已具有 UIAccess 标志。
/// </summary>
public static bool HasUIAccess()
{
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, out IntPtr hToken))
return false;
try
{
IntPtr buf = Marshal.AllocHGlobal(sizeof(uint));
try
{
Marshal.WriteInt32(buf, 0);
if (!GetTokenInformation(hToken, TokenUIAccess, buf, sizeof(uint), out _))
return false;
return Marshal.ReadInt32(buf) != 0;
}
finally
{
Marshal.FreeHGlobal(buf);
}
}
finally
{
CloseHandle(hToken);
}
}
/// <summary>
/// 以 UIAccess 令牌重启自身。当前进程必须已经以管理员身份运行。
/// 成功时新进程已启动,调用方应立即退出当前进程。
/// </summary>
/// <param name="extraArgs">追加到新进程的额外命令行参数(例如 --skip-mutex-check)。</param>
public static bool RestartWithUIAccess(string extraArgs = null)
{
try
{
if (HasUIAccess())
{
LogHelper.WriteLogToFile("UIAccess | 当前进程已具有 UIAccess,跳过重启");
return true;
}
if (!CreateUIAccessToken(out IntPtr uiaToken))
{
LogHelper.WriteLogToFile($"UIAccess | 创建 UIAccess 令牌失败 (LastError={Marshal.GetLastWin32Error()})", LogHelper.LogType.Error);
return false;
}
try
{
return LaunchWithToken(uiaToken, extraArgs);
}
finally
{
CloseHandle(uiaToken);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"UIAccess | RestartWithUIAccess 异常: {ex}", LogHelper.LogType.Error);
return false;
}
}
#endregion
#region Token Manipulation
private static bool CreateUIAccessToken(out IntPtr uiaToken)
{
uiaToken = IntPtr.Zero;
// 1. 获取当前进程的 session id
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, out IntPtr hSelfQuery))
{
LogHelper.WriteLogToFile($"UIAccess | OpenProcessToken(query) 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
return false;
}
uint sessionId;
try
{
IntPtr sesBuf = Marshal.AllocHGlobal(sizeof(uint));
try
{
if (!GetTokenInformation(hSelfQuery, TokenSessionId, sesBuf, sizeof(uint), out _))
{
LogHelper.WriteLogToFile($"UIAccess | GetTokenInformation(SessionId) 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
return false;
}
sessionId = (uint)Marshal.ReadInt32(sesBuf);
}
finally { Marshal.FreeHGlobal(sesBuf); }
}
finally { CloseHandle(hSelfQuery); }
// 2. 找到同一会话的 winlogon 模拟令牌
if (!GetWinlogonImpersonationToken(sessionId, out IntPtr winlogonToken))
{
LogHelper.WriteLogToFile("UIAccess | 未能获取 winlogon 模拟令牌(需要管理员权限)", LogHelper.LogType.Error);
return false;
}
try
{
// 3. 模拟 winlogon
if (!SetThreadToken(IntPtr.Zero, winlogonToken))
{
LogHelper.WriteLogToFile($"UIAccess | SetThreadToken(winlogon) 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
return false;
}
try
{
// 4. 复制自身令牌为主令牌
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE, out IntPtr hSelfDup))
{
LogHelper.WriteLogToFile($"UIAccess | OpenProcessToken(dup) 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
return false;
}
IntPtr dupToken;
try
{
bool ok = DuplicateTokenEx(
hSelfDup,
TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID,
IntPtr.Zero,
SecurityAnonymous,
TokenPrimary,
out dupToken);
if (!ok)
{
LogHelper.WriteLogToFile($"UIAccess | DuplicateTokenEx 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
return false;
}
}
finally { CloseHandle(hSelfDup); }
// 5. 在副本上设置 UIAccess = TRUE
IntPtr uiBuf = Marshal.AllocHGlobal(sizeof(uint));
try
{
Marshal.WriteInt32(uiBuf, 1);
if (!SetTokenInformation(dupToken, TokenUIAccess, uiBuf, sizeof(uint)))
{
int err = Marshal.GetLastWin32Error();
LogHelper.WriteLogToFile($"UIAccess | SetTokenInformation(UIAccess) 失败: {err}", LogHelper.LogType.Error);
CloseHandle(dupToken);
return false;
}
}
finally { Marshal.FreeHGlobal(uiBuf); }
uiaToken = dupToken;
return true;
}
finally
{
RevertToSelf();
}
}
finally
{
CloseHandle(winlogonToken);
}
}
private static bool GetWinlogonImpersonationToken(uint sessionId, out IntPtr winlogonToken)
{
winlogonToken = IntPtr.Zero;
IntPtr snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE || snapshot == IntPtr.Zero)
{
LogHelper.WriteLogToFile($"UIAccess | CreateToolhelp32Snapshot 失败: {Marshal.GetLastWin32Error()}", LogHelper.LogType.Error);
return false;
}
try
{
var pe = new PROCESSENTRY32W { dwSize = (uint)Marshal.SizeOf(typeof(PROCESSENTRY32W)) };
bool more = Process32FirstW(snapshot, ref pe);
while (more)
{
if (string.Equals(pe.szExeFile, "winlogon.exe", StringComparison.OrdinalIgnoreCase))
{
if (TryDuplicateWinlogonToken(pe.th32ProcessID, sessionId, out winlogonToken))
return true;
}
more = Process32NextW(snapshot, ref pe);
}
}
finally { CloseHandle(snapshot); }
return false;
}
private static bool TryDuplicateWinlogonToken(uint pid, uint sessionId, out IntPtr dupToken)
{
dupToken = IntPtr.Zero;
IntPtr hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
if (hProc == IntPtr.Zero) return false;
try
{
if (!OpenProcessToken(hProc, TOKEN_QUERY | TOKEN_DUPLICATE, out IntPtr hToken))
return false;
try
{
// 检查 session id 匹配
IntPtr sesBuf = Marshal.AllocHGlobal(sizeof(uint));
try
{
if (!GetTokenInformation(hToken, TokenSessionId, sesBuf, sizeof(uint), out _))
return false;
if ((uint)Marshal.ReadInt32(sesBuf) != sessionId)
return false;
}
finally { Marshal.FreeHGlobal(sesBuf); }
if (!DuplicateTokenEx(
hToken,
TOKEN_IMPERSONATE | TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY | TOKEN_DUPLICATE,
IntPtr.Zero,
SecurityImpersonation,
TokenImpersonation,
out dupToken))
{
return false;
}
// 启用 SeAssignPrimaryTokenPrivilegeInkeys 行为)
var tkp = new TOKEN_PRIVILEGES
{
PrivilegeCount = 1,
Privilege = new LUID_AND_ATTRIBUTES { Attributes = SE_PRIVILEGE_ENABLED }
};
if (LookupPrivilegeValueW(null, SE_ASSIGNPRIMARYTOKEN_NAME, out tkp.Privilege.Luid))
{
AdjustTokenPrivileges(dupToken, false, ref tkp, (uint)Marshal.SizeOf(tkp), IntPtr.Zero, IntPtr.Zero);
}
return true;
}
finally { CloseHandle(hToken); }
}
finally { CloseHandle(hProc); }
}
#endregion
#region Process Launch
private static bool LaunchWithToken(IntPtr token, string extraArgs)
{
string exePath = Process.GetCurrentProcess().MainModule.FileName;
string workDir = System.IO.Path.GetDirectoryName(exePath);
// 重建命令行:保留原始参数,追加 --skip-mutex-check 防止单实例阻塞
var cmdBuilder = new StringBuilder(32768);
cmdBuilder.Append('"').Append(exePath).Append('"');
string[] args = Environment.GetCommandLineArgs();
for (int i = 1; i < args.Length; i++)
{
cmdBuilder.Append(' ');
AppendQuoted(cmdBuilder, args[i]);
}
if (!string.IsNullOrEmpty(extraArgs))
cmdBuilder.Append(' ').Append(extraArgs);
// 防止单实例 Mutex 阻塞新进程
if (Array.IndexOf(args, "--skip-mutex-check") < 0
&& (extraArgs == null || extraArgs.IndexOf("--skip-mutex-check", StringComparison.Ordinal) < 0))
{
cmdBuilder.Append(" --skip-mutex-check");
}
var si = new STARTUPINFOW { cb = (uint)Marshal.SizeOf(typeof(STARTUPINFOW)) };
GetStartupInfoW(ref si);
bool ok = CreateProcessWithTokenW(
token,
LOGON_WITH_PROFILE,
null,
cmdBuilder,
CREATE_UNICODE_ENVIRONMENT,
IntPtr.Zero,
workDir,
ref si,
out PROCESS_INFORMATION pi);
if (!ok)
{
int err = Marshal.GetLastWin32Error();
LogHelper.WriteLogToFile($"UIAccess | CreateProcessWithTokenW 失败: {err}", LogHelper.LogType.Error);
return false;
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
LogHelper.WriteLogToFile($"UIAccess | 已使用 UIAccess 令牌启动新进程 (PID={pi.dwProcessId})");
return true;
}
private static void AppendQuoted(StringBuilder sb, string arg)
{
if (arg == null) { sb.Append("\"\""); return; }
bool needQuote = arg.Length == 0 || arg.IndexOfAny(new[] { ' ', '\t', '"' }) >= 0;
if (!needQuote) { sb.Append(arg); return; }
sb.Append('"');
int backslashes = 0;
foreach (char c in arg)
{
if (c == '\\') { backslashes++; continue; }
if (c == '"')
{
sb.Append('\\', backslashes * 2 + 1);
sb.Append('"');
}
else
{
sb.Append('\\', backslashes);
sb.Append(c);
}
backslashes = 0;
}
sb.Append('\\', backslashes * 2);
sb.Append('"');
}
#endregion
}
}
@@ -36,12 +36,6 @@ namespace Ink_Canvas.Windows.SettingsViews.Helpers
[DllImport("user32.dll")]
private static extern bool IsIconic(IntPtr hWnd);
[DllImport("UIAccessDLL_x86.dll", EntryPoint = "PrepareUIAccess", CallingConvention = CallingConvention.Cdecl)]
private static extern Int32 PrepareUIAccessX86();
[DllImport("UIAccessDLL_x64.dll", EntryPoint = "PrepareUIAccess", CallingConvention = CallingConvention.Cdecl)]
private static extern Int32 PrepareUIAccessX64();
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
@@ -300,6 +294,14 @@ namespace Ink_Canvas.Windows.SettingsViews.Helpers
{
try
{
// 已具有 UIAccess 时无需重启
if (UIAccessHelper.HasUIAccess())
{
LogHelper.WriteLogToFile("UIAccess | 当前进程已具有 UIAccess 权限");
App.IsUIAccessTopMostEnabled = true;
return;
}
OnStopKillProcessTimer?.Invoke();
if (App.watchdogProcess != null && !App.watchdogProcess.HasExited)
@@ -308,18 +310,24 @@ namespace Ink_Canvas.Windows.SettingsViews.Helpers
App.watchdogProcess = null;
}
App.StartWatchdogIfNeeded();
// 使用 Inkeys 方式:通过 winlogon 模拟令牌为自身令牌设置 UIAccess 标志后重启
App.IsUIAccessTopMostEnabled = true;
App.IsAppExitByUser = true;
(Application.Current as App)?.ReleaseMutexForRestart();
if (Environment.Is64BitProcess)
bool started = UIAccessHelper.RestartWithUIAccess();
if (started)
{
PrepareUIAccessX64();
Application.Current.Shutdown();
}
else
{
PrepareUIAccessX86();
LogHelper.WriteLogToFile("UIAccess | 启动失败,回退到普通管理员模式", LogHelper.LogType.Warning);
App.IsUIAccessTopMostEnabled = false;
App.IsAppExitByUser = false;
App.StartWatchdogIfNeeded();
OnStartKillProcessTimer?.Invoke();
}
OnStartKillProcessTimer?.Invoke();
}
catch (Exception ex)
{