How I Ruined My Vacation by Reverse Engineering WSC

How I Ruined My Vacation by Reverse Engineering WSC

Original text: “how i ruined my vacation by reverse engineering wsc”es3n1n, blog.es3n1n.eu (May 8, 2025). Code blocks and figures are reproduced verbatim with attribution captions.

Executive Summary

es3n1n is the author of no-defender, a tool that disabled Windows Defender by calling Windows Security Center (WSC) COM APIs while posing as an authorized security provider. After a DMCA takedown removed the original repository, a request to rebuild a clean re-implementation sparked a four-day reverse engineering sprint—conducted almost entirely from a vacation in Seoul, Korea, on an ARM64 MacBook with Parallels, across two remote Windows VMs and a 210 ms transatlantic link. The result is defendnot: a new open-source tool that deregisters any installed antivirus from Windows Security Center.

This article is a day-by-day engineering diary of that process. The author traces each layer of WSC’s caller-validation stack: the initial COM RPC call that returns access denied for unsigned callers, the Process Protection Level (PPL) gate on the calling binary, the WinDefend SID token membership check inside WscServiceUtils::CreateExternalBaseFromCaller, the failed impersonation detour, the secondary signature and DllCharacteristics.ForceIntegrity validation path in CSecurityVerificationManager::CreateExternalBaseFromPESettings, and finally the discovery that Taskmgr.exe satisfies all requirements—making it a perfect host process for an injected registration DLL.

A One-Year Step Back

About a year before this adventure, es3n1n published no-defender—a tool that leveraged undocumented WSC COM interfaces to deregister Windows Defender and register a fake antivirus in its place, causing the Windows Security Center to report the system as “protected” by a non-existent product. The project accumulated roughly 1,500 GitHub stars before an antivirus vendor filed a DMCA takedown notice. GitHub complied, the repository was removed, and that version of the project was gone.

How It Started

On May 4, 2025, while vacationing in Seoul with an M4 Pro MacBook—deliberately leaving the x86 development machine at home—es3n1n received a message from a researcher called MrBruh asking about recreating a clean implementation of the no-defender concept. The question opened a rabbit hole that consumed the rest of the vacation.

Day 1 — Initial Research

The first step was examining the WSC service and its COM interfaces. Within about an hour of rebuilding the antivirus SDK implementation and spinning up ARM64 Windows inside Parallels, the initial call to register an AV provider returned an access denied error.

Access denied error when calling WSC COM API from an unsigned process
Access denied on the first WSC COM registration attempt. Source: original article.

The hypothesis was that WSC validates the calling process’s signature. To test it, the approach shifted from a standalone tool to a DLL injected directly into the antivirus’s own process. Running the WSC registration call from inside the AV’s address space succeeded immediately. Updating the AV status through the returned interface also worked.

First successful WSC AV registration, running from within the AV process via DLL injection
First successful registration—DLL injected into the antivirus process. Source: original article.

The proof of concept was enough to share publicly. A screenshot and tweet went out showing Windows Security Center reporting no AV installed while the real one was still running.

Screenshot posted to Twitter showing Windows Security Center reporting no antivirus
The working proof-of-concept shared on Twitter. Source: original article.
Tweet announcing the defendnot project in early development
The tweet announcing the project. Source: original article.

Day 1 — Eliminating the AV Binary Dependency

Requiring the victim antivirus binary to be present is a significant operational limitation. The next goal was finding a generic Windows system process that WSC would also accept as a host. The first attempt used cmd.exe, but WSC rejected it. Reverse engineering wscsvc.dll revealed why: the service checks the Process Protection Level of the calling binary, not just its signature.

Decompiled wscsvc.dll code showing the PPL protection check on the calling process
PPL check found in wscsvc.dll—rejecting non-PPL callers. Source: original article.

Process Protection Level (PPL) is a Windows kernel mechanism that restricts which processes can open, inject into, or be impersonated by other processes. WSC uses it as a first-pass gate: if the calling process is not PPL-protected, the registration path is rejected before signature checks even run. This moved the problem from “wrong signature” to “wrong protection level.”

Day 2 — Environment Setup

Building and testing on an ARM64 MacBook without a native x86 debugger (x64dbg does not run on ARM64 Windows) required an improvised workflow. A friend named Pindos shared remote access to a physical Windows PC via Parsec, but the latency from Seoul to the United States averaged around 210 ms—high enough to make every debugger interaction painful. The pipeline was: compile with MSVC on ARM64 Windows in Parallels, copy artifacts to a shared folder, transfer them to the remote VM via AnyDesk, then debug interactively over Parsec.

Debugger attached to remote Windows VM via Parsec high-latency setup from Seoul
The debugging environment—210 ms of Parsec latency from Seoul to the USA. Source: original article.

Day 2 — Debugging the WSC Service

To bypass the PPL check, the author loaded a kernel driver that stripped PPL protection from target processes on demand. With PPL removed, deeper tracing into wscsvc.dll became possible. The failure now occurred inside WscServiceUtils::CreateExternalBaseFromCaller. Static analysis there revealed a token membership check against a hardcoded SID:

Decompiled WSC code showing token membership check against the WinDefend service SID
The SID check found in WscServiceUtils::CreateExternalBaseFromCaller. Source: original article.

The decompiled logic impersonates the RPC client and then checks whether the impersonated token is a member of the WinDefend service SID (S-1-5-80-1913148863-3492339771-4165695881-2087618961-4109116736):

v10 = RpcImpersonateClient(ClientBinding);
if ( ConvertStringSidToSidW(L"S-1-5-80-1913148863-3492339771-4165695881-2087618961-4109116736", Sid) )
{
    if ( !CheckTokenMembership(0, Sid[0], &IsMember) )
    {
        IsMember = 0;
    }
    LocalFree(Sid[0]);
}
if ( IsMember )
{
    v8 = (*(__int64 (__fastcall **)(void *, struct CExternalBase **))(*(_QWORD *)a2 + 8LL))(a2, a4);
}
else
{
    // a bunch of some other stuff
}

Only if the token passes the SID check does WSC call the actual registration handler. Any other caller falls through to a secondary path that applies completely different validation logic.

Day 2 — Impersonating WinDefend

The straightforward solution appeared to be impersonating the WinDefend security principal before making the RPC call, so that the token membership check would pass. The author spent three hours studying Windows token mechanics and built an implementation that set up a WinDefend-matching token for the calling thread.

Diagram of the WinDefend impersonation approach to bypass the WSC SID token check
The impersonation plan. Source: original article.
Implementation of the WinDefend token impersonation attempt in code
Implementation of the WinDefend token impersonation. Source: original article.

The call returned STATUS_SUCCESS. But no antivirus registration appeared in Windows Security Center.

STATUS_SUCCESS returned but the AV registration did not appear in Windows Security Center
STATUS_SUCCESS with no visible result—the impersonation path was a dead end. Source: original article.

Day 3 — Rebuilding the Validation Algorithm

Re-examining the trace revealed the mistake: the WinDefend SID check had never actually passed for the original antivirus binary either. Instead, the “else” branch of that check led to CSecurityVerificationManager::CreateExternalBaseFromPESettings, which applies two separate validations to the calling binary’s on-disk PE image:

  • ForceIntegrity flag: the DllCharacteristics field of the PE optional header must have bit 7 set (IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY, value 0x80), indicating the binary was signed with a certificate that enforces integrity checking at load time.
  • Signature hash: the binary’s public key is hashed via CryptHashPublicKeyInfo and the result is stored against a known-good list. This means an arbitrary self-signed binary cannot pass even if ForceIntegrity is set artificially.
Recreated WSC binary validation algorithm from PE header inspection and signature hash check
The reconstructed validation algorithm identifying the DllCharacteristics check and signature hash. Source: original article.
/// Check DllCharacteristics flag
FileW = CreateFileW(a2, 0x80000000, 1u, 0, 3u, 0x8000000u, 0);
FileMappingW = CreateFileMappingW(FileW, 0, 2u, 0, 0, 0);
v12 = MapViewOfFile(FileMappingW, 4u, 0, 0, 0);
v14 = ImageNtHeader(v12);
v6 = SLOBYTE(v14->OptionalHeader.DllCharacteristics) < 0;
UnmapViewOfFile(v13);

/// Check the signature
CertContext = GetCertContext(a2, &pCertContext);
if ( CryptHashPublicKeyInfo(
        0,
        0x8003u,
        0,
        1u,
        &pCertContext->pCertInfo->SubjectPublicKeyInfo,
        (BYTE *)&SystemTime,
        &pcbComputedHash) )
{
    /// Store signature info
}

With the algorithm fully understood, it was possible to scan Windows System32 binaries programmatically for the right combination of ForceIntegrity and a matching signature hash. Taskmgr.exe satisfied all requirements.

Day 3 — Taskmgr.exe as the Victim Process

Initial injection attempts into Taskmgr.exe failed for an unrelated reason: the injected DLL was reading its own AV name from a configuration file (ctx.bin) using the module base of Taskmgr rather than the base of the injected DLL itself, so the path derivation produced null bytes. The name field was empty, and WSC rejected the registration.

Diagnosing this in slow motion over a 210 ms Parsec link with software-encoded video became intolerable. The author paid $30 for a Shadow.tech bare-metal cloud PC subscription to get a lower-latency debugging session. After fixing the module-base pointer and rebuilding, the registration succeeded.

Successful AV deregistration via Taskmgr injection and WSC COM API registration
It worked—Windows Defender deregistered via injection into Taskmgr.exe. Source: original article.
Reaction screenshot to successful defendnot implementation
The author’s reaction. Source: original article.

Day 3 — Code Cleanup

The author spent the remainder of Day 3 (until 8 AM) refactoring the implementation, separating concerns, and beginning work on an autorun capability so the tool would survive reboots. Autorun testing was deferred to the next day.

Day 4 — Autorun via Windows Task Scheduler

Autorun was implemented using Windows Task Scheduler. The first test failed because two default Task Scheduler settings prevented the task from running in the target environment:

  • “Run only when user is logged in” — prevented background execution.
  • “Run only on AC power” — blocked execution on battery-powered systems.
Windows Task Scheduler power settings checkboxes that blocked autorun execution
The two Task Scheduler power settings that silently blocked autorun. Source: original article.

Disabling both flags resolved the issue. Autorun worked as expected after the fix, followed by another round of code cleanup.

Acknowledgements

The author credits Pindos for providing remote PC access during development, and MrBruh for the initial inspiration and ongoing feedback throughout the project. The defendnot source code is published at github.com/es3n1n/defendnot, including a wsc-binary-check folder that contains the standalone validation algorithm used to scan for eligible victim processes.

Key Takeaways

  • Windows Security Center caller validation is layered: PPL check → WinDefend SID token membership check → PE ForceIntegrity flag + signature hash. Each layer can fail independently and the failure mode differs per layer.
  • Impersonating a service SID (CheckTokenMembership passes) does not guarantee execution of the privileged code path—WSC still falls through to secondary validation logic on the calling binary’s PE image.
  • The DllCharacteristics.ForceIntegrity bit (0x80) combined with a matching signature hash is the real gating mechanism for non-WinDefend callers. Scanning System32 for binaries that satisfy both conditions is a repeatable technique for finding suitable host processes.
  • Taskmgr.exe meets the requirements and is a widely present, inherently trusted system binary—it is running by default on most Windows installations and is unlikely to be flagged for injection based on name alone.
  • WSC’s ctx.bin configuration file reads the AV provider name by deriving a path from the calling module’s base address. Injected DLLs must use their own module base, not the host process base, to produce a valid path.
  • Windows Task Scheduler’s default power and login conditions silently suppress task execution without error logging—always explicitly clear these flags in persistence implementations.
  • High-latency remote debugging (210 ms) is viable for static analysis and single-step tracing but not for heavy interactive use; a cloud bare-metal VM subscription is worth the cost for time-sensitive work.

Defensive Recommendations

  • Monitor for DLL injection into Taskmgr.exe. This process is a known-good host for WSC registration bypass. An EDR rule that alerts on remote thread creation targeting Taskmgr (or any System32 binary with ForceIntegrity set) is a high-value detection opportunity.
  • Audit WSC provider registration events. Windows Security Center logs provider registration and deregistration. Any event that changes the registered AV provider outside of a known software installation context warrants investigation.
  • Detect PPL removal via kernel driver. Stripping PPL from protected processes requires kernel access, typically via a signed-but-vulnerable driver (BYOVD). Correlate driver load events (EtwTi, Sysmon Event ID 6) with subsequent process-open operations against PPL-protected targets.
  • Alert on RpcImpersonateClient calls from unexpected hosts. wscsvc RPC calls from processes other than known AV binaries are anomalous. If your EDR can hook RPC server-side stubs, this is a reliable signal.
  • Do not rely solely on WSC status as an AV health indicator. As this research demonstrates, WSC can be deceived into reporting a false security posture. Complement it with direct process and file presence checks for your endpoint agent.
  • Restrict Task Scheduler task creation by non-administrative users. The autorun mechanism here uses the standard Task Scheduler API. Group Policy can limit who may create tasks in the root scheduler namespace.
  • Enforce code integrity policies. Windows Defender Application Control (WDAC) can block unsigned or low-integrity binaries from executing, reducing the pool of injectable DLLs and making victim-process selection more difficult.

Conclusion

What started as a vacation curiosity became a systematic dismantling of every layer in WSC’s caller-validation stack. The key insight—that the WinDefend SID check is a red herring and the real gate is the PE ForceIntegrity flag plus signature hash on the else branch—was only visible because the first impersonation attempt appeared to succeed while producing no observable effect. The resulting defendnot tool demonstrates that WSC’s security provider registration model is not a reliable integrity signal on its own: a userspace tool can deregister a running antivirus and make the system appear unprotected without touching the AV process itself.

Original text: “how i ruined my vacation by reverse engineering wsc” by es3n1n at blog.es3n1n.eu.

Comments are closed.