Windows 10中的窗体Z序

定义窗体 Z-order :“段”
澄清一下,在这种情况下,“段”一词指的是Z-order组
在Windows 8之前,只有一个段,即ZBID_DESKTOP段,这是您编写创建新窗口的应用程序时的默认段,当它获得焦点时,它将进入最高的Z序,这意味着它将位于其他窗口之上。除非有置顶窗体,否则这是绝对正确的。顾名思义,它将保持在其他窗口之上。如果有两个置顶窗体怎么办?好吧,在这种情况下,最后获得焦点的窗口将停留在另一个窗口之上。最后,在ZBID_DESKTOP波段中有两组Z序,正常和最高。无论如何,这些将永远不会被彼此“碰触”,置顶的窗口将始终位于其他正常窗口的顶部。
从Windows 8开始,Microsoft“引入”了其他窗体段,并且所有这些段的Z序均高于桌面窗体段。例如,开始菜单位于ZBID_IMMERSIVE_MOGO上。任务管理器“置于顶层”后位于ZBID_SYSTEM_TOOLS上。这些始终位于ZBID_DESKTOP区域中任何窗口的顶部,也就是说,由第三方开发人员创建的任何常规或置顶窗口都不能覆盖开始菜单的内容。这些窗体段无法相互重叠,例如,这意味着ZBID_DESKTOP将永远不会位于ZBID_IMMERSIVE_MOGO的顶部,也就是说,窗口段也具有其Z序。
像ZBID_DESKTOP一样,其他窗体段也可以细分为两个组:正常窗口和置顶窗口。
段的顺序如下,从最低到最高Z序(我也不知道某些ZBID顺序干什么用的)
- ZBID_DESKTOP
- ZBID_IMMERSIVE_BACKGROUND
- ZBID_IMMERSIVE_APPCHROME
- ZBID_IMMERSIVE_MOGO
- ZBID_IMMERSIVE_INACTIVEMOBODY
- ZBID_IMMERSIVE_NOTIFICATION
- ZBID_IMMERSIVE_EDGY
- ZBID_SYSTEM_TOOLS
- ZBID_LOCK(仅Windows 10)
- ZBID_ABOVELOCK_UX(仅Windows 10)
- ZBID_IMMERSIVE_IHM
- ZBID_GENUINE_WINDOWS
- ZBID_UIACCESS
无序Z-order段
- ZBID_IMMERSIVE_INACTIVEDOCK
- ZBID_IMMERSIVE_ACTIVEMOBODY
- ZBID_IMMERSIVE_ACTIVEDOCK
- ZBID_IMMERSIVE_RESTRICTED(未使用)
可视化窗体段

ZBID_DESKTOP:该段是我们所有窗口停留的位置。假设您在此处有两个窗口,即“画图”和“照片”。两者都是桌面窗口,只要聚焦一个窗口或另一个窗口,它们便可以彼此重叠。现在假设“画图”窗口位于最上方,无论其是否获得焦点,它都将位于“照片”顶部。

ZBID_IMMERSIVE_MOGO:此区域由开始屏幕和任务栏使用(仅在打开开始屏幕的情况下)。

ZBID_IMMERSIVE_NOTIFICATIONS:由操作中心,通知和一些系统弹出窗口(例如,网络、音量)使用。

ZBID_SYSTEM_TOOLS:已启用“置于顶层”的任务管理器,Alt-Tab视图。

ZBID_ABOVELOCK_UX:由“即时播放”使用(即On Screen Display)。该段和其他更高的窗体段将保留在锁定屏幕的顶部。
- ZBID_IMMERSIVE_SEARCH:Cortana / Windows搜索
- ZBID_IMMERSIVE_INACTIVEMOBODY:画中画(UWP CompactOverlay)
- ZBID_IMMERSIVE_APPCHROME:任务视图
- ZBID_GENUINE_WINDOWS:“激活Windows”
技术部分
ZBID枚举
Windows 8.x / Windows 10
enum ZBID
{ZBID_DEFAULT = 0,ZBID_DESKTOP = 1,ZBID_UIACCESS = 2,ZBID_IMMERSIVE_IHM = 3,ZBID_IMMERSIVE_NOTIFICATION = 4,ZBID_IMMERSIVE_APPCHROME = 5,ZBID_IMMERSIVE_MOGO = 6,ZBID_IMMERSIVE_EDGY = 7,ZBID_IMMERSIVE_INACTIVEMOBODY = 8,ZBID_IMMERSIVE_INACTIVEDOCK = 9,ZBID_IMMERSIVE_ACTIVEMOBODY = 10,ZBID_IMMERSIVE_ACTIVEDOCK = 11,ZBID_IMMERSIVE_BACKGROUND = 12,ZBID_IMMERSIVE_SEARCH = 13,ZBID_GENUINE_WINDOWS = 14,ZBID_IMMERSIVE_RESTRICTED = 15,ZBID_SYSTEM_TOOLS = 16,//Windows 10+ZBID_LOCK = 17,ZBID_ABOVELOCK_UX = 18,
};
CreateWindowInBand
这是User32.dll中的私有API函数。由于Windows SDK标头/库中不存在该函数,因此必须使用GetProcAddress来调用该函数。
CreateWindowInBand函数与CreateWindowEx相同,除了它还有1个参数dwBand,指定窗口应停留范围(ZBID)的参数。
HWND WINAPI CreateWindowInBand(DWORD dwExStyle,LPCWSTR lpClassName,LPCWSTR lpWindowName,DWORD dwStyle,int x,int y,int nWidth,int nHeight,HWND hWndParent,HMENU hMenu,HINSTANCE hInstance,LPVOID lpParam,DWORD dwBand
);
CreateWindowInBandEx
User32.dll中的私有API函数。
与CreateWindowInBand相同,外加1个参数dwTypeFlags
HWND WINAPI CreateWindowInBandEx(DWORD dwExStyle,LPCWSTR lpClassName,LPCWSTR lpWindowName,DWORD dwStyle,int x,int y,int nWidth,int nHeight,HWND hWndParent,HMENU hMenu,HINSTANCE hInstance,LPVOID lpParam,DWORD dwBand,DWORD dwTypeFlags
);
SetWindowBand
User32.dll中的私有API函数。
SetWindowBand函数有3个参数:hWnd,hWndInserAfter和dwBand。第二个参数与SetWindowPos中的第二个参数相同。返回值指示成功或失败。如果失败,请调用GetLastError。我有99.9%的把握确定您得到了一个错误0x5(拒绝访问)
BOOL WINAPI SetWindowBand(HWND hWnd, HWND hwndInsertAfter, DWORD dwBand
);
GetWindowBand
User32.dll中的私有API函数。
GetWindowBand函数具有2个参数,hWnd和pdwBand。pdwBand是接收HWND的窗体段(ZBID)的指针。返回值表示成功或失败。如果失败,请调用GetLastError。
BOOL WINAPI GetWindowBand(HWND hWnd, PDWORD pdwBand
);
我可以调用这些API吗?
简单来说,是的(在某些条件下),有时候不行。
- GetWindowBand在任何情况下都可工作,您可以毫无问题地使用它。
- 仅当您将ZBID_DEFAULT或ZBID_DESKTOP作为dwBand参数传递时, CreateWindowInBand / Ex才有效。此外,仅当进程具有UIAccess令牌时才允许ZBID_UIACCESS(例如,可以通过在app.manifest中设置uiAccess = true来获得,了解更多信息)。使用其他任何ZBID都会失败,并显示为0x5(拒绝访问)。
- SetWindowBand将永远无法正常工作,它将始终失败并显示0x5(拒绝访问)。
为什么我不能使用其它ZBID?
因为Microsoft为这些ZBID增加了额外的检查
为了使CreateWindowInBand / Ex能够使用更多的ZBID,该程序必须具有一个名为“ .imrsiv”(bss_seg)的特殊PE标头,标有IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY,并且用“Microsoft Windows”的证书设置数字签名(所以只有微软自己的应用程序才能调用此类函数)。
对于SetWindowBand,嗯……目前我不知道它内部是如何工作的,但是它有额外的检查。Windows资源管理器通过twinui.pcshell.dll调用它

使用此功能可以更改窗口段,对于Windows资源管理器,可以将任务栏从ZBID_DESKTOP改变为ZBID_IMMERSIVE_MOGO(反之亦然)。
有没有办法绕过它?您做到了是吗?
是的。您可能不喜欢我的方式,但这(目前)是唯一的方式。
- 要CreateWindowInBand使用任何 ZBID,您需要将dll注入Immersive进程中。就我而言,我使用的是RuntimeBroker(explorer.exe很容易崩溃)。可以在此处找到代码示例:启动器(用于注入dll)和dll

- 要使用SetWindowBand,您需要将dll注入explorer.exe并hook SetWindowBand(没错, 就像一个蹦床一样)。当您点击“开始”按钮后,将会立刻调用该被hook的函数,并且可以使用其他目标的hwnd调用原始函数。这种方式显然很垃圾,但是呢,我还没有找到更好的方法(原作者发现调用NtUserEnableIAMAccess可能是更好的办法,但没有成功)。
- 后续补充 NtUserEnableIAMAccess 需要一个 uint64 “密钥”作为参数,并且该密钥与获取它的唯一进程相关联,IAM访问密钥貌似是在NtUserSetShellWindowEx中生成的,使用NtUserAcquireIAMKey只能获取一次。
当资源管理器启动时,它将自己设置为 Shell 窗口,然后 twinui.pcshell.dll 获得该密钥后,没有人可以再次获得密钥
如果资源管理器正在运行,SetShellWindowEx 会失败。
所以认为最好的解决方案是杀死当前的资源管理器,启动一个新的资源管理器并注入一个 DLL,挂钩 NtUserAcquireIAMKey 并获取该密钥。

结论
从Windows 8开始,Microsoft引入段的概念,一组永远不会彼此“接触”的Z序。其实有很多窗口段,但对于第三方开发人员来说,只能访问ZBID_DESKTOP(CreateWindow / Ex的默认段)。
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
