真正单文件命令行工具:通过 shellcode 注入(无 DLL、无外部文件),对任意窗口设置 / 查询 「截图亲和力」(Window Display Affinity),控制该窗口在截图 / 录屏中的表现:
| 模式 | 取值 | 效果 |
|---|---|---|
none |
WDA_NONE (0x0) |
不限制,截图正常 |
monitor |
WDA_MONITOR (0x1) |
截图里该窗口区域变黑 |
exclude |
WDA_EXCLUDEFROMCAPTURE (0x11) |
截图 / 录屏里该窗口整个消失(Win10 2004+) |
窗口在屏幕上始终正常显示,只影响「被其它进程捕获」的结果。
wda-inject list [--hidden] [--pid N] [--exe 名称] [--title 文本]
wda-inject set <target> <none|monitor|exclude> [--all]
wda-inject get <target> [--all]
target:
| 写法 | 含义 |
|---|---|
0x00A01234 |
窗口句柄(list 输出的第一列) |
pid:4532 |
该进程的顶级窗口 |
exe:notepad.exe |
按进程名(不区分大小写,子串匹配,exe:notepad 也能匹配到) |
title:记事本 |
标题子串(不区分大小写) |
示例:
wda-inject list --exe chrome :: 列出 Chrome 的顶层窗口
wda-inject set exe:notepad.exe exclude :: 记事本从截图/录屏中消失
wda-inject set title:记事本 monitor :: 标题含「记事本」的窗口截图变黑
wda-inject set 0x00000000000A01234 none :: 按句柄恢复
wda-inject get pid:4532 :: 查询该进程窗口当前的亲和力
wda-inject set exe:chrome.exe exclude --all :: 匹配到的所有窗口全部设置退出码:0 全部成功;1 参数错误;2 没匹配到窗口;3 部分/全部失败。
微软官方文档对 SetWindowDisplayAffinity 的要求:
"A handle to the top-level window. The window must belong to the current process."
—— 窗口必须属于调用进程。所以必须在目标进程自己的上下文里调用该 API。
v2.0 的注入方式是 shellcode 注入,把原来的「注入 DLL」升级成「注入机器码」, 因此 EXE 里不需要打包任何 DLL:
- 解析地址:在注入器进程内
GetProcAddress拿到SetWindowDisplayAffinity/GetWindowDisplayAffinity/GetLastError的地址。 Windows 的 ASLR 是系统级的——同一会话里user32.dll/kernel32.dll在所有进程中的加载基址一致,所以这些地址在目标进程里同样有效。 - 写入参数:
VirtualAllocEx在目标进程分配参数/结果区(RW),WriteProcessMemory写入{函数地址, 窗口句柄, 亲和力, 结果槽指针, GetLastError地址, 错误码槽}。 - 写入载荷:再分配一块 RWX 内存,写入内嵌在 EXE 里的 shellcode 字节
(x64: 60 字节;x86: 44 字节,见
src/shellcode_*.S,已汇编成src/sc*.inc内嵌)。 - 执行:
CreateRemoteThread让目标进程运行 shellcode——由目标进程上下文调用Set/GetWindowDisplayAffinity,把返回值、错误码写回结果区。 - 收尾:等待远程线程结束 →
ReadProcessMemory读结果 → 释放内存、关闭句柄。
相比 DLL 注入:没有 DLL 文件、没有临时文件落盘、不会被杀软隔离或留下文件; 「进程内调用 API」的语义完全相同。
关于 32 位目标:x64 注入器遇到 32 位(WOW64)进程时,需要 32 位上下文来解析
32 位地址并注入 32 位 shellcode。v2.3 起 32 位子程序已经内嵌在 wda-inject.exe 里:
运行时自动释放到 %TEMP%、执行、用完即删——你只需要一个 wda-inject.exe,
无论目标进程是 32 位还是 64 位都能处理。
get 默认走进程内 shellcode 查询(最准确);set/get 都可以加 --direct
强制直接跨进程调用(注入不可用时的兜底,官方文档不保证跨进程 set 成功)。
list 里的「亲和力」列是跨进程快速查询的结果,exclude 状态在跨进程查询时
可能显示为 monitor(隐私保护设计),以 get 的进程内查询为准。
需要 VS2019/VS2022(含 C++ 桌面开发或 Build Tools)。在项目目录执行:
build.batshellcode 已预生成成 C 字节数组(src/sc64.inc、src/sc86.inc)随源码提交,
MSVC 直接编译,不需要汇编器。
make # 构建 wda-inject.exe(64) + wda-inject32.exe + testwin.exe
make testwin.exe修改了 src/shellcode_*.S 后需要重新生成字节数组(需要 mingw 的 as):
./tools/gen_shellcode.shwda-inject.exe x64 主程序(平时就用它;已内嵌 32 位子程序,处理 32/64 位目标都行)
wda-inject64.exe x64 主程序(同上)
wda-inject32.exe x86 主程序(独立版,仅调试用;正常使用不需要)
testwin.exe 测试窗口工具(见下)
依赖仅系统自带的 KERNEL32 / USER32 / msvcrt,任何 Windows 10/11 直接可用。 建议在目标机器上用 MSVC 重新构建以排除交叉编译差异。
testwin.exe 60 :: 弹出一个标题为 wda-test-window 的窗口,60 秒后退出
testwin.exe cjk :: 弹出一个中文长标题窗口(验证中文控制台渲染/截断)
wda-inject get title:wda-test-window
wda-inject set title:wda-test-window exclude
:: 用 Win+Shift+S 截图:应该看不到这个窗口
wda-inject get title:wda-test-window
wda-inject set title:wda-test-window none :: 恢复exclude需要 Windows 10 2004 (build 19041) 及以上;更老的系统上会自动按monitor处理(官方文档行为)。- 依赖 DWM 桌面合成(默认开启)。远程桌面 / 无 GPU 直通的虚拟机里可能不生效。
- 目标进程权限高于当前进程(如管理员进程)时,注入会被拒绝 —— 以管理员身份运行
wda-inject。 - 受保护进程(部分系统进程、DRM 媒体进程等)无法打开/注入。
WDA_EXCLUDEFROMCAPTURE只对顶级窗口生效(官方文档)。- 主流截图/录屏(Win+Shift+S、OBS、Xbox Game Bar、QQ/微信截图等)都会遵守 WDA; 个别绕过 DWM 的 GDI 抓屏老软件可能仍然能抓到。
- 部分自带防截屏的应用(播放器、网银控件)本身已设置
WDA_MONITOR,set ... none可解除(若权限允许)。 GetWindowDisplayAffinity官方文档说明仅对「分层窗口 + DWM 合成」保证成功, 个别窗口在list里亲和力可能显示?,属正常。- 不要并发对同一个进程跑两个
set/get。 - 本工具不是安全/DRM 功能:
WDA只约束操作系统公开的捕获 API,拦不住拍照、直接读显存等。 - 注入载荷是 shellcode(
CreateRemoteThread执行机器码),部分安全软件可能对此告警; 请仅用于你自己的系统与程序。
Q: 为什么 set 报「拒绝访问」? 目标进程比 wda-inject 权限高(如管理员/系统进程)。右键「以管理员身份运行」命令行再试。
Q: 为什么 exclude 设了之后窗口还在截图里?
确认系统是 Win10 2004+;确认不是远程桌面会话;确认目标窗口是顶级窗口;
个别应用会周期性地把亲和力改回去,可配合 --all 循环设置。
Q: 32 位进程注入不了?
v2.3 起的 wda-inject.exe 已内嵌 32 位子程序,无需任何额外文件即可处理 32 位目标。
若仍失败,运行 wda-inject set <target> <mode> --verbose 查看:应能看到
「已释放 32 位载荷到 …」的调试输出,随后子进程执行注入。
Q: 设置之后如何恢复?
wda-inject set <target> none。
Q: set 报「错误码 87 参数错误」?
先运行 wda-inject set <target> <mode> --verbose:
- 若显示
注入失败(阶段: OpenProcess)—— 请升级到 v2.2.0。v2.2 修复了一个关键 bug: 之前把GetWindowThreadProcessId的**返回值(线程 ID)**误当成进程 ID, 导致OpenProcess拿线程 ID 当 PID 打开而报 87。v2.2 起 list 显示的 PID 为真实进程 ID。 - 若显示
目标进程内调用 ... 返回失败,则按下面排查:- 传入了子窗口句柄(如从 Spy++ 拿到的控件句柄)——工具会自动检测并改用其
顶层窗口,并打印「已自动改用其顶层窗口」提示;也可先
wda-inject list --pid <pid>查看该进程的顶级窗口。 - 系统版本太旧:
exclude需要 Win10 2004 (build 19041)+。set exclude 时工具会 自动检查系统版本并告警;可先用wda-inject set <target> monitor试试—— 若 monitor 成功而 exclude 失败,基本就是系统版本问题。 - 个别特殊窗口(无边框/半透明/自定义框架,如部分 WinUI 应用)可能拒绝设置 参考。
- 传入了子窗口句柄(如从 Spy++ 拿到的控件句柄)——工具会自动检测并改用其
顶层窗口,并打印「已自动改用其顶层窗口」提示;也可先
Q: list 输出中文乱码 / 列对不齐?
输出到控制台用 WriteConsoleW 原生宽字符输出,不依赖代码页(chcp 936/65001 都正常),
列宽按「显示宽度」计算(中文按 2 格),长标题自动截断。若仍显示方块,把 cmd 窗口字体
设置为「新宋体」等含中文字形的字体;输出重定向到文件时文件为 UTF-8 编码。
Q: list 里「进程」列是空的?
v2.2.1 起已修复:此前进程名缓冲区只有 64 字符,而现代记事本等打包应用的路径
(C:\Program Files\WindowsApps\...)很长,会导致 QueryFullProcessImageNameW
缓冲区不足而失败。现在缓冲区扩到 MAX_PATH 并回退到 Toolhelp 快照取进程名,
即使 OpenProcess 因权限被拒也能显示。
wda-inject/
├── README.md
├── build.bat MSVC 一键构建(先 32 位后 64 位,自动生成内嵌数据)
├── Makefile MinGW/MSYS2 构建
├── tools/
│ ├── gen_shellcode.sh shellcode 汇编 -> C 字节数组
│ ├── gen_embed.py wda-inject32.exe -> C 字节数组(Linux/CI)
│ ├── gen_embed.ps1 wda-inject32.exe -> C 字节数组(Windows/MSVC)
│ └── testwin.c 测试窗口工具
├── src/
│ ├── wda_inject.c 主程序(CLI + 注入器 + 32位载荷释放逻辑)
│ ├── wda_common.h 共享常量
│ ├── shellcode_x64.S x64 注入载荷汇编源(60B)
│ ├── shellcode_x86.S x86 注入载荷汇编源(44B)
│ ├── sc64.inc / sc86.inc shellcode 字节数组
│ └── wda32_exe.inc 32 位子程序的字节数组(构建时生成)
└── *.exe 构建产物(单文件)