之前,我在博客中探讨了Windows 10/11中面向用户公开的虚拟桌面功能与Sysinternals的Desktops工具之间的区别。在这篇文章中,我想进一步阐述窗口工作站、桌面和窗口的相关知识。在继续阅读之前,我假设您已经阅读了之前的那篇博客文章。 我们了解到,窗口工作站是包含在会话中的。那么,我们能枚举这些窗口工作站吗?WindowsAPI中提供了EnumWindowStations API,但它只返回当前会话中的窗口工作站。并没有“EnumSessionWindowStations”这样的API。不过,窗口工作站是命名对象,因此在诸如WinObj(以管理员权限运行)这样的工具中是可以看到的:
会话 0 中的窗口站
在会话0中的窗口工作站位于\Windows\WindowStations 在会话x中的窗口工作站位于\Sessions\x\Windows\WindowStations OpenWindowStation API 仅接受调用者会话下的“本地”名称。而原生的 NtUserOpenWindowStation API(来自 Win32u.dll)则更加灵活,它接受完整的对象名称:
- HWINSTA NtUserOpenWindowStation(POBJECT_ATTRIBUTES attr, ACCESS_MASK access);
复制代码
以下是一个打开“msswindowstation”窗口站的示例:
- #include <Windows.h>
- #include <winternl.h>
-
- #pragma comment(lib, "ntdll")
-
- HWINSTA NTAPI _NtUserOpenWindowStation(_In_ POBJECT_ATTRIBUTES attr, _In_ ACCESS_MASK access);
- int main() {
- // force Win32u.DLL to load
- ::LoadLibrary(L"user32");
- auto NtUserOpenWindowStation = (decltype(_NtUserOpenWindowStation)*)
- ::GetProcAddress(::GetModuleHandle(L"win32u"), "NtUserOpenWindowStation");
-
- UNICODE_STRING winStaName;
- RtlInitUnicodeString(&winStaName, L"\\Windows\\WindowStations\\msswindowstation");
- OBJECT_ATTRIBUTES winStaAttr;
- InitializeObjectAttributes(&winStaAttr, &winStaName, 0, nullptr, nullptr);
- auto hWinSta = NtUserOpenWindowStation(&winStaAttr, READ_CONTROL);
- if (hWinSta) {
- // do something with hWinSta
- ::CloseWindowStation(hWinSta);
复制代码
您可能没有足够的权限来打开具有所需访问权限的句柄,这取决于所讨论的窗口站。在会话0中的窗口站几乎无法从非会话0进程访问,即使使用SYSTEM账户也是如此。您可以使用内核调试器来检查它们的安全描述符(因为其他工具会返回访问被拒绝的错误): - lkd> !object \Windows\WindowStations\msswindowstation
- Object: ffffe103f5321c00 Type: (ffffe103bb0f0ae0) WindowStation
- ObjectHeader: ffffe103f5321bd0 (new version)
- HandleCount: 4 PointerCount: 98285
- Directory Object: ffff808433e412b0 Name: msswindowstation
- lkd> dt nt!_OBJECT_HEADER ffffe103f5321bd0
-
- +0x000 PointerCount : 0n98285
- +0x008 HandleCount : 0n4
- +0x008 NextToFree : 0x00000000`00000004 Void
- +0x010 Lock : _EX_PUSH_LOCK
- +0x018 TypeIndex : 0xa2 ''
- +0x019 TraceFlags : 0 ''
- +0x019 DbgRefTrace : 0y0
- +0x019 DbgTracePermanent : 0y0
- +0x01a InfoMask : 0xe ''
- +0x01b Flags : 0 ''
- +0x01b NewObject : 0y0
- +0x01b KernelObject : 0y0
- +0x01b KernelOnlyAccess : 0y0
- +0x01b ExclusiveObject : 0y0
- +0x01b PermanentObject : 0y0
- +0x01b DefaultSecurityQuota : 0y0
- +0x01b SingleHandleEntry : 0y0
- +0x01b DeletedInline : 0y0
- +0x01c Reserved : 0
- +0x020 ObjectCreateInfo : 0xfffff801`21c53940 _OBJECT_CREATE_INFORMATION
- +0x020 QuotaBlockCharged : 0xfffff801`21c53940 Void
- +0x028 SecurityDescriptor : 0xffff8084`3da8aa6c Void
- +0x030 Body : _QUAD
- lkd> !sd 0xffff8084`3da8aa60
- ->Revision: 0x1
- ->Sbz1 : 0x0
- ->Control : 0x8014
- SE_DACL_PRESENT
- SE_SACL_PRESENT
- SE_SELF_RELATIVE
- ->Owner : S-1-5-18
- ->Group : S-1-5-18
- ->Dacl :
- ->Dacl : ->AclRevision: 0x2
- ->Dacl : ->Sbz1 : 0x0
- ->Dacl : ->AclSize : 0x1c
- ->Dacl : ->AceCount : 0x1
- ->Dacl : ->Sbz2 : 0x0
- ->Dacl : ->Ace[0]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
- ->Dacl : ->Ace[0]: ->AceFlags: 0x0
- ->Dacl : ->Ace[0]: ->AceSize: 0x14
- ->Dacl : ->Ace[0]: ->Mask : 0x0000011b
- ->Dacl : ->Ace[0]: ->SID: S-1-1-0
复制代码
你可以使用Sysinternals的PsExec工具以SYSTEM身份启动一个命令窗口(或任何其他程序),从而在保持交互会话的同时,获得SYSTEM权限的帮助: 复制代码
如果其他所有方法都失败了,你可能需要使用“取得所有权”权限,使自己成为对象的所有者,并更改其DACL(访问控制列表),以便为自己授予完全访问权限。然而,显然即使这样做也不一定能行,因为从另一个会话的窗口站获取某些内容似乎是被阻止的(参见Twitter线程中的回复)。不过,READ_CONTROL权限可以用来获取一些基本信息。 以下是对象资源管理器(Object Explorer)在SYSTEM权限下运行的截图,它显示了“msswindowstation”窗口站的一些详细信息:
猜猜哪些进程持有这个隐藏的窗口站的句柄?
一旦你能够获取到一个窗口站的句柄,如果你能够至少获得WINSTA_ENUMDESKTOPS访问掩码,那么你或许可以更进一步,通过枚举桌面来深入探索: - ::EnumDesktops(hWinSta, [](auto deskname, auto param) -> BOOL {
- printf(" Desktop: %ws\n", deskname);
- auto h = (HWINSTA)param;
- return TRUE;
- }, (LPARAM)hWinSta);
复制代码
再深入一层,你可以枚举每个桌面(如果有的话)中的顶级窗口。为此,你需要将进程连接到感兴趣的窗口站,然后调用EnumDesktopWindows函数: - void DoEnumDesktopWindows(HWINSTA hWinSta, PCWSTR name) {
- if (::SetProcessWindowStation(hWinSta)) {
- auto hdesk = ::OpenDesktop(name, 0, FALSE, DESKTOP_READOBJECTS);
- if (!hdesk) {
- printf("--- failed to open desktop %ws (%d)\n", name, ::GetLastError());
- return;
- }
- static WCHAR pname[MAX_PATH];
- ::EnumDesktopWindows(hdesk, [](auto hwnd, auto) -> BOOL {
- static WCHAR text[64];
- if (::IsWindowVisible(hwnd) && ::GetWindowText(hwnd, text, _countof(text)) > 0) {
- DWORD pid;
- auto tid = ::GetWindowThreadProcessId(hwnd, &pid);
- auto hProcess = ::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
- BOOL exeNameFound = FALSE;
- PWSTR exeName = nullptr;
- if (hProcess) {
- DWORD size = MAX_PATH;
- exeNameFound = ::QueryFullProcessImageName(hProcess, 0, pname, &size);
- ::CloseHandle(hProcess);
- if (exeNameFound) {
- exeName = ::wcsrchr(pname, L'\\');
- if (exeName == nullptr)
- exeName = pname;
- else
- exeName++;
- }
- }
- printf(" HWND: 0x%08X PID: 0x%X (%d) %ws TID: 0x%X (%d): %ws\n",
- (DWORD)(DWORD_PTR)hwnd, pid, pid,
- exeNameFound ? exeName : L"", tid, tid, text);
- }
- return TRUE;
- }, 0);
- ::CloseDesktop(hdesk);
- }
- }
复制代码
调用 SetProcessWindowStation 只能对属于当前会话的 Windows 工作站起作用。 以下是交互式会话(使用 EnumWindowStations 枚举的 Window Stations)的一个示例输出: - Window station: WinSta0
- Desktop: Default
- HWND: 0x00010E38 PID: 0x4D04 (19716) Zoom.exe TID: 0x5FF8 (24568): ZPToolBarParentWnd
- HWND: 0x000A1C7A PID: 0xB804 (47108) VsDebugConsole.exe TID: 0xDB50 (56144): D:\Dev\winsta\x64\Debug\winsta.exe
- HWND: 0x00031DE8 PID: 0xBF40 (48960) devenv.exe TID: 0x94E8 (38120): winsta - Microsoft Visual Studio Preview
- HWND: 0x00031526 PID: 0x1384 (4996) msedge.exe TID: 0xE7C (3708): zodiacon/ObjectExplorer: Explore Kernel Objects on Windows and
- HWND: 0x00171A9A PID: 0xA40C (41996) TID: 0x9C08 (39944): WindowStation (\Windows\WindowStations\msswindowstation)
- HWND: 0x000319D0 PID: 0xA40C (41996) TID: 0x9C08 (39944): Object Manager - Object Explorer 2.0.2.0 (Administrator)
- HWND: 0x001117DC PID: 0x253C (9532) ObjExp.exe TID: 0x9E10 (40464): Object Manager - Object Explorer 2.0.2.0 (Administrator)
- HWND: 0x00031CA8 PID: 0xBE5C (48732) devenv.exe TID: 0xC250 (49744): OpenWinSta - Microsoft Visual Studio Preview (Administrator)
- HWND: 0x000B1884 PID: 0xA8A0 (43168) DbgX.Shell.exe TID: 0xA668 (42600): - KD '', Local Connection - WinDbg 1.2306.12001.0 (Administra
- ...
- HWND: 0x000101C8 PID: 0x3598 (13720) explorer.exe TID: 0x359C (13724): Program Manager
- Window station: Service-0x0-45193$
- Desktop: sbox_alternate_desktop_0x6A80
- Desktop: sbox_alternate_desktop_0xA94C
- Desktop: sbox_alternate_desktop_0x3D8C
- Desktop: sbox_alternate_desktop_0x7EF8
- Desktop: sbox_alternate_desktop_0x72FC
- Desktop: sbox_alternate_desktop_0x27B4
- Desktop: sbox_alternate_desktop_0x6E80
- Desktop: sbox_alternate_desktop_0x6C54
- Desktop: sbox_alternate_desktop_0x68C8
- Desktop: sbox_alternate_desktop_0x691C
- Desktop: sbox_alternate_desktop_0x4150
- Desktop: sbox_alternate_desktop_0x6254
- Desktop: sbox_alternate_desktop_0x5B9C
- Desktop: sbox_alternate_desktop_0x59B4
- Desktop: sbox_alternate_desktop_0x1384
- Desktop: sbox_alternate_desktop_0x5480
复制代码
上面“Service-0x0-45193$”Windows 工作站中的桌面似乎没有顶级可见窗口。 如果你拥有足够强大的句柄,你也可以访问给定Windows工作站的剪贴板和原子表。我将此也留作一个练习。 最后,关于会话枚举呢?那是比较简单的部分——你不需要使用NtOpenSession来调用在对象管理器命名空间的“\KernelObjects”目录中可以找到的会话对象。你可以使用WTS(Windows Terminal Services)函数族。特别是,WTSEnumerateSessionsEx函数可以提供会话的一些重要属性: - void EnumSessions() {
- DWORD level = 1;
- PWTS_SESSION_INFO_1 info;
- DWORD count = 0;
- ::WTSEnumerateSessionsEx(WTS_CURRENT_SERVER_HANDLE, &level, 0, &info, &count);
- for (DWORD i = 0; i < count; i++) {
- auto& data = info[i];
- printf("Session %d (%ws) Username: %ws\\%ws State: %s\n", data.SessionId, data.pSessionName,
- data.pDomainName ? data.pDomainName : L"NT AUTHORITY", data.pUserName ? data.pUserName : L"SYSTEM",
- StateToString((WindowStationState)data.State));
- }
- ::WTSFreeMemory(info);
- }
复制代码
关于创建一个进程以使用不同的Windows工作站和桌面,这是可以做到的。在调用CreateProcess函数时,传递给它的STARTUPINFO结构体的一个成员(lpDesktop)允许你设置一个桌面名称,以及一个可选的、由反斜杠分隔的Windows工作站名称(例如,“MyWinSta\MyDesktop”)。 Windows工作站和桌面的内涵远比表面上看到的要丰富……这应该能为有兴趣的读者在进行进一步研究时提供一个良好的起点。
原文链接 |