diff --git a/Ink Canvas/Helpers/UIAccessHelper.cs b/Ink Canvas/Helpers/UIAccessHelper.cs new file mode 100644 index 00000000..cc0335d1 --- /dev/null +++ b/Ink Canvas/Helpers/UIAccessHelper.cs @@ -0,0 +1,557 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ink_Canvas.Helpers +{ + /// + /// 通过 Winlogon 令牌模拟实现 UIAccess 提权重启。 + /// 1. 找到当前会话中 winlogon.exe 的令牌,复制为模拟令牌; + /// 2. SetThreadToken 暂时模拟 winlogon(拥有 TCB 权限); + /// 3. 在自身令牌副本上 SetTokenInformation(TokenUIAccess, TRUE); + /// 4. RevertToSelf 后用 CreateProcessWithTokenW 启动新进程; + /// 5. 新进程具有 UIAccess 权限,可置顶于 UAC 提示之上。 + /// + 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 + + /// + /// 检查当前进程是否已具有 UIAccess 标志。 + /// + 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); + } + } + + /// + /// 以 UIAccess 令牌重启自身。当前进程必须已经以管理员身份运行。 + /// 成功时新进程已启动,调用方应立即退出当前进程。 + /// + /// 追加到新进程的额外命令行参数(例如 --skip-mutex-check)。 + 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; + } + + // 启用 SeAssignPrimaryTokenPrivilege(Inkeys 行为) + 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 + } +} \ No newline at end of file diff --git a/Ink Canvas/Windows/SettingsViews/Helpers/WindowSettingsHelper.cs b/Ink Canvas/Windows/SettingsViews/Helpers/WindowSettingsHelper.cs index ca915e7b..a99d21ab 100644 --- a/Ink Canvas/Windows/SettingsViews/Helpers/WindowSettingsHelper.cs +++ b/Ink Canvas/Windows/SettingsViews/Helpers/WindowSettingsHelper.cs @@ -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) {