窗口隐藏与反截屏系列 · 第 3 期
工具:IDA · CE · Hawkeye Lab · Cursor MCP
目录
- 背景与系列规划
- BitBlt 与 DWM 的联系
- 「显示」与「截屏」
- 分析与突破点
- 实现流程梳理
- Hawkeye Lab
- 开源实现
1. 背景与系列规划
本系列共三篇,本篇为第三期:
- 第 1 期:
SetWindowDisplayAffinity 原理与检测
- 第 2 期:Visual 层隐藏原理与检测
- 第 3 期(本文):基于 BitBlt 的「超级截屏」方案
前两期分别介绍了窗口级与 Visual 层的反截屏原理与检测;本篇离开检测视角,从 BitBlt 截屏路径入手,探讨一种可绕过上述保护的「超级截屏」方案。
2. BitBlt 与 DWM 的联系
简单的流程:
gdi32!BitBlt
└─ win32kfull!NtGdiBitBlt
└─ DwmSyncCaptureSurfaceBits
└─ LpcSendWaitReceivePort → dwm.exe
└─ dwmcore!CaptureBitsResponse::RenderForCapture
└─ 原路返回
和 DisplayAffinity 类似,BitBlt 的调用还是会先进内核,经过一系列的函数调用,不出意外地发送 LPC 消息给 dwm.exe。dwm.exe 接到消息后,最终落到了 dwmcore!CaptureBitsResponse::RenderForCapture。完事后原路返回。
即,BitBlt 的截屏结果不是看内核,而是看 DWM 的心情。更直白点,完全由 dwmcore.dll 来支配。
3. 「显示」与「截屏」
经过分析,「显示」与「截屏」在 RenderTarget 上各走一路,在 Visual 树与合成代码上却是同一套:各有独立的 RenderTarget,却共享同一棵全局 Visual 树与几乎同一套合成逻辑。
正常显示路线:
CDDisplayRenderTarget::RenderAndPresent
→ BeginFrame(显示器 RT)
→ DrawVisualTree
→ Present
截屏路线(RenderForCapture 里):
BeginFrame(captureRenderTarget)
→ DrawVisualTree
→ 从 capturebits scratch 读像素
→ section 回内核
4. 分析与突破点
IDA 分析 CaptureBitsResponse::RenderForCapture,调用顺序大致如下:
CaptureBitsResponse::RenderForCapture
├─ CComposition::ShowHideCursors(0) (Hide 鼠标 Visual;CExcludeVisualReference::Hide)
├─ CDrawingContext::Create (堆上 AllocClear 创建;此时尚未绑定 RT)
└─ 一帧绘制
├─ CDrawingContext::BeginFrame (传入 capture IDeviceTarget,绑定 RT)
├─ CDrawingContext::DrawVisualTree ← 截屏 / 显示共用
└─ CDrawingContext::EndFrame
(结束后 RestoreCursors,并清 g_pComposition+0x196A)
DrawVisualTree 最关键:它把全局 Visual 树里可以画的 Visual 画上去;截屏与显示两条路线都会经过它。
readback 标志:g_pComposition + 0x196A
进入 capture: [g_pComposition+0x196A] ← 1
BeginFrame → DrawVisualTree → EndFrame
离开 capture: [g_pComposition+0x196A] ← 0
在 RenderForCapture 里,capture 开始前会把 1 写入 g_pComposition + 0x196A,IDA 里对应汇编如下:
进入 capture:mov r15d, 1 后写 [g_pComposition+196Ah]。
capture 结束后再写回 0(此处 r13b 为 0),对应汇编如下:
离开 capture:写 [g_pComposition+196Ah] ← 0,随后 RestoreCursors。
整段一帧绘制被这对写 / 清操作包起来,这是最值得怀疑的突破点。 [记得上一期还表扬了微软]
进一步查证
对 196Ah 做 IDA 全局搜索,能追到 readback 相关代码;Win11 的一些版本的 PDB 里还直接带有 IsScreenReadBack()函数:

CComposition::IsScreenReadBack 读取 [rcx+196Ah] 并返回,即检测 readback / 截屏语义标志。
名字里带 ReadBack,似乎就是在回答「当前是不是回写/截屏状态」。可惜 Win10 上往往没有这个符号;本文分析的机器是 Win11,算是运气比较好。
Lab 验证
静态分析到这里还缺实锤:在运行中的 dwm.exe 里,把下图红框处写 1 改成写 0,看普通 BitBlt 是否也能截到受保护窗口。
用 Hawkeye Lab 直接修改 dwm.exe,把 mov r15d, 1 的 immediate 01 改成 00:
Hawkeye Lab 将 mov r15d, 1 改为 mov r15d, 0(immediate 01 → 00)。
改完后再触发普通 BitBlt 截屏,所有被反截屏的窗口和 Visual 都可以截到。
有趣的 bug
正常情况下,这个标志只在截屏的一帧里存活:RenderForCapture 进入绘制前把 g_pComposition + 0x196A 置为 1,帧结束后写回 0。
反过来又试了一下:不动截屏代码,直接在内存里把该字节改成 1,让 DWM 长期处于 readback 语义。桌面上的反截屏窗口会从屏幕上消失。不是变透明,而是显示路径在 DrawVisualTree 里同样会读这一标志,受保护内容被整体跳过。再触发一次截屏、让 capture 正常走完置位和清零,又恢复了正常的逻辑。
5. 实现流程梳理
Lab 里改 immediate 的验证说明方向是对的;真正用起来,需要的是一套可重复、可自动化的流程。思路很直接:在 BitBlt 调用之前,远程改掉写 readback 标志的那条指令,让 capture 期间 g_pComposition + 0x196A 始终为 0;BitBlt 调用完毕后把代码改回去。全程不必注入 dwm.exe,也不必 Hook——只是对dwm.exe进程做极短窗口的代码 patch。
对第 1 期的 DisplayAffinity 与第 2 期的 Visual 隐藏都适用:它们在 capture 路径里正是被 readback 语义挡住的。按下面四步操作即可复现:
- 定位 patch 点。 在
dwmcore.dll 的 RenderForCapture 里找写入 [g_pComposition+196Ah] 的指令。本文 Win11 样例为 mov [rax+196Ah], r15b(图 1,此前由 mov r15d, 1 准备写入值)。不同版本指令编码可能略有差异,宜按模式扫描,不宜硬编码。
- 改写指令。 把「写
1」改成「写 0」。可直接改 store 为 mov [rax+196Ah], 0,或只改 mov r15d, 1 的 immediate(01 → 00)。
- 正常截图。 调用 BitBlt。
- 恢复修改点。 把第 2 步的 patch 原样写回。
6. Hawkeye Lab (给自己打广告)
本系列三篇把对抗面交代清楚了:DisplayAffinity、Visual 隐藏,以及 BitBlt readback 绕过。对游戏反作弊工程师来说,真正吃功夫的通常不是某一个小技巧,而是整条响应链——分析行为、定位外挂代码、快速特征上线-—能不能又快又稳、一轮轮跑通。
Hawkeye Lab 面向反作弊这一场景:把零散排查收成工业化、流程化的固定动作,少了在互不打通的工具之间摸黑;PDF 分析报告直接输出,检测结果与研判维度一次给齐,方便全方位参考和后续跟进。
- Hawkeye Lab 介绍:212K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Z5j5i4N6C8k6i4W2W2i4K6u0V1L8r3g2G2i4K6u0W2k6$3W2@1K9s2g2T1i4K6u0W2K9h3!0Q4x3V1k6Z5j5i4N6C8k6i4W2W2i4K6u0r3L8r3q4T1i4K6u0r3
7. 开源实现
本期代码已经开源。
- 源码仓库:6e7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Z5j5i4N6C8k6i4W2W2i4K6u0V1e0r3g2G2i4K6u0r3K9r3q4%4K9$3g2&6k6g2)9J5k6r3y4G2L8h3#2#2L8X3W2@1P5b7`.`.
- 预编译包:aacK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Z5j5i4N6C8k6i4W2W2i4K6u0V1e0r3g2G2i4K6u0r3K9r3q4%4K9$3g2&6k6g2)9J5k6r3y4G2L8h3#2#2L8X3W2@1P5g2)9J5c8Y4u0W2L8r3g2S2M7$3g2K6i4K6u0r3L8r3q4@1k6i4y4@1