Hawkeye Research

Research · Capture · Part 1

SetWindowDisplayAffinity: mechanism & detection

Hidden windows & anti-capture series Tools: IDA · Cheat Engine · Hawkeye Community

1. TL;DR & scope

SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE) eventually sets bit 6 (0x40) of a DWORD at CWindowContext+0x114 inside dwm.exe, matching dwmredir!CWindowContext::HasProtectedContent (SHR 6; AND 1). The win32k path mostly forwards protection into DWM; a practical defender-side surface is scanning CWindowContext objects (vtable + flag + HWND/PID), not tagWND alone.

Test environment: Windows 11 x64, build 26100 (figures below). The detection approach applies to Windows 10 and Windows 11; absolute field offsets in the figures vary by build, but the vtable + protection bit + HWND/PID pattern is the same. Hawkeye Community resolves the vtable from dwmredir.dll (PDB) and discovers object-relative offsets during !hidden_windows -init — no hard-coded layout in the open-source scanner. Scope: detection and reverse-engineering notes for triage tooling; not a bypass or evasion guide.

2. Background & series

SetWindowDisplayAffinity (typically with WDA_EXCLUDEFROMCAPTURE) is a well-known way to keep a window visible on the desktop while excluding it from most screen-capture and recording APIs.

This series has three parts. This article is part 1:

3. Win32k chain

The user-mode API enters the kernel at win32kfull!NtUserSetWindowDisplayAffinity; core logic lives in SetDisplayAffinity. Simplified flow:

NtUserSetWindowDisplayAffinity
  └─ SetDisplayAffinity
       ├─ ComposeWindowIfNeeded
       ├─ InternalSetProp / InternalRemoveProp("SysDispAffinity")
       └─ ChangeWindowTreeProtection
            └─ ProtectWindowBitmap
                 ├─ ChangeWindowBitmapOwner
                 └─ GreProtectSpriteContent
                      └─ DwmAsyncUpdateSprite → dwm.exe

DwmAsyncUpdateSprite ultimately delivers an LPC message to the dwm.exe process. Aside from updating the SysDispAffinity property on the window (tagWND), other kernel-side changes are relatively few. Property atom indices also shift between Windows versions, so detecting hidden windows from kernel state alone tends to be less stable and less portable. Takeaway: if the durable state lives in DWM, scan dwm.exe rather than betting on win32k-only signatures.

Analysis of dwmcore.dll shows separate paths for capture and direct display, corresponding to CCaptureRenderTarget and CDDisplayRenderTarget, respectively. On closer inspection, anti-capture logic lives almost entirely in dwm.exe; the kernel plays a limited role.

4. DWM investigation

If a window is hidden from capture, that state should show up in DWM’s CCaptureRenderTarget composition path. dwm.exe loads many modules; we started with dwm.exe, dwmcore.dll, dwmredir.dll, and uDWM.dll.

In Hawkeye Community, attach to dwm.exe with !probe -pid:<dwm_pid>, then search for protect across those modules: !probe -find:protect -mod:dwmcore,dwmredir,udwm,dwm

Hawkeye !probe search for protect in DWM modules, listing IsProtected and HasProtectedContent symbols.
Figure 1. !probe -find:protect — many protection-related symbols; below we focus on CWindowContext.

Strongest candidates in dwmredir — two functions tied to window content:

The names are explicit: one tests whether window content is protected; the other notifies the composition pipeline when protection changes. DWM also exposes many IsProtected and SetHardwareProtection symbols worth tracking separately.

5. What HasProtectedContent does

Disassembly of dwmredir!CWindowContext::HasProtectedContent is straightforward:

HasProtectedContent loads dword at object offset 0x114, shifts right 6, ANDs with 1.
Figure 2. Load [rcx+0x114], SHR 6, AND 1 — i.e. test bit 6 (0x40) of that DWORD.

So CWindowContext carries a flag DWORD at offset +0x114; bit 6 marks protected content (consistent with anti-capture semantics).

When locating CWindowContext instances, note the class has two vftables. Use the one for IDwmWindow (example from build 26100 below):

!probe finds two CWindowContext vftables; IDwmWindow at 0x7FFAF582A000.
Figure 3. !probe -find:CWindowContext,vftable -mod:dwmredir — locate the IDwmWindow vftable.
Memory Viewer at CWindowContext IDwmWindow vftable address in dwmredir.dll.
Figure 4. Vftable lives in dwmredir.dll — use it to scan DWM heap objects.
6. Memory validation

Experiment: call SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE) on a window, then search for that HWND value in dwm.exe with Cheat Engine to land on the matching CWindowContext object.

CWindowContext memory: vftable at +0, 0x40 protection bit at +0x114, HWND and PID fields.
Figure 5. Layout check: first 8 bytes = vftable; at +0x114, value 0xC7 has bit 6 (0x40) set; cross-check HWND and owning PID.
Field Example Notes
vftable 0x00007FFAF582A000 CWindowContext (IDwmWindow) vtable pointer
Protection flag +0x114, bit 6 = 1 Matches HasProtectedContent disassembly
Window kind 0x1 / 0x2 / 0x4 DWM internal type enum (rough guess): 0x1 console-class, 0x2 GUI window (frameless / custom title bar), 0x4 GUI window
HWND / PID Window handle and owning process ID

First 8 bytes of the object are the vftable pointer. Exact offsets (such as +0x114 on build 26100) move between Windows builds, but the combination vftable + protection bit + HWND/PID has been stable enough on Windows 10 and 11 to build detection around. Treat figure values as one validated snapshot, not constants to paste into a scanner.

7. Detection

Window composition for capture exclusion lives mainly in the DWM process and is largely decoupled from the kernel path, so scanning dwm.exe is a practical detection surface.

A workable scheme mirrors the manual CE steps above, but automates layout discovery:

Absolute field offsets change between Windows versions and builds. Portability comes from anchoring on the vtable and running a one-time calibration on the target machine. The open-source Community build does this automatically: vtable from dwmredir.dll symbols, relative offsets learned during !hidden_windows -init. Window kind is filtered to 0x2 and 0x4 during scans even though 0x1 may appear in memory dumps.

8. Reference implementation (Hawkeye Community)

The detection approach in this article is implemented in Hawkeye Community: !hidden_windows (run -init first) and the staging command !hidden_windows_sim. Validated on most Windows 10 and 11 systems we tested — not every build or configuration. The Community build resolves the CWindowContext / IDwmWindow vtable from dwmredir.dll at runtime, calibrates relative offsets for flags, kind, HWND, and PID during -init, then scans dwm.exe memory. No fixed +0x114-style constants are baked into the scanner.

Source: github.com/hawkeye-Leo/hawkeye-community
Binaries: GitHub Releases (latest)

Next in the series: Visual-layer hiding — mechanism & detection.

↑ Contents

← Capture series Part 2 →