Memory-Mapped Files
Section objects as the kernel mechanism behind file mapping, shared memory for IPC, copy-on-write semantics, and how module stomping abuses file-backed sections to hide shellcode
An EDR flags a suspicious memory region in a process: MEM_MAPPED, PAGE_EXECUTE_READ, backed by C:\Windows\System32\ntdll.dll. That sounds like a normal DLL mapping — but the hash of that region's content doesn't match the ntdll.dll on disk. The attacker used module stomping: map a legitimate DLL to get a file-backed executable region, then write shellcode over it. Understanding memory-mapped file mechanics is what lets you detect this.
Section Objects
The kernel mechanism underlying all memory-mapped files is the section object (also called a file mapping object in Win32 terminology). A section object represents a block of memory that can be mapped into one or more processes' virtual address spaces.
Section Object and Mapping
─────────────────────────────────────────────────────────────────────
File on Disk Section Object Process Address Space
────────────────── ────────────────── ──────────────────────
ntdll.dll ──► [SECTION object] ───► VA 0x7FFC00000000
│ (MEM_MAPPED, PAGE_EXECUTE_READ)
└──► VA 0x7FFB00000000 (another process)
(shared read-only pages — physical
pages shared between all processes)
Multiple processes map the same section:
Physical RAM page P → mapped into Process A at 0x7FFC00001000
→ mapped into Process B at 0x7FFC00001000
→ mapped into Process C at 0x7FF900001000
All three VAs point to the same physical page → zero wasted RAM
Memory Mapping API
// File mapping example: read a file's content via memory mapping
#include <windows.h>
PVOID MapFileReadOnly(LPCWSTR filePath, PDWORD pSize) {
HANDLE hFile = CreateFileW(filePath, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) return NULL;
*pSize = GetFileSize(hFile, NULL);
// Create a section object backed by this file
HANDLE hMapping = CreateFileMappingW(hFile, NULL,
PAGE_READONLY, // protection for the section
0, 0, // max size 0 = use file size
NULL); // unnamed section
CloseHandle(hFile);
if (!hMapping) return NULL;
// Map a view of the section into our address space
PVOID pView = MapViewOfFile(hMapping,
FILE_MAP_READ, // desired access
0, 0, // offset 0
0); // map whole section
CloseHandle(hMapping);
// pView points to the file content; unmap with UnmapViewOfFile(pView)
return pView;
}
Shared Memory Between Processes
Named section objects are the cleanest IPC mechanism for large data sharing. Unlike pipes (stream-based), shared memory allows random-access to a shared buffer:
// Producer: create named shared memory
HANDLE hMap = CreateFileMappingW(
INVALID_HANDLE_VALUE, // pagefile-backed (not file-backed)
NULL,
PAGE_READWRITE,
0, 4096, // 4 KB shared region
L"Local\\MySharedMemory");
PVOID pShared = MapViewOfFile(hMap, FILE_MAP_WRITE, 0, 0, 4096);
sprintf_s((char*)pShared, 4096, "Hello from producer\n");
// Consumer: open the named section
HANDLE hMap2 = OpenFileMappingW(FILE_MAP_READ, FALSE,
L"Local\\MySharedMemory");
PVOID pShared2 = MapViewOfFile(hMap2, FILE_MAP_READ, 0, 0, 4096);
printf("%s", (char*)pShared2);
// Both processes share the same physical pages — no copy needed
Named sections use the Object Namespace — the same namespace as named mutexes. "Local\\" prefix puts the name in the current session's namespace; "Global\\" crosses session boundaries (requires SeCreateGlobalObjects privilege). Malware C2 communication sometimes uses named sections for inter-process communication to avoid network traffic. If you see a process creating or opening an unusual named mapping, especially with the Global\ prefix, it's worth investigating.
Copy-on-Write (CoW)
When multiple processes map the same section and one tries to write to it, Windows uses copy-on-write: the kernel silently creates a private copy of the modified page for the writing process, while other processes continue to see the original shared page.
This is used by: (1) DLL code sections — all processes sharing ntdll.dll read from shared pages; if any process uses an inline hook to patch ntdll, it gets its own private copy of the patched page while other processes remain unaffected. (2) The Windows loader for executable images — the image's data sections are initially mapped shared (copy-on-write) and become private only when first written.
Copy-on-Write for DLL Code Sections ───────────────────────────────────────────────────────────────────── Before EDR hook: Process A.ntdll.dll → Physical Page P1 → [original NtCreateFile code] Process B.ntdll.dll → Physical Page P1 → [original NtCreateFile code] EDR.ntdll.dll → Physical Page P1 → [original NtCreateFile code] (all sharing page P1) After EDR patches NtCreateFile in Process A: Process A.ntdll.dll → Physical Page P1' → [JMP hook_trampoline] (private copy) Process B.ntdll.dll → Physical Page P1 → [original NtCreateFile code] EDR.ntdll.dll → Physical Page P1 → [original NtCreateFile code] (P1' is a new CoW page; P1 unchanged)
Module Stomping (Mapped Shellcode)
Module stomping is an injection evasion technique that uses file-backed sections to hide shellcode. Because the memory region is backed by a legitimate DLL on disk, it appears as MEM_MAPPED instead of MEM_PRIVATE — bypassing checkers that only look for private executable regions:
// Module stomping: map a legitimate DLL, then overwrite with shellcode
void ModuleStomping(PVOID shellcode, SIZE_T shellcodeLen) {
// Step 1: Map a "legitimate" DLL as a section
HANDLE hFile = CreateFileW(L"C:\\Windows\\System32\\version.dll",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, 0, NULL);
HANDLE hMapping = CreateFileMappingW(hFile, NULL,
PAGE_EXECUTE_READWRITE, // RWX section
0, 0, NULL);
PVOID pView = MapViewOfFile(hMapping,
FILE_MAP_ALL_ACCESS | FILE_MAP_EXECUTE,
0, 0, 0);
// Step 2: Overwrite section with shellcode
memcpy(pView, shellcode, shellcodeLen);
FlushInstructionCache(GetCurrentProcess(), pView, shellcodeLen);
// Step 3: Execute from the mapped region
((void(*)())pView)();
// The memory region shows as MEM_MAPPED, backed by version.dll
// VirtualQuery shows Type=MEM_MAPPED, not MEM_PRIVATE
}
Read the PE content from a mapped section and hash it. If the hash doesn't match the hash of the file on disk at the section's backing path, the section has been modified (stomped). Tools like PE-sieve implement this check: they compare each loaded module's in-memory content against the corresponding file on disk, flagging pages where the content diverges beyond expected CoW modifications.
Shared Memory vs Named Pipes
| Feature | Shared Memory (Section) | Named Pipe |
|---|---|---|
| Access pattern | Random access — both sides read/write at any offset | Stream — FIFO, sequential read |
| Performance | Highest — no kernel copy, direct page sharing | Moderate — kernel buffers, data copied |
| Sync required | Yes — manual mutex/event needed | Built-in — read blocks until data available |
| Bidirectional | Yes, both sides can write to same buffer | Yes, PIPE_ACCESS_DUPLEX |
| Malware use | Large data transfer, C2 staging buffers | Shell pipes (stdin/stdout redirect to C2) |
Q & A
Why do all processes see ntdll.dll at the same virtual address?
This is a deliberate optimization, not an accident. ntdll.dll is loaded into every single process on the system — it's the gateway between user-mode code and the kernel. If each process loaded ntdll at a different ASLR-randomized address, the physical pages could not be shared: shared memory requires that the same physical page is mapped at the same virtual address in all processes (otherwise virtual addresses in return values and pointers would be wrong across processes). Windows handles this by sharing a single ASLR base address for ntdll.dll system-wide, chosen once per boot. All processes load ntdll.dll at that same address. This allows a single set of physical pages to serve every process — vastly reducing RAM consumption. The same applies to kernel32.dll, kernelbase.dll, and a few other "known DLLs" listed in HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs. These DLLs are mapped from a pre-created section object at a fixed address.
Can module stomping be detected at the kernel level?
Yes, through several mechanisms: (1) Page hash checking: Windows Defender Credential Guard and Hypervisor Protected Code Integrity (HVCI) compute a hash of every executable page before allowing execution. If the page hash doesn't match the expected hash of the backing file, execution is blocked. This is the most robust defense against module stomping. (2) ETW security events: when a section's protection is changed (VirtualProtect on a mapped section), ETW captures this event. Mapped executable sections shouldn't normally change protection. (3) EDR memory scanning: tools like PE-sieve, Moneta, and built-in EDR memory scanner compare in-memory module content against on-disk content at regular intervals. Stomped pages show as "modified" regions — the bytes diverge from the file hash. The detection gap is that module stomping using a file that the scanning tool doesn't have reference hash for (a DLL that was modified before mapping) can slip through content comparison. HVCI is the strongest mitigation since it enforces page hash policy in the hypervisor layer.