尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Windows窗口超级置顶:绕过UIPI限制的令牌窃取技术实践

Windows窗口超级置顶:绕过UIPI限制的令牌窃取技术实践 1. 项目概述与核心思路拆解最近在折腾一些自动化脚本和桌面工具时遇到了一个经典的老问题如何让一个普通权限的应用程序窗口能够稳定地“置顶”在所有其他窗口之上甚至盖过那些需要管理员权限或更高完整性级别的窗口比如任务管理器、某些安全软件的弹窗常规的SetWindowPos配合HWND_TOPMOST标志在很多情况下会失效尤其是当目标窗口的权限比你高的时候。这不仅仅是前端显示的问题更深层涉及到 Windows 操作系统的安全模型——用户界面特权隔离UIPI。为了解决这个痛点我深入研究了 Windows 的令牌Token机制和 UIAccess 权限并实现了一种无需修改程序清单Manifest、直接通过令牌操作提权的方法让普通程序也能获得“超级置顶”的能力。简单来说这个项目的核心就是“借壳生蛋”。我们不直接修改或提升自己进程的权限而是巧妙地“窃取”一个已经拥有我们所需权限这里是UIAccess的进程的访问令牌然后用这个令牌创建一个新的、具有高权限的进程实例。这个新进程和我们原来的程序在功能上完全一致但它运行在一个更高的权限上下文里从而绕过了 UIPI 的限制实现了真正的窗口超级置顶。整个过程完全在用户态完成不依赖任何未公开的 API 或内核漏洞稳定性很高。下面我就把整个研究思路、技术细节、实操步骤以及踩过的坑毫无保留地分享出来。2. 技术原理深度解析令牌、UIAccess 与 UIPI要理解这个方案必须先把 Windows 中几个关键的安全概念理清楚。很多人知道管理员权限Administrator但对 UIAccess 和 UIPI 可能比较陌生。这正是实现窗口层级控制的关键所在。2.1 访问令牌Access Token是什么你可以把访问令牌想象成进程的“身份证”和“通行证”。每当一个进程启动时Windows 都会为其创建一个主令牌Primary Token。这个令牌里封装了该进程所属用户的安全标识符SID、所属的组、特权列表Privileges比如SeDebugPrivilege可以用来调试其他进程以及完整性级别Integrity Level。进程在访问任何受保护的对象如文件、注册表键、甚至是其他进程时系统都会检查其令牌中的信息来决定是否允许访问。我们常说的“提权”很多时候就是在操作这个令牌为其启用某个特权或者提升其完整性级别。2.2 用户界面特权隔离UIPI如何限制窗口消息UIPI 是从 Windows Vista 开始引入的一种安全机制旨在防止低权限进程通过窗口消息攻击高权限进程。它基于进程的完整性级别来实施限制。完整性级别从低到高大致分为低Low、中Medium、高High、系统System。一个中完整性级别的进程无法向高或系统完整性级别的进程发送特定的窗口消息比如WM_COPYDATA、SendMessage中的某些操作也无法通过SetWindowPos等函数将窗口置于更高完整性级别进程的窗口之上。这就是为什么你用SetWindowPos设置HWND_TOPMOST后你的窗口仍然可能被任务管理器通常是高完整性级别盖住的原因。UIPI 在底层阻止了这种越级的窗口层级操作。2.3 UIAccess用户界面访问权限的突破口那么有没有一种“合法”的途径可以让一个进程获得超越其完整性级别的窗口操作能力呢答案是有的这就是UIAccess。UIAccess是一个在程序清单manifest中声明的属性。当一个程序被标记为UIAccess”true”并且满足一定条件如存放在受保护路径如Program Files或Windows\System32且具有有效的数字签名后启动系统会赋予它一个特殊的权限。拥有UIAccess权限的进程其窗口可以绕过 UIPI 的部分限制。具体来说它可以将窗口置于更高完整性级别的窗口之上。向更高完整性级别的进程发送键盘和鼠标输入消息。读取更高完整性级别进程的屏幕内容用于辅助功能等。这简直完美契合了我们“超级置顶”的需求但是给我们的程序添加清单、签名、并放到特定目录流程繁琐且不灵活。我们的思路就是找到一个已经拥有UIAccess权限的进程比如系统自带的某些辅助工具或者一些已知的、常驻的UIAccess程序借用它的令牌来启动我们的程序副本。2.4 核心 API 函数链整个实现依赖于几个关键的 Windows APIOpenProcess: 打开目标进程的句柄。OpenProcessToken: 打开目标进程的令牌。DuplicateTokenEx: 复制令牌并可以指定新的令牌类型如模拟令牌 Impersonation Token和访问权限。SetTokenInformation: 这是关键一步用于修改令牌的信息。我们需要用它将复制的令牌的完整性级别提升到“高”SECURITY_MANDATORY_HIGH_RID这是拥有UIAccess效果所必需的级别之一通常UIAccess进程运行在高完整性级别。CreateProcessWithTokenW: 使用我们修改好的令牌创建一个新的进程。这个新进程将“继承”该令牌的所有属性包括高完整性级别从而在行为上具备UIAccess的能力。注意CreateProcessWithTokenW函数要求调用者本身必须持有SeImpersonatePrivilege特权。在标准的用户账户下这个特权默认是禁用的。管理员账户下该特权默认启用。因此我们的“提权”程序本身可能需要以管理员身份运行一次来启用这个特权或者寻找其他具有该特权的进程作为跳板。这是一个重要的前提条件。3. 实操步骤与核心代码实现理论讲完了我们进入实战环节。我会用一个 C 的示例来演示整个过程并解释每一个参数和步骤的用意。请确保你的开发环境支持 Windows SDK。3.1 环境准备与目标进程选择首先我们需要选择一个“令牌捐赠者”。理想的目标是它是一个拥有UIAccess权限的进程。它相对稳定不会频繁退出。它的令牌我们可以打开有足够的权限。一个常见且可靠的目标是explorer.exeWindows 资源管理器。在默认配置下explorer.exe运行在中完整性级别但它有时会为了兼容性运行在某些特殊上下文中。更稳妥的方法是寻找那些明确设计为辅助功能的程序或者我们自己可以提前部署一个带有UIAccess清单的“傀儡”进程。为了演示的通用性我们这里以打开一个指定 PID 的进程为例。在实际操作中你可能需要先枚举进程找到符合条件的 PID。#include windows.h #include iostream #include sddl.h // 用于完整性级别SID // 提升当前进程的调试权限以便打开某些系统进程 bool EnableDebugPrivilege() { HANDLE hToken; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, hToken)) { return false; } TOKEN_PRIVILEGES tp; LUID luid; if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, luid)) { CloseHandle(hToken); return false; } tp.PrivilegeCount 1; tp.Privileges[0].Luid luid; tp.Privileges[0].Attributes SE_PRIVILEGE_ENABLED; if (!AdjustTokenPrivileges(hToken, FALSE, tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL)) { CloseHandle(hToken); return false; } CloseHandle(hToken); return GetLastError() ERROR_SUCCESS; }3.2 令牌窃取与提权核心流程假设我们已经通过某种方式如用户输入、配置文件或进程查找获得了目标进程的 PIDdwTargetPid以及我们想要启动的程序路径lpApplicationPath通常就是当前程序自身的路径。BOOL CreateProcessWithUIAccessToken(DWORD dwTargetPid, LPCWSTR lpApplicationPath) { HANDLE hTargetProcess NULL; HANDLE hTargetToken NULL; HANDLE hNewToken NULL; BOOL bResult FALSE; // 1. 打开目标进程 hTargetProcess OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, dwTargetPid); if (hTargetProcess NULL) { std::cerr OpenProcess failed: GetLastError() std::endl; goto cleanup; } // 2. 打开目标进程的令牌 if (!OpenProcessToken(hTargetProcess, TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ADJUST_DEFAULT, hTargetToken)) { std::cerr OpenProcessToken failed: GetLastError() std::endl; goto cleanup; } // 3. 复制令牌创建一个新的主令牌Primary Token // TOKEN_TYPE 设为 TokenPrimary用于创建新进程。 // SECURITY_IMPERSONATION_LEVEL 可以设为 SecurityAnonymous因为我们要的是主令牌。 if (!DuplicateTokenEx(hTargetToken, MAXIMUM_ALLOWED, NULL, SecurityAnonymous, TokenPrimary, hNewToken)) { std::cerr DuplicateTokenEx failed: GetLastError() std::endl; goto cleanup; } // 4. 提升新令牌的完整性级别为“高” // 这是模拟 UIAccess 行为的关键。UIAccess 进程通常运行在高完整性级别。 BOOL bSetIL SetTokenIntegrityLevel(hNewToken, SECURITY_MANDATORY_HIGH_RID); if (!bSetIL) { std::cerr SetTokenIntegrityLevel failed: GetLastError() std::endl; // 注意即使失败有时也可能继续但可能没有UIAccess效果。 // 可以根据实际情况决定是否 goto cleanup; } // 5. 使用新令牌创建进程 STARTUPINFOW si { sizeof(si) }; PROCESS_INFORMATION pi { 0 }; // 关键CREATE_NEW_CONSOLE 或 0 都可以取决于你的程序类型。 // 这里我们不需要继承句柄。 si.dwFlags STARTF_USESHOWWINDOW; si.wShowWindow SW_SHOW; // 正常显示窗口 bResult CreateProcessWithTokenW(hNewToken, LOGON_WITH_PROFILE, // 加载用户配置文件 lpApplicationPath, // 应用程序路径 NULL, // 命令行可为空从lpApplicationPath解析 CREATE_NEW_CONSOLE, // 创建标志 NULL, // 环境块继承 NULL, // 当前目录继承 si, pi); if (!bResult) { std::cerr CreateProcessWithTokenW failed: GetLastError() std::endl; } else { std::cout New process created with PID: pi.dwProcessId std::endl; // 记得关闭返回的进程和线程句柄避免泄漏 CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } cleanup: if (hNewToken) CloseHandle(hNewToken); if (hTargetToken) CloseHandle(hTargetToken); if (hTargetProcess) CloseHandle(hTargetProcess); return bResult; } // 辅助函数设置令牌完整性级别 BOOL SetTokenIntegrityLevel(HANDLE hToken, DWORD dwIntegrityLevel) { BOOL bResult FALSE; PWSTR pwszIntegritySid NULL; // 根据完整性级别RID构造SID字符串 switch (dwIntegrityLevel) { case SECURITY_MANDATORY_LOW_RID: pwszIntegritySid LS-1-16-4096; // 低完整性级别 break; case SECURITY_MANDATORY_MEDIUM_RID: pwszIntegritySid LS-1-16-8192; // 中完整性级别 break; case SECURITY_MANDATORY_HIGH_RID: pwszIntegritySid LS-1-16-12288; // 高完整性级别 break; case SECURITY_MANDATORY_SYSTEM_RID: pwszIntegritySid LS-1-16-16384; // 系统完整性级别 break; default: return FALSE; } PSID pIntegritySid NULL; if (!ConvertStringSidToSidW(pwszIntegritySid, pIntegritySid)) { return FALSE; } TOKEN_MANDATORY_LABEL tml { 0 }; tml.Label.Attributes SE_GROUP_INTEGRITY; tml.Label.Sid pIntegritySid; // 调用 SetTokenInformation 设置令牌的完整性标签 bResult SetTokenInformation(hToken, TokenIntegrityLevel, tml, sizeof(TOKEN_MANDATORY_LABEL) GetLengthSid(pIntegritySid)); LocalFree(pIntegritySid); return bResult; }3.3 窗口置顶逻辑的注入新创建的高权限进程需要知道它要执行什么任务。一个优雅的方式是通过进程间通信IPC比如命令行参数、命名管道、共享内存等告诉新进程“你的任务是把窗口置顶”。这里我们用最简单的命令行参数传递窗口句柄HWND。我们在主程序中先获取自己窗口的句柄然后将其作为参数传递给新进程。// 在主程序中 int main() { // ... 你的程序初始化创建窗口等 ... HWND hWnd GetConsoleWindow(); // 如果是控制台程序获取控制台窗口句柄 // 或者对于 GUI 程序是你创建的窗口句柄例如 hWnd CreateWindow(...); if (!EnableDebugPrivilege()) { std::cout Warning: Could not enable debug privilege. May fail to open system processes. std::endl; } // 假设我们目标进程PID是 explorer.exe 的这里需要先获取它。 DWORD dwExplorerPid 0; // 这里省略了查找 explorer.exe PID 的代码可以使用 CreateToolhelp32Snapshot 等API。 // 构造命令行传递窗口句柄作为参数 WCHAR cmdLine[MAX_PATH * 2]; // 获取当前可执行文件路径用于启动新进程 WCHAR selfPath[MAX_PATH]; GetModuleFileNameW(NULL, selfPath, MAX_PATH); // 将窗口句柄一个数值转换为字符串作为参数传递 swprintf_s(cmdLine, L\%s\ --topmost-hwnd %lld, selfPath, (long long)hWnd); // 调用函数使用目标进程令牌创建新进程并传递构造好的命令行 // 注意CreateProcessWithUIAccessToken 函数需要修改以接受命令行参数 // 我们将 lpApplicationPath 设为 selfPath并将 cmdLine 作为命令行参数传递给 CreateProcessWithTokenW BOOL success CreateProcessWithUIAccessToken(dwExplorerPid, selfPath, cmdLine); // 假设函数签名已扩展 if (success) { std::cout High-integrity process launched. The window should now be topmost. std::endl; // 主进程可以退出或者继续做其他事情。置顶任务由新进程负责。 // 注意主进程窗口可能被新进程的窗口覆盖或关系改变需根据实际情况处理。 } // ... 主程序其他逻辑 ... return 0; }然后在新启动的高权限进程中我们需要解析命令行参数获取窗口句柄并执行置顶操作。// 在高权限子进程的主函数中 int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) { int argc; LPWSTR* argv CommandLineToArgvW(GetCommandLineW(), argc); HWND hWndToTop NULL; for (int i 1; i argc; i) { // i从1开始跳过程序路径本身 if (wcscmp(argv[i], L--topmost-hwnd) 0 i 1 argc) { hWndToTop (HWND)_wcstoi64(argv[i 1], NULL, 10); break; } } LocalFree(argv); if (hWndToTop ! NULL IsWindow(hWndToTop)) { // 关键置顶操作 SetWindowPos(hWndToTop, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); // 为了更稳定可以尝试多次设置或者结合其他标志 // SetWindowPos(hWndToTop, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); std::cout Window set to TOPMOST from high integrity process. std::endl; } else { std::cerr Invalid or no window handle provided. std::endl; } // 这个进程可以立即退出因为置顶操作是一次性的。 // 如果需要持久化置顶比如窗口移动后依然置顶则需要运行消息循环或定时检查。 // 这里为了简单我们操作完就退出。 return 0; }4. 常见问题、排查技巧与安全考量在实际操作中你肯定会遇到各种各样的问题。下面是我在开发和测试过程中遇到的一些典型情况及其解决方法。4.1 错误代码 5访问被拒绝 (ERROR_ACCESS_DENIED)这是最常见的问题通常发生在OpenProcess或OpenProcessToken阶段。原因1权限不足。即使启用了SeDebugPrivilege对于某些受保护的系统进程如csrss.exe,services.exe仍然需要更高的权限通常是SYSTEM权限。explorer.exe在大多数情况下可以被标准管理员打开。解决选择权限要求更低的目标进程。可以尝试以SYSTEM权限运行你的程序不推荐过于危险或者寻找那些运行在用户上下文下的、非关键的系统进程。一个更安全的目标是dwm.exe桌面窗口管理器但它也可能有保护。原因2目标进程运行在低于当前进程的完整性级别且没有启用SeImpersonatePrivilege。UIPI 也会影响进程句柄的打开。解决确保你的“提权”程序运行在较高的完整性级别如管理员权限并且成功启用了SeImpersonatePrivilege。4.2 错误代码 1314客户端没有所需的特权 (ERROR_PRIVILEGE_NOT_HELD)这个错误通常来自CreateProcessWithTokenW。原因调用进程的令牌中没有启用SeImpersonatePrivilege特权。这是该 API 的硬性要求。解决以管理员身份运行最简单的方法。管理员组的成员默认拥有此特权但可能是禁用的我们的EnableDebugPrivilege函数启用了SeDebugPrivilege但SeImpersonatePrivilege通常也需要启用。你需要修改函数同时启用这两个特权。使用已有特权的进程令牌如果你的程序本身无法以管理员运行可以考虑多阶段操作。先以一个拥有该特权的进程如服务完成令牌复制和提升然后通过 IPC 让该服务帮你创建高权限进程。这复杂度陡增。4.3 窗口置顶后仍然被覆盖原因1完整性级别提升不够。仅仅提升到“高”可能还不够。某些系统核心UI组件运行在“系统”级别。或者目标窗口本身可能也通过某种方式获得了高权限。排查使用Process ExplorerSysinternals 套件中的工具查看你新创建进程的Integrity Level是否确实为High。同时查看那些能覆盖你的窗口的进程的完整性级别。解决尝试将令牌提升到SECURITY_MANDATORY_SYSTEM_RID系统级别。警告这非常危险以系统权限运行的程序拥有至高无上的权力一个错误就可能导致系统崩溃或不稳定。仅在绝对必要且受控的环境下尝试并且确保你的程序逻辑绝对安全。原因2窗口样式或所有者关系。HWND_TOPMOST对于拥有者窗口owned windows或有特殊样式的窗口可能行为异常。解决尝试在置顶前先使用SetWindowLongPtr移除一些可能冲突的样式或者确保窗口没有不合适的父窗口或所有者。4.4 安全软件拦截与行为误报由于这种技术涉及进程令牌操作和创建高权限进程它非常容易被安全软件如杀毒软件、EDR标记为可疑或恶意行为例如“进程空心化”Process Hollowing或“令牌窃取”Token Theft攻击的典型特征。缓解措施代码签名为你的最终可执行文件购买有效的代码签名证书并签名。这能极大降低误报率。明确用途在软件说明中清晰表明该功能用于辅助窗口管理非恶意用途。用户知情在触发此操作前明确告知用户并取得同意。白名单引导用户将你的程序添加到安全软件的白名单中。4.5 稳定性与资源管理句柄泄漏示例代码中使用了goto cleanup模式来集中清理这是一个好习惯。务必确保每一个成功打开的句柄HANDLE在函数退出前都被CloseHandle。泄漏的句柄会占用系统资源。目标进程退出如果你依赖一个特定的进程如explorer.exe作为令牌源而用户重启了资源管理器你的逻辑就会失败。需要增加健壮性检查例如定期检查目标进程是否存在或准备备用进程列表。新进程的生命周期在我们的简单示例中高权限子进程置顶窗口后就退出了。如果窗口状态需要持续维护例如窗口移动或调整大小时保持置顶子进程需要保持运行并可能需要一个消息循环来响应WM_WINDOWPOSCHANGING等消息重新应用HWND_TOPMOST。这增加了复杂性。5. 进阶思路与替代方案探讨掌握了基础方法后我们可以思考一些更深入或更优雅的解决方案。5.1 寻找更稳定的“令牌源”与其依赖用户环境中可能不稳定的进程不如自己创建一个可靠的UIAccess令牌源。制作一个合法的 UIAccess 程序创建一个简单的、带有正确清单和数字签名的程序。它的唯一任务就是运行一个服务端等待连接。清单文件示例 (app.manifest):?xml version1.0 encodingUTF-8 standaloneyes? assembly xmlnsurn:schemas-microsoft-com:asm.v1 manifestVersion1.0 trustInfo xmlnsurn:schemas-microsoft-com:asm.v3 security requestedPrivileges requestedExecutionLevel levelasInvoker uiAccesstrue/ /requestedPrivileges /security /trustInfo /assembly将这个程序安装到Program Files目录下并确保其有有效的签名即使是自签名证书也需要用户手动安装证书并信任。你的主程序通过 IPC如命名管道向这个“服务”发送请求由这个高权限服务来执行创建窗口或置顶窗口的操作。这样主程序完全不需要提权所有敏感操作都委托给了合法的UIAccess进程。5.2 利用 AppContainer 和 Lower IL 的思考我们一直在讨论如何“向上”突破。反过来想有没有办法让那些“高高在上”的窗口自愿“降级”呢对于自己控制的程序可以尝试让需要被置顶的窗口运行在较低的完整性级别而我们的控制器运行在中等级别。这样由于 UIPI 的限制是单向的低不能向高发送消息但高可以向低发送中等权限的控制器可以轻松操作低权限窗口。但这显然不适用于控制第三方高权限窗口。5.3 与驱动级方案对比还有一种更底层的方案是使用内核驱动Kernel Driver直接操作窗口管理器如win32k.sys的数据结构。这种方法能力极强可以无视所有用户层的限制。但是其缺点也非常明显开发难度极高涉及内核编程容易引发蓝屏BSOD。签名要求严格从 Windows 10 开始内核驱动必须有微软的扩展验证EV代码签名普通开发者几乎无法获取。安全风险巨大此类驱动一旦被恶意利用后果严重。 因此对于窗口置顶这种需求驱动方案是典型的“杀鸡用牛刀”且法律和安全风险使其不具实用性。我们讨论的用户态令牌方案在能力和复杂性之间取得了较好的平衡。6. 总结与最终建议通过“令牌窃取”实现窗口超级置顶是一个深入理解 Windows 安全模型的有趣实践。它巧妙地利用了系统已有的UIAccess机制通过合法的 API 组合达到了提升窗口层级的目的。回顾整个流程其技术要点在于找到合适令牌源、复制并提升令牌完整性级别、利用高权限令牌创建新进程。在实际应用时我强烈建议你优先考虑“自制 UIAccess 服务”的方案。虽然前期需要处理签名和部署但这是最稳定、最合规、最不易被安全软件干扰的方式。它清晰地将高权限模块与普通业务逻辑分离符合安全开发的最佳实践。如果你决定使用文中直接窃取系统进程令牌的方法请务必注意充分测试在不同 Windows 版本Win10, Win11和不同安全配置下进行测试。优雅降级当提权失败时程序应有回退机制例如使用普通的SetWindowPos而不是直接崩溃。用户提示在尝试提权操作前告知用户可能触发的安全软件警报。代码清晰做好错误处理和日志记录方便排查问题。最后技术本身是中性的。这项技术既可以用于开发更强大的辅助工具、桌面增强软件也可能被用于制作令人厌烦的广告软件或恶意程序。作为开发者我们在掌握技术的同时更应思考如何用它来创造积极正面的用户体验并始终对安全保持敬畏。希望这篇长文能为你打开一扇窗不仅仅是关于窗口置顶更是关于如何深入理解和安全地运用操作系统底层机制。
返回列表