-
-
[原创]SetWindowDisplayAffinity:原理与检测(DWM篇)
-
发表于: 1小时前 42
-
窗口隐藏与反截屏系列 · 第 1 期
工具:IDA · CE · Hawkeye Community · Cursor MCP
目录
1. 背景与系列规划
SetWindowDisplayAffinity(常见用法:WDA_EXCLUDEFROMCAPTURE)是一种广为人知的反截屏隐私手段。基于此我们打算使用三期来围绕这个话题展开探索。
本系列共三篇,本篇为第一期:
- 第 1 期(本文):
SetWindowDisplayAffinity原理与检测 - 第 2 期:Visual 层隐藏原理与检测
- 第 3 期:基于 BitBlt 的「超级截屏」方案
2. win32k 内核链
用户态调用进入内核后,落脚在 win32kfull!NtUserSetWindowDisplayAffinity,核心逻辑在 SetDisplayAffinity。简化调用链如下:
NtUserSetWindowDisplayAffinity
└─ SetDisplayAffinity
├─ ComposeWindowIfNeeded
├─ InternalSetProp / InternalRemoveProp("SysDispAffinity")
└─ ChangeWindowTreeProtection
└─ ProtectWindowBitmap
├─ ChangeWindowBitmapOwner
└─ GreProtectSpriteContent
└─ DwmAsyncUpdateSprite → dwm.exe
DwmAsyncUpdateSprite 最终发送了一个 LPC 消息到 dwm.exe 进程。在整个过程中,除了对 tagWND 中 SysDispAffinity 属性有所涉及,其他相关改动较少。另外,不同 Windows 版本中属性编号时常变动,因此,基于内核去检测窗口的隐藏,在稳定性和兼容性上会差一些。
经过对 dwmcore.dll 的研究,发现截屏路径与直接显示路径是不同的,分别对应 CCaptureRenderTarget 与 CDDisplayRenderTarget。结合更多观察与分析,可以给出判定:在反截屏方面,DWM 进程负责完整逻辑,内核方面作用有限。
3. DWM 侧线索
窗口是否「截屏隐藏」,从机制上应体现在 DWM CCaptureRenderTarget 的合成输出上。dwm.exe 加载多个模块;但嫌疑最大的应该是 dwm.exe、dwmcore.dll、dwmredir.dll、uDWM.dll(下文先在这几个模块里搜线索)——毕竟名字里都带 dwm。
在 Hawkeye Community 中先用 !probe -pid:<dwm_pid> attach dwm.exe,再对下列四个模块联合搜索 protect 关键字:
!probe -find:protect -mod:dwmcore,dwmredir,udwm,dwm

图 1. !probe -find:protect 命中大量保护相关符号;下文聚焦 CWindowContext。
其中嫌疑最大的是 dwmredir 中与窗口内容相关的两个函数:
CWindowContext::HasProtectedContentCWindowContext::NotifyProtectionChange
命名已经说明用途:判断窗口内容是否处于保护状态,以及在保护状态变化时通知合成管线。DWM 内还有大量 IsProtected / SetHardwareProtection 等符号,值得继续挖掘。
4. HasProtectedContent 在做什么
反汇编 dwmredir!CWindowContext::HasProtectedContent 非常直接:

*图 2. 读取 [rcx+0x114],SHR 6 后 AND 1 — 即检测该 DWORD 的 bit 6(0x40)。*
我们可以大胆的猜测:CWindowContext 对象在偏移 +0x114 处有一个标志字段,bit 6 表示内容是否受保护。
另外我们还可以猜测,每个窗口应该都会对应一个CWindowContext对象。且此对象中应该存在CWindowContext虚表指针。基于此猜测,我们就有了搜索目标。即,基于虚表指针来反推CWindowContext对象的结构。
定位 CWindowContext 实例时,注意该类有两张 vftable。应使用 IDwmWindow 接口对应的那张(本机示例如下):

图 3. !probe -find:CWindowContext,vftable -mod:dwmredir — 定位 IDwmWindow vftable。

图 4. vftable 地址在 dwmredir.dll 内,可用于在 DWM 堆上扫描对象。
5. 内存验证
实验:对某窗口调用 SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE),在 CE 中对 dwm.exe 进程搜索该窗口 HWND 的数值,即可找到对应的 CWindowContext 对象。(搜索过程和验证过程省略)

图 5. 对象布局验证:前 8 字节为 vftable;+0x114 处 0xC7 的 bit6(0x40)为保护位;可交叉验证 HWND 与 owning PID。
| 观察项 | 示例 | 说明 |
|---|---|---|
| vftable | 0x00007FFAF582A000 |
CWindowContext(IDwmWindow)虚表指针 |
| 保护标志 | +0x114,bit 6 = 1 |
与 HasProtectedContent 汇编一致 |
| 窗口 kind | 0x1 / 0x2 / 0x4 |
DWM 内部类型枚举(大体猜测):0x1 控制台类、0x2 GUI 窗口(无边框 / 自定义标题栏)、0x4 GUI 窗口 |
| HWND / PID | — | 窗口的句柄和窗口所属进程 PID |
再次确认:CWindowContext 对象前 8 字节即为 vftable。不同 Windows 版本中具体偏移会有变动,但 vftable + 保护位 + HWND/PID 这一组特征在多版本上相对稳定,可据此设计检测方案。
6. 检测思路(有法有解)
基于以上分析,窗口截屏排除相关的合成逻辑主要在 DWM 进程内完成,与内核路径关系不大,因此扫描 dwm.exe 是较可行的检测面。
可操作的方案与上文 CE 手工验证类似,但把布局发现自动化:
- 从
dwmredir.dll符号(PDB)解析IDwmWindowvtable 地址。 - 扫描
dwm.exe可读写的内存区域,查找等于该 vtable 的指针。 - 校准阶段:对已知窗口开启反截屏 — 鹰眼在初始化时对自身主窗口调用
SetWindowDisplayAffinity— 再按 vtable、保护位 bit 6、窗口 kind、HWND、所属 PID 锁定匹配对象。 - 记录 flags / kind / HWND / PID 相对对象起始的字节偏移,供后续扫描复用。
绝对偏移(例如本机样例中标志 DWORD 在 +0x114)会随 Windows 版本变化;以 vtable 为锚点、校准一次即可保持工具可移植。Community 实现中,扫描时 window kind 仅匹配 0x2 与 0x4,尽管内存 dump 里也可能出现 0x1。
7. 开源实现
本文所述检测思路已在 Hawkeye Community 落地并开源,对应命令 !hidden_windows(需先 -init)与测试命令 !hidden_windows_sim。Community 版以 dwmredir.dll 中 CWindowContext / IDwmWindow 的 vtable 为锚点,经一次校准得到 flags / kind / HWND / PID 的相对偏移后扫描 dwm.exe 内存。
- 源码仓库:3a9K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Z5j5i4N6C8k6i4W2W2i4K6u0V1e0r3g2G2i4K6u0r3K9r3q4%4K9$3g2&6k6g2)9J5k6r3y4G2L8h3#2#2L8X3W2@1P5b7`.`.
- 预编译包:d57K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Z5j5i4N6C8k6i4W2W2i4K6u0V1e0r3g2G2i4K6u0r3K9r3q4%4K9$3g2&6k6g2)9J5k6r3y4G2L8h3#2#2L8X3W2@1P5g2)9J5c8Y4u0W2L8r3g2S2M7$3g2K6i4K6u0r3L8r3q4@1k6i4y4@1
系列下一篇:隐藏Visual — 更加自由和隐蔽的反截屏方案。
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。