文章总结: 本文探讨了Windows任务栏透明覆盖窗口的状态同步机制及规避游戏反作弊误杀的方案。因使用SetWindowDisplayAffinity隐藏窗口易被EAC等反作弊引擎判定为外挂,作者提出四种替代策略:使用无重定向表面标志结合DirectComposition渲染规避截图;注入explorer进程内部绘制以消除独立窗口特征;监听截图行为时临时销毁重建窗口;以及让覆盖层视觉融入任务栏不作隐藏。最终结论指出,反作弊主要针对刻意隐藏的覆盖层,若非必须,保持窗口可见是最安全的策略。 综合评分: 89 文章分类: 安全开发,免杀,实战经验,逆向分析
如何避免窗口被游戏反作弊误杀以及替代方案
KsaNL KsaNL
看雪学苑
2026年7月16日 17:59 上海
在小说阅读器读本章
去阅读
同步透明覆盖窗口与任务栏状态
核心问题
任务栏可能:
- 被全屏应用遮挡/隐藏
- 自动隐藏
- 位置/大小变化
- 虚拟桌面切换
方案:监听任务栏状态同步
class TaskbarOverlay {
HWND m_hOverlay = NULL;
HWND m_hTray = NULL;
// 检测任务栏是否可见
bool IsTaskbarVisible() {
if (!m_hTray || !IsWindowVisible(m_hTray))
return false;
// 检查是否被全屏窗口遮挡
APPBARDATA abd = { sizeof(abd) };
UINT state = (UINT)SHAppBarMessage(ABM_GETSTATE, &abd);
// ABS_AUTOHIDE 时可能隐藏了
if (state & ABS_AUTOHIDE) {
RECT rc;
GetWindowRect(m_hTray, &rc);
// 任务栏缩到1像素说明已隐藏
if ((rc.bottom - rc.top) <= 2 || (rc.right - rc.left) <= 2)
return false;
}
// 检查是否有全屏窗口遮挡
return !IsFullscreenAppRunning();
}
bool IsFullscreenAppRunning() {
// 方法1: 用 Shell API
APPBARDATA abd = { sizeof(abd) };
// ABM_GETSTATE 不能直接检测全屏
// 用前台窗口判断
HWND hFg = GetForegroundWindow();
if (!hFg || hFg == m_hTray) return false;
RECT rcWnd, rcMonitor;
GetWindowRect(hFg, &rcWnd);
HMONITOR hMon = MonitorFromWindow(hFg, MONITOR_DEFAULTTONEAREST);
MONITORINFO mi = { sizeof(mi) };
GetMonitorInfoW(hMon, &mi);
rcMonitor = mi.rcMonitor;
// 窗口覆盖整个屏幕 = 全屏
return (rcWnd.left <= rcMonitor.left &&
rcWnd.top <= rcMonitor.top &&
rcWnd.right >= rcMonitor.right &&
rcWnd.bottom >= rcMonitor.bottom);
}
void SyncOverlay() {
if (IsTaskbarVisible()) {
RECT rc;
GetWindowRect(m_hTray, &rc);
SetWindowPos(m_hOverlay, HWND_TOPMOST,
rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top,
SWP_SHOWWINDOW | SWP_NOACTIVATE);
} else {
ShowWindow(m_hOverlay, SW_HIDE);
}
}
};
更好的方案:Hook Shell 事件
注册 Shell Hook 消息
// 注册 Shell Hook —— 收到前台窗口变化等通知
UINTWM_SHELLHOOK = RegisterWindowMessageW(L"SHELLHOOK");
RegisterShellHookWindow(m_hOverlay);
// WndProc 中处理
case WM_SHELLHOOK: // 即上面注册的消息
switch (wParam) {
case HSHELL_RUDEAPPACTIVATED: // 全屏应用激活
ShowWindow(m_hOverlay, SW_HIDE);
break;
case HSHELL_WINDOWACTIVATED: // 普通窗口激活
SyncOverlay();
break;
case HSHELL_REDRAW:
SyncOverlay();
break;
}
break;
监听 ABN_FULLSCREENAPP
// 注册 AppBar 接收全屏通知
APPBARDATA abd = { sizeof(abd) };
abd.hWnd = m_hOverlay;
abd.uCallbackMessage = WM_USER + 100;
SHAppBarMessage(ABM_NEW, &abd);
// WndProc
case WM_USER + 100:
if (wParam == ABN_FULLSCREENAPP) {
if (lParam) {
// 全屏应用进入 —— 隐藏
ShowWindow(m_hOverlay, SW_HIDE);
} else {
// 全屏应用退出 —— 恢复
SyncOverlay();
}
}
break;
最简方案:组合使用
static HWND g_hOverlay;
static HWND g_hTray;
LRESULT CALLBACK OverlayProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
static UINT WM_SHELLHOOK;
switch (msg) {
case WM_CREATE:
WM_SHELLHOOK = RegisterWindowMessageW(L"SHELLHOOK");
RegisterShellHookWindow(hwnd);
// 同时注册 AppBar 以获取全屏通知
{
APPBARDATA abd = { sizeof(abd), hwnd, WM_USER + 1 };
SHAppBarMessage(ABM_NEW, &abd);
}
// 定时器兜底(处理边界情况)
SetTimer(hwnd, 1, 1000, NULL);
break;
case WM_TIMER:
SyncPosition();
break;
case WM_USER + 1: // AppBar callback
if (wp == ABN_FULLSCREENAPP) {
ShowWindow(hwnd, lp ? SW_HIDE : SW_SHOWNOACTIVATE);
if (!lp) SyncPosition();
} else if (wp == ABN_POSCHANGED) {
SyncPosition(); // 任务栏位置变了
}
break;
case WM_DESTROY:
DeregisterShellHookWindow(hwnd);
{
APPBARDATA abd = { sizeof(abd), hwnd };
SHAppBarMessage(ABM_REMOVE, &abd);
}
break;
}
// Shell Hook 消息
if (msg == WM_SHELLHOOK) {
switch (wp) {
case HSHELL_RUDEAPPACTIVATED:
ShowWindow(hwnd, SW_HIDE);
break;
case HSHELL_WINDOWACTIVATED:
SyncPosition();
ShowWindow(hwnd, SW_SHOWNOACTIVATE);
break;
}
return 0;
}
return DefWindowProcW(hwnd, msg, wp, lp);
}
void SyncPosition() {
RECT rc;
GetWindowRect(g_hTray, &rc);
SetWindowPos(g_hOverlay, HWND_TOPMOST,
rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top,
SWP_NOACTIVATE | SWP_SHOWWINDOW);
}
总结
| 事件 | 检测方式 | 响应 |
| — | — | — |
| 全屏游戏进入 | ABN_FULLSCREENAPP / HSHELL_RUDEAPPACTIVATED | 隐藏 overlay |
| 全屏游戏退出 | ABN_FULLSCREENAPP(lp=0) / HSHELL_WINDOWACTIVATED | 显示 + 同步位置 |
| 任务栏移动/调整大小 | ABN_POSCHANGED / Timer 兜底 | 重新定位 |
| 任务栏自动隐藏 | Timer 检查 GetWindowRect | 跟随隐藏 |
ABN_FULLSCREENAPP 是最可靠的全屏检测,Windows Shell 自己就是用这个判断是否隐藏任务栏。
避免被游戏反作弊误杀的替代方案
为什么 SetWindowDisplayAffinity 会被误杀
反作弊引擎(EAC、BattlEye、Vanguard)会:
- 枚举所有窗口,检测
WDA_EXCLUDEFROMCAPTURE - 认为这是作弊覆盖层(隐藏外挂 UI)
- 直接封号或踢出
替代方案
方案 1:不隐藏,而是让窗口”本来就不会被捕获”
利用 WS_EX_NOREDIRECTIONBITMAP:
HWND hOverlay = CreateWindowExW(
WS_EX_NOREDIRECTIONBITMAP | WS_EX_TOPMOST |
WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW,
L"Overlay", NULL,
WS_POPUP | WS_VISIBLE,
x, y, w, h, NULL, NULL, hInst, NULL);
这个标志让窗口没有重定向表面(Redirection Surface):
-
BitBlt/
PrintWindow捕获到的是空的 -
部分截图工具抓不到内容
-
但需要自己通过 DirectComposition 渲染
// 配合 DirectComposition 渲染
IDCompositionDevice* pDCompDevice = nullptr;
DCompositionCreateDevice(nullptr, IID_PPV_ARGS(&pDCompDevice));
IDCompositionTarget* pTarget = nullptr;
pDCompDevice->CreateTargetForHwnd(hOverlay, TRUE, &pTarget);
IDCompositionVisual* pVisual = nullptr;
pDCompDevice->CreateVisual(&pVisual);
pTarget->SetRoot(pVisual);
// 用 DXGI surface 或 D2D 绘制内容到 visual 上
// ...
pDCompDevice->Commit();
缺点: 不是所有截图方式都能规避,Windows.Graphics.Capture 仍可能抓到。
方案 2:根本不用窗口 —— 直接画到任务栏进程内
注入 explorer.exe,在内部创建 DirectComposition visual:
// 注入到 explorer.exe 后
// 获取任务栏的 IDCompositionTarget
// 在现有 visual tree 上添加自己的 visual
// 这样内容属于任务栏本身,不存在独立窗口
优点: 没有独立窗口,反作弊无从检测 缺点: 注入 explorer 本身风险高
方案 3:监听截图行为,临时销毁/重建窗口
不是隐藏,而是真正销毁,截图结束后重建:
// 全局监听
HWINEVENTHOOKhEventHook = SetWinEventHook(
EVENT_SYSTEM_CAPTURESTART, EVENT_SYSTEM_CAPTUREEND,
NULL, WinEventProc, 0, 0, WINEVENT_OUTOFCONTEXT);
// 更实用:监听已知截图进程
void MonitorThread() {
while (running) {
boolscreenshotActive =false;
// 检测常见截图工具进程
// SnippingTool, ScreenClippingHost, ShareX 等
HWNDhSnip = FindWindowW(L"Microsoft-Windows-SnipperToolbar", NULL);
if (hSnip && IsWindowVisible(hSnip))
screenshotActive = true;
// 检测 PrintScreen 状态(剪贴板监听)
static DWORDlastSeq = GetClipboardSequenceNumber();
DWORDcurSeq = GetClipboardSequenceNumber();
if (curSeq != lastSeq) {
lastSeq = curSeq;
// 剪贴板变化,可能是截图
}
if (screenshotActive && g_hOverlay) {
DestroyWindow(g_hOverlay);
g_hOverlay = NULL;
} else if (!screenshotActive && !g_hOverlay) {
g_hOverlay = CreateOverlay();
}
Sleep(50);
}
}
缺点: 无法覆盖所有截图方式,有延迟
方案 4:用普通窗口 + 全透明,截图中自然”看不到”
如果 overlay 只是少量文字/图标,让它截图时无害可见:
// 不做任何隐藏处理
// 让 overlay 在截图中也出现,但伪装成任务栏的一部分
// 视觉上与任务栏融为一体,截图里看到也无所谓
最安全的策略:如果不需要隐藏,就别隐藏。
最佳实践总结
| 场景 | 建议 |
| — | — |
| 不跑游戏/无反作弊 | WDA_EXCLUDEFROMCAPTURE |
| 有反作弊运行 | 不隐藏,让 overlay 与任务栏融为一体 |
| 需要绝对隐藏 | WS_EX_NOREDIRECTIONBITMAP + DComp 渲染 |
| 最安全 | 注入 explorer,不创建独立窗口 |
现实建议: 如果 overlay 只是显示信息(时钟、监控等),让它截图中可见反而最安全。反作弊针对的是刻意隐藏的覆盖层,一个正常可见的窗口不会触发检测。
#
看雪ID:KsaNL
https://bbs.kanxue.com/user-home-969146.htm
*本文为看雪论坛优秀文章,由 KsaNL 原创,转载请注明来自看雪社区
第十届安全开发者峰会【议题征集】-欢迎投稿
往期推荐
一道简单但不简约的堆题——CISCN2026初赛堆题robo分析
第二届软件系统安全赛决赛 StudentManagement 详解
APP整体加固原理
获取SYS加载方源头进程的方案-基于ETW技术
libmsaoaidsec.so 检测监测——绕过 Hook 脚本
球分享
球点赞
球在看
点击阅读原文查看更多
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:看雪学苑 KsaNL KsaNL《如何避免窗口被游戏反作弊误杀以及替代方案》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论