DWMShield — Kernel-Mode Window Capture Exclusion on Windows

A kernel driver PoC that calls win32kfull!GreProtectSpriteContent directly to make any window invisible to every user-mode screen recorder — without going through SetWindowDisplayAffinity and without the constraints that come with it.

1. Starting point

SetWindowDisplayAffinity with WDA_EXCLUDEFROMCAPTURE (value 0x11) hides a window from all user-mode screen capture — BitBlt, PrintWindow, DXGI output duplication, the Windows Graphics Capture API, OBS, all of it. The window stays visible on the physical display; only capture output is affected.

Two things about the documented API are worth noting. First, it only works on windows owned by the calling process. Second, the protection state is readable from any process via GetWindowDisplayAffinity, so a recorder can query it before trying to capture.

I wanted to understand what was actually enforcing the exclusion below the API — whether the mechanism was purely a policy decision in the user-mode graphics stack or something deeper — and whether calling it directly from the kernel would bypass the two constraints above.

The full source is on GitHub: ahossu/DWMShield.

2. How DWM enforces capture exclusion

The Desktop Window Manager (dwm.exe) is a user-mode compositing process — it reads the contents of every window's backing surface and composites them into the final frame. What makes capture exclusion work is that DWM maintains two separate rendering paths: one for the physical display and one for any capture consumer (DXGI output duplication, WGC, etc.). When a window is marked with WDA_EXCLUDEFROMCAPTURE, DWM includes it in the display render pass but skips it in the capture render pass — the window shows on your screen, but the captured frame sees nothing there.

The exclusion flag is not stored by the capture API itself. It lives on the window's sprite object inside win32kfull.sys. DWM reads it through its normal communication with the kernel's window manager when compositing each frame. This is why no user-mode recorder can work around it — the flag is set in the kernel, and DWM enforces it before any user-mode code ever sees the pixel data.

3. Tracing the call chain in win32kfull.sys

SetWindowDisplayAffinity in user32.dll is a thin syscall wrapper. It dispatches to NtUserSetWindowDisplayAffinity in the kernel, where win32kfull.sys handles the actual window-object update. I loaded win32kfull.sys in IDA and traced the call chain down from there.

A few layers in, the chain reaches GreProtectSpriteContent — the internal GDI/DWM function (the Gre prefix denotes the Graphics Engine component inside win32kfull) that applies the capture-exclusion flag to the window's sprite object. It takes an HWND and a DWORD protection value. Passing 0x11 sets WDA_EXCLUDEFROMCAPTURE. The address in a live kernel session:

lkd> x win32kfull!GreProtectSpriteContent
fffff806`12345678 win32kfull!GreProtectSpriteContent

GreProtectSpriteContent is not exported from win32kfull.sys and has no documented prototype. The call chain going through NtUserSetWindowDisplayAffinity also updates a user-queryable affinity property on the window object — the value that GetWindowDisplayAffinity reads. Calling GreProtectSpriteContent directly from a driver skips that step, so the protection is applied without the window's affinity property being updated in the standard location.

4. The driver

The driver creates a device \Device\HideWindow with a symbolic link at \DosDevices\HideWindow, accessible as \\.\HideWindow from user mode. A companion binary, protect.exe, sends a single IOCTL containing the HWND of any target window — it does not need to own it.

The IRP_MJ_DEVICE_CONTROL handler reads the HWND from the input buffer and calls GreProtectSpriteContent at the hardcoded address:

// driver IOCTL handler (simplified)
HWND hwnd = *(HWND *)inputBuffer;

typedef void (*GRE_PROTECT_FN)(HWND, DWORD);
GRE_PROTECT_FN GreProtectSpriteContent =
    (GRE_PROTECT_FN)GRE_PROTECT_SPRITE_CONTENT_ADDR;

GreProtectSpriteContent(hwnd, 0x11);  // 0x11 = WDA_EXCLUDEFROMCAPTURE

After that call, DWM marks the window's sprite as excluded from every capture render pass. OBS, DXGI output duplication, the Windows Graphics Capture API, BitBlt/PrintWindow — all of them get a blank region where the window was. Installing the driver requires test-signing mode and administrator rights:

bcdedit /set testsigning on
Restart-Computer

sc.exe create HideWindow type= kernel binPath= C:\Drivers\HideWindow.sys
sc.exe start HideWindow

protect.exe   # sends IOCTL with the target HWND

protect.exe can be compiled with any C compiler. It doesn't need to run as Administrator — only the driver load step does.

5. KASLR

Because GreProtectSpriteContent is not exported, there's no import table entry or documented symbol to resolve it by. KASLR randomises the base address of win32kfull.sys on every boot, so the address of the function changes accordingly. For this PoC the address is hardcoded as a #define and must be updated after each reboot. The current value for any given boot:

lkd> x win32kfull!GreProtectSpriteContent

A more robust implementation would scan the loaded win32kfull image at driver load time for a known byte pattern to locate the function dynamically, without rebuilding. That's outside the scope of what this PoC was built to answer.

6. Result

Calling GreProtectSpriteContent directly from a kernel driver produces the same DWM capture-exclusion behavior as the documented SetWindowDisplayAffinity path — on any window, regardless of which process owns it. The window stays visible on the physical display and disappears from every user-mode capture surface.

Because the driver bypasses the NtUserSetWindowDisplayAffinity path, the window's user-queryable affinity property is not updated. A recorder calling GetWindowDisplayAffinity on the target window gets back WDA_NONE rather than WDA_EXCLUDEFROMCAPTURE — even though the exclusion is fully in effect at the DWM compositor level.

The exclusion is enforced by DWM when compositing to a capture surface, not by the capture API itself. Any recording tool operating entirely in user mode sees only what DWM puts into the captured frame — and DWM has already removed the protected window before that point.

7. Limitations

  • KASLR. The address of GreProtectSpriteContent changes on every reboot and must be updated manually. The function's byte signature can also change across Windows updates.
  • Windows 10 / 11 only. The function was located and tested on build 19041 (Windows 10 2004). Its presence and signature are not guaranteed on future builds.
  • No HWND validation. The driver passes the HWND directly to the internal function without any sanity check. A malformed or stale handle passed from user mode could cause unpredictable behaviour in the kernel.
  • Test signing required. The driver is test-signed and requires bcdedit /set testsigning on. Deploying on a system with Secure Boot and driver signature enforcement would need a valid EV certificate.