github.com
SignGuiPatcher β Windows Watermark Remover

π Table of Contents
- The Problem
- Debugging Session
- Step 1 β Locating the watermark renderer
- Step 2 β Disassembling s_DesktopBuildPaint on Build 28000
- Step 3 β Finding DrawTextWithGlow as the actual renderer
- Step 4 β Identifying the import mechanism
- Step 5 β Verifying shell32 resource IDs on Build 28000
- Step 6 β Defense-in-depth: five hooks
- How It Works
- Usage
- Requirements
- Building from Source
- Compatibility
- Disclaimer
The Problem
Windows displays desktop watermarks in the bottom-right corner of the screen in several scenarios:
- Test Signing Mode β
bcdedit /set testsigning on(unsigned drivers) - Evaluation builds β trial/demo editions of Windows
- Insider / pre-release builds β build number + branch strings
- Activation strings β "Activate Windows" reminders on unlicensed installs
All watermarks are rendered by shell32!CDesktopWatermark::s_DesktopBuildPaint, a single function called during desktop composition. On older builds (VistaβWin10) it uses GDI ExtTextOutW; on modern builds (Win11 26H1 / Build 28000+) it routes through UxTheme!DrawTextWithGlow (ordinal 126) for the glow effect. The tool intercepts every known rendering path.
Debugging Session
Step 1 β Locating the watermark renderer in shell32.dll
Symbols were loaded and the desktop watermark class was identified:
x shell32!CDesktopWatermark::*
Key methods:
shell32!CDesktopWatermark::s_DesktopBuildPaint
shell32!CDesktopWatermark::s_IsTestSigningEnabled
shell32!CDesktopWatermark::s_GetTestModeString
shell32!CDesktopWatermark::s_GetProductBuildString
s_DesktopBuildPaint is the root painter β it assembles all strings and calls the render API. Breaking on it confirmed it fires on every desktop repaint containing a watermark.
Step 2 β Disassembling s_DesktopBuildPaint on Build 28000
The function opens with a call to BrandingLoadStringForEdition (from winbrand.dll):
call qword ptr [shell32+0x753C48] ; BrandingLoadStringForEdition (delay IAT slot)
test eax, eax
je +0x9ED ; if result == 0 β skip edition string
Initial hypothesis: returning 0 from BrandingLoadStringForEdition would cause an early exit at je +0x9ED and bypass all rendering.
This was wrong. Tracing the jump target revealed it does NOT skip rendering β it only zeroes the edition string buffer and falls through to s_GetProductBuildString at +0x102, which assembles the build number + branch string and proceeds to DrawTextWithGlow.
"Windows 11 Pro" disappeared from the watermark, but "Test Mode" and "Build 28000β¦" remained.
Step 3 β Finding DrawTextWithGlow as the actual renderer
WinDbg was used to inspect the shell32 delay-load import table for UxTheme:
dq SHELL32+753AB0 L1 ; IAT slot β holds current function pointer
ln poi(SHELL32+753AB0) ; resolve symbol
Result: UxTheme!DrawTextWithGlow β ordinal 126.
Every watermark string (Test Mode, build number, activation text) is rendered through this single call. ExtTextOutW and DrawTextW are also present as fallback paths in older code, but on Build 28000 DrawTextWithGlow is the only active renderer.
Step 4 β Identifying the import mechanism (delay-load by ordinal)
DrawTextWithGlow is delay-loaded from UxTheme β its IAT slot holds a thunk stub until the function is first called. A standard value-scan would fail at DllMain time because UxTheme may not yet be resolved. The import name table (INT) must be scanned directly.
Scanning the INT revealed the entry for DrawTextWithGlow has bit 63 set β it is an ordinal-only import (no name string), ordinal 126 in bits 15:0:
INT entry: 0x800000000000007E β bit63=1 (ordinal), ordinal = 0x7E = 126
BrandingLoadStringForEdition from winbrand.dll is similarly delay-loaded, but imported by name (bit 63 clear); the INT entry points to IMAGE_IMPORT_BY_NAME with the ASCII name at RVA + 2.
Step 5 β Verifying shell32 resource IDs on Build 28000
An earlier diagnosis suggested shell32 resource IDs 33088β33123 (activation watermark strings) had been removed from Build 28000. PowerShell verification disproved this:
Add-Type -TypeDefinition '...'
[Win32]::LoadString([Win32]::LoadLibraryEx("shell32.dll", 0, 2), 33088)
# β "Test Mode"
Resources are present. The LoadStringW hook that blocks IDs 62000/62001 (evaluation strings) and learns IDs 33088β33123 (live watermark patterns) is still necessary for full coverage on older builds where the GDI/USER32 paths are active.
Step 6 β Defense-in-depth: five hooks
Each hook targets a different rendering path that has been active across different Windows versions:
| Hook | Source DLL | Import type | Builds |
|---|---|---|---|
LoadStringW |
api-ms-win-core-libraryloader | regular IAT | Vista+ |
ExtTextOutW |
gdi32 | regular IAT | VistaβWin10 |
DrawTextW |
user32 | regular IAT | some Win10 |
BrandingLoadStringForEdition |
winbrand | delay IAT by name | Win8+ |
DrawTextWithGlow |
UxTheme ord 126 | delay IAT by ordinal | Win11 Build 28000 |
On Build 28000, DrawTextWithGlow alone is sufficient. On older builds the first three hooks carry the load. All five are installed together for full compatibility.
How It Works
COM CLSID Hijack β the capital-I trick
Windows Explorer loads ExplorerFrame.dll via COM:
HKCR\CLSID\{ab0b37ec-56f6-4a0e-a8fd-7a8bf7c2da96}\InProcServer32
(Default) = %SystemRoot%\system32\ExplorerFrame.dll
The patcher changes one character of the registry value to:
(Default) = %SystemRoot%\system32\ExpIorerFrame.dll
^
capital I (U+0049) β visually identical to lowercase l (U+006C)
Windows loads ExpIorerFrame.dll (our proxy) instead of the real ExplorerFrame.dll. The filename is visually indistinguishable in Explorer and most file listing tools.
Proxy DLL β ExplorerFrame (ExpIorerFrame.dll)
The proxy DLL forwards all ExplorerFrame.dll exports via a .def file with ExplorerFrame.dll as the forwarding target, so Explorer's functionality is completely preserved. At DllMain attach, PatchShell32Imports is called to install all five IAT hooks.
IAT patching
Regular imports (LoadStringW, ExtTextOutW, DrawTextW):
- Walk
IMAGE_NT_HEADERS64 β DataDirectory[1](import directory) to find the DLL descriptor - Scan
FirstThunkfor the current function address VirtualProtect(PAGE_EXECUTE_READWRITE)β overwrite slot β restore old protection
Delay imports (BrandingLoadStringForEdition, DrawTextWithGlow):
- Walk
DataDirectory[13](delay-load directory) to find theImgDelayDescrfor the DLL - Walk the INT (Import Name Table at
rvaINT):- By name: bit 63 = 0 β lower 32 bits = RVA of
IMAGE_IMPORT_BY_NAME, name at+2 - By ordinal: bit 63 = 1 β bits 15:0 = ordinal number
- By name: bit 63 = 0 β lower 32 bits = RVA of
- Patch the corresponding IAT slot at the same index in
rvaIAT
Scanning the INT works regardless of whether the DLL is loaded β the name/ordinal data is always present in the PE image, unlike the IAT slots which hold thunk stubs until first call.
Hook implementations
InterceptedLoadStringW β blocks resource IDs 62000 (0xF230) and 62001 (0xF231) (evaluation/watermark string table entries). All other IDs are forwarded. As a side effect, when shell32 loads resource IDs 33088β33123 (live activation strings), the hook copies each string into g_brandingPatterns[] so the text-level hooks have accurate patterns.
InterceptedExtTextOutW / InterceptedDrawTextW β call ContainsBrandingWatermark() on the text argument. If any loaded pattern matches (substring search via WideStrFind), the call is suppressed (returns TRUE / 0 without drawing). Otherwise tail-called to the original function preserving all stack arguments.
InterceptedBrandingLoadStringForEdition β zeroes the output buffer and returns 0. This removes the edition string from the watermark on builds that display it.
InterceptedDrawTextWithGlow β returns S_OK (0) immediately. One-instruction leaf function. Suppresses all glow-rendered watermark text on Win11 Build 28000+.
TrustedInstaller deployment
Writing to System32 and modifying HKCR\CLSID requires TrustedInstaller privileges. The patcher:
- Opens the
TrustedInstallerservice and retrieves its process token - Duplicates the token as an impersonation token
- Calls
ImpersonateLoggedOnUserto assume TI identity - Extracts
ExpIorerFrame.dllfrom the embedded CAB resource (viaFDI) - Writes the DLL to
%SystemRoot%\system32\ExpIorerFrame.dll - Updates the registry value
- Calls
RevertToSelfto drop TI impersonation - Restarts Explorer (
TerminateProcessβWaitForMultipleObjects(500ms)βShellExecuteExW)
Restore reverses steps 4β6: registry value is reset to the original, DLL is deleted (MoveFileExW(MOVEFILE_DELAY_UNTIL_REBOOT) if locked), Explorer is restarted.
Usage
GUI mode
Run SignGuiPatcher.exe as Administrator (UAC manifest embedded β elevation prompt appears automatically).
- APPLY PATCH β deploys DLL, patches registry, restarts Explorer
- RESTORE β reverts registry, removes DLL, restarts Explorer
- Status indicator shows current state in real time (green / red / orange during transition)
CLI mode
SignGuiPatcher.exe [switch]
(no switch) GUI mode
-apply Apply watermark patch
-restore Remove watermark patch
-status Query patch status
/? -h -help This help
Example β scripted deploy:
Start-Process SignGuiPatcher.exe -ArgumentList '-apply' -Verb RunAs -Wait
Status exit codes: 0 = success, non-zero = failure (check console output).
Requirements
| OS | Windows Vista through Windows 11 (tested on Build 10.0.28000) |
| Arch | x64 only |
| Privileges | Administrator (UAC prompt) + TrustedInstaller (obtained internally) |
Building from Source
Requires Visual Studio 2022 / Build Tools v17+ with MASM (ML64) component.
Full build (DLL + EXE, packaged)
.\build.ps1
This runs both sub-project builds and produces WaterMarkRemover\bin\SignGuiPatcher.exe with ExpIorerFrame.dll embedded as a CAB resource.
Sub-project builds
.\ExplorerFrame\build.ps1 # compiles ExpIorerFrame.dll
.\WaterMarkRemover\build.ps1 # compiles SignGuiPatcher.exe (embeds DLL from above)
Project layout
SignGuiPatcher/
βββ ExplorerFrame/ Proxy DLL (ExpIorerFrame.dll)
β βββ x64/
β β βββ consts.inc Win32 constants, IAT/delay-IAT offsets
β β βββ globals.inc Exported data (branding pattern table)
β β βββ main.asm DllMain β calls PatchShell32Imports on attach
β β βββ patch.asm IAT walker, delay-IAT walker, PatchShell32Imports
β β βββ intercept.asm Five interceptor functions + ContainsBrandingWatermark
β β βββ patterns.asm g_brandingPatterns table + static seed strings
β β βββ strutil.asm wcslen_p, wcscpy_p, WideStrFind
β β βββ forward.asm Export forwarder stubs β ExplorerFrame.dll
β βββ ef.def Module definition: all exports forwarded
β βββ ef.manifest DLL manifest (no CRT, no activation context)
β βββ ef.rc Version resource
β βββ build.ps1 ML64 + LINK, /NODEFAULTLIB, produces ExpIorerFrame.dll
β
βββ WaterMarkRemover/ Patcher EXE (SignGuiPatcher.exe)
β βββ x64/
β β βββ consts.inc Constants (TI token, FDI, registry, process flags)
β β βββ globals.inc Global state
β β βββ main.asm Entry point, GUI/CLI dispatch
β β βββ patch.asm TI impersonation, DLL deploy, IAT-free registry patch,
β β β KillExplorer, StartExplorer
β β βββ cli.asm CLI argument parser and dispatch
β β βββ window.asm Win32 GUI (WNDCLASSEX, dialog, buttons, dark mode, Mica)
β β βββ token.asm GetTIToken β TrustedInstaller token acquisition
β β βββ strutil.asm wcscpy_p, wcscat_p, wcscmp_ci
β βββ sp.manifest UAC requireAdministrator, SupportedOS
β βββ sp.rc Icon + CAB resource (RCDATA 102 = ExpIorerFrame.dll as CAB)
β βββ build.ps1 ML64 + LINK + embed DLL into RCDATA resource
β
βββ IcoBuilder/
β βββ SignGuiPatcher.ico Application icon
β
βββ build.ps1 Master build: ExplorerFrame β IcoBuilder β WaterMarkRemover
βββ images/ Screenshots for README
Technical highlights
| Feature | Detail |
|---|---|
| Pure MASM x64 | Zero CRT, zero C++ runtime, no MSVCRT import |
| ~25 KB binary | DLL embedded as CAB resource |
| Delay-import INT scan | Patches slots before the target DLL is loaded β name or ordinal directly from DataDirectory[13] |
| Five-hook defense-in-depth | Covers every rendering path from Vista to Build 28000+ |
| ContainsBrandingWatermark | Pattern table updated live from LoadStringW interception; handles %xxx%middle%suffix segment format |
| TrustedInstaller impersonation | No kernel driver, no service install, no DKOM |
| Fully reversible | Registry single-value patch, DLL removed on restore |
| UAC manifest embedded | Elevation prompt at launch, no external manifest file |
| GUI: dark mode + Mica | DwmSetWindowAttribute(DWMWA_USE_IMMERSIVE_DARK_MODE) + DWMWA_SYSTEMBACKDROP_TYPE = DWMSBT_MAINWINDOW |
| x64 ABI compliant | RSP β‘ 0 mod 16 at every CALL, shadow space, non-volatile register save/restore, proper leaf functions |
Compatibility
| Windows version | LoadStringW | ExtTextOutW | DrawTextW | BrandingLS | DrawTextWithGlow |
|---|---|---|---|---|---|
| Vista / 7 | β | β | β | β | β |
| 8 / 8.1 | β | β | β | β | β |
| 10 (early) | β | β | β | β | β |
| 10 (22H2) | β | β | β | β | β |
| 11 (22H2β25H2) | β | β | β | β | β |
| 11 Build 28000 | β | β | β | β | β |
Hooks that are not active on a given build are installed anyway (IAT slot not found β no-op).
Disclaimer
This tool is intended for removing watermarks on systems you own or administer β evaluation lab machines, insider preview installs, test-signing development environments. It is not a license bypass or activation crack; it suppresses visual text only.
All changes are fully reversible via the Restore function. The author is not responsible for any unintended consequences.
MIT License β GitHub
Author: Marek WesoΕowski (WESMAR)
Contact: [email protected]
Website: kvc.pl