如何避免窗口被游戏反作弊误杀以及替代方案

admin 2026-07-17 04:33:36 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文探讨了Windows任务栏透明覆盖窗口的状态同步机制及规避游戏反作弊误杀的方案。因使用SetWindowDisplayAffinity隐藏窗口易被EAC等反作弊引擎判定为外挂,作者提出四种替代策略:使用无重定向表面标志结合DirectComposition渲染规避截图;注入explorer进程内部绘制以消除独立窗口特征;监听截图行为时临时销毁重建窗口;以及让覆盖层视觉融入任务栏不作隐藏。最终结论指出,反作弊主要针对刻意隐藏的覆盖层,若非必须,保持窗口可见是最安全的策略。 综合评分: 89 文章分类: 安全开发,免杀,实战经验,逆向分析


cover_image

如何避免窗口被游戏反作弊误杀以及替代方案

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&nbsp;((rc.bottom - rc.top) <=&nbsp;2&nbsp;|| (rc.right - rc.left) <=&nbsp;2)
return&nbsp;false;
&nbsp; &nbsp; &nbsp; &nbsp; }

// 检查是否有全屏窗口遮挡
return&nbsp;!IsFullscreenAppRunning();
&nbsp; &nbsp; }

bool&nbsp;IsFullscreenAppRunning()&nbsp;{
// 方法1: 用 Shell API
&nbsp; &nbsp; &nbsp; &nbsp; APPBARDATA abd = {&nbsp;sizeof(abd) };
// ABM_GETSTATE 不能直接检测全屏
// 用前台窗口判断
&nbsp; &nbsp; &nbsp; &nbsp; HWND hFg = GetForegroundWindow();
if&nbsp;(!hFg || hFg == m_hTray)&nbsp;return&nbsp;false;

&nbsp; &nbsp; &nbsp; &nbsp; RECT rcWnd, rcMonitor;
&nbsp; &nbsp; &nbsp; &nbsp; GetWindowRect(hFg, &rcWnd);

&nbsp; &nbsp; &nbsp; &nbsp; HMONITOR hMon = MonitorFromWindow(hFg, MONITOR_DEFAULTTONEAREST);
&nbsp; &nbsp; &nbsp; &nbsp; MONITORINFO mi = {&nbsp;sizeof(mi) };
&nbsp; &nbsp; &nbsp; &nbsp; GetMonitorInfoW(hMon, &mi);
&nbsp; &nbsp; &nbsp; &nbsp; rcMonitor = mi.rcMonitor;

// 窗口覆盖整个屏幕 = 全屏
return&nbsp;(rcWnd.left <= rcMonitor.left &&
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; rcWnd.top <= rcMonitor.top &&
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; rcWnd.right >= rcMonitor.right &&
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; rcWnd.bottom >= rcMonitor.bottom);
&nbsp; &nbsp; }

void&nbsp;SyncOverlay()&nbsp;{
if&nbsp;(IsTaskbarVisible()) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; RECT rc;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; GetWindowRect(m_hTray, &rc);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; SetWindowPos(m_hOverlay, HWND_TOPMOST,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; SWP_SHOWWINDOW | SWP_NOACTIVATE);
&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ShowWindow(m_hOverlay, SW_HIDE);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
};

更好的方案:Hook Shell 事件

注册 Shell Hook 消息

// 注册 Shell Hook —— 收到前台窗口变化等通知
UINTWM_SHELLHOOK&nbsp;=&nbsp;RegisterWindowMessageW(L"SHELLHOOK");
RegisterShellHookWindow(m_hOverlay);

// WndProc 中处理
case&nbsp;WM_SHELLHOOK:&nbsp;// 即上面注册的消息
switch&nbsp;(wParam) {
case&nbsp;HSHELL_RUDEAPPACTIVATED: &nbsp;// 全屏应用激活
&nbsp; &nbsp; &nbsp; &nbsp; ShowWindow(m_hOverlay, SW_HIDE);
break;
case&nbsp;HSHELL_WINDOWACTIVATED: &nbsp;&nbsp;// 普通窗口激活
&nbsp; &nbsp; &nbsp; &nbsp; SyncOverlay();
break;
case&nbsp;HSHELL_REDRAW:
&nbsp; &nbsp; &nbsp; &nbsp; SyncOverlay();
break;
&nbsp; &nbsp; }
break;

监听 ABN_FULLSCREENAPP

// 注册 AppBar 接收全屏通知
APPBARDATA abd = {&nbsp;sizeof(abd) };
abd.hWnd = m_hOverlay;
abd.uCallbackMessage = WM_USER +&nbsp;100;
SHAppBarMessage(ABM_NEW, &abd);

// WndProc
case&nbsp;WM_USER +&nbsp;100:
if&nbsp;(wParam == ABN_FULLSCREENAPP) {
if&nbsp;(lParam) {
// 全屏应用进入 —— 隐藏
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ShowWindow(m_hOverlay, SW_HIDE);
&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;{
// 全屏应用退出 —— 恢复
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; SyncOverlay();
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
break;

最简方案:组合使用

static&nbsp;HWND g_hOverlay;
static&nbsp;HWND g_hTray;

LRESULT&nbsp;CALLBACK&nbsp;OverlayProc(HWND hwnd,&nbsp;UINT&nbsp;msg, WPARAM wp, LPARAM lp) {
static&nbsp;UINT&nbsp;WM_SHELLHOOK;

switch&nbsp;(msg) {
case&nbsp;WM_CREATE:
&nbsp; &nbsp; &nbsp; &nbsp; WM_SHELLHOOK = RegisterWindowMessageW(L"SHELLHOOK");
&nbsp; &nbsp; &nbsp; &nbsp; RegisterShellHookWindow(hwnd);

// 同时注册 AppBar 以获取全屏通知
&nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; APPBARDATA abd = {&nbsp;sizeof(abd), hwnd, WM_USER +&nbsp;1&nbsp;};
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; SHAppBarMessage(ABM_NEW, &abd);
&nbsp; &nbsp; &nbsp; &nbsp; }
// 定时器兜底(处理边界情况)
&nbsp; &nbsp; &nbsp; &nbsp; SetTimer(hwnd,&nbsp;1,&nbsp;1000,&nbsp;NULL);
break;

case&nbsp;WM_TIMER:
&nbsp; &nbsp; &nbsp; &nbsp; SyncPosition();
break;

case&nbsp;WM_USER +&nbsp;1:&nbsp;// AppBar callback
if&nbsp;(wp == ABN_FULLSCREENAPP) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ShowWindow(hwnd, lp ? SW_HIDE : SW_SHOWNOACTIVATE);
if&nbsp;(!lp) SyncPosition();
&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;if&nbsp;(wp == ABN_POSCHANGED) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; SyncPosition();&nbsp;// 任务栏位置变了
&nbsp; &nbsp; &nbsp; &nbsp; }
break;

case&nbsp;WM_DESTROY:
&nbsp; &nbsp; &nbsp; &nbsp; DeregisterShellHookWindow(hwnd);
&nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; APPBARDATA abd = {&nbsp;sizeof(abd), hwnd };
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; SHAppBarMessage(ABM_REMOVE, &abd);
&nbsp; &nbsp; &nbsp; &nbsp; }
break;
&nbsp; &nbsp; }

// Shell Hook 消息
if&nbsp;(msg == WM_SHELLHOOK) {
switch&nbsp;(wp) {
case&nbsp;HSHELL_RUDEAPPACTIVATED:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ShowWindow(hwnd, SW_HIDE);
break;
case&nbsp;HSHELL_WINDOWACTIVATED:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; SyncPosition();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ShowWindow(hwnd, SW_SHOWNOACTIVATE);
break;
&nbsp; &nbsp; &nbsp; &nbsp; }
return&nbsp;0;
&nbsp; &nbsp; }

return&nbsp;DefWindowProcW(hwnd, msg, wp, lp);
}

void&nbsp;SyncPosition() {
&nbsp; &nbsp; RECT rc;
&nbsp; &nbsp; GetWindowRect(g_hTray, &rc);
&nbsp; &nbsp; SetWindowPos(g_hOverlay, HWND_TOPMOST,
&nbsp; &nbsp; &nbsp; &nbsp; rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top,
&nbsp; &nbsp; &nbsp; &nbsp; SWP_NOACTIVATE | SWP_SHOWWINDOW);
}

总结

| 事件 | 检测方式 | 响应 | | — | — | — | | 全屏游戏进入 | ABN_FULLSCREENAPPHSHELL_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 =&nbsp;CreateWindowExW(
&nbsp; &nbsp; WS_EX_NOREDIRECTIONBITMAP | WS_EX_TOPMOST |
&nbsp; &nbsp; WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW,
L"Overlay",&nbsp;NULL,
&nbsp; &nbsp; WS_POPUP | WS_VISIBLE,
&nbsp; &nbsp; x, y, w, h,&nbsp;NULL,&nbsp;NULL, hInst,&nbsp;NULL);

这个标志让窗口没有重定向表面(Redirection Surface)

  • BitBlt

    /PrintWindow 捕获到的是空的

  • 部分截图工具抓不到内容

  • 但需要自己通过 DirectComposition 渲染

// 配合 DirectComposition 渲染
IDCompositionDevice* pDCompDevice =&nbsp;nullptr;
DCompositionCreateDevice(nullptr,&nbsp;IID_PPV_ARGS(&pDCompDevice));

IDCompositionTarget* pTarget =&nbsp;nullptr;
pDCompDevice->CreateTargetForHwnd(hOverlay, TRUE, &pTarget);

IDCompositionVisual* pVisual =&nbsp;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&nbsp;=&nbsp;SetWinEventHook(
&nbsp; &nbsp; EVENT_SYSTEM_CAPTURESTART, EVENT_SYSTEM_CAPTUREEND,
&nbsp; &nbsp; NULL, WinEventProc,&nbsp;0,&nbsp;0, WINEVENT_OUTOFCONTEXT);

// 更实用:监听已知截图进程
void&nbsp;MonitorThread()&nbsp;{
while&nbsp;(running) {
boolscreenshotActive&nbsp;=false;

// 检测常见截图工具进程
// SnippingTool, ScreenClippingHost, ShareX 等
HWNDhSnip&nbsp;=&nbsp;FindWindowW(L"Microsoft-Windows-SnipperToolbar", NULL);
if&nbsp;(hSnip && IsWindowVisible(hSnip))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; screenshotActive =&nbsp;true;

// 检测 PrintScreen 状态(剪贴板监听)
static&nbsp;DWORDlastSeq&nbsp;=&nbsp;GetClipboardSequenceNumber();
DWORDcurSeq&nbsp;=&nbsp;GetClipboardSequenceNumber();
if&nbsp;(curSeq != lastSeq) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; lastSeq = curSeq;
// 剪贴板变化,可能是截图
&nbsp; &nbsp; &nbsp; &nbsp; }

if&nbsp;(screenshotActive && g_hOverlay) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; DestroyWindow(g_hOverlay);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; g_hOverlay = NULL;
&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;if&nbsp;(!screenshotActive && !g_hOverlay) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; g_hOverlay = CreateOverlay();
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; Sleep(50);
&nbsp; &nbsp; }
}

缺点: 无法覆盖所有截图方式,有延迟

方案 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《如何避免窗口被游戏反作弊误杀以及替代方案》

评论:0   参与:  0