Virtual Address Descriptors: The VAD Tree and Memory Region Tracking
The PEB lies. The loader list can be unlinked, module names spoofed, Ldr entries stripped so that EnumProcessModules returns garbage. If you have ever chased an injected DLL and found the user-mode reports contradicting each other, you already know the frustration. The VAD tree, held in the kernel and maintained by the Memory Manager itself, is the ground truth: every reservation, every commit, every mapped file the process actually owns. It is the ledger you cross-check when the user-mode view is compromised.
This post walks the VAD end to end on Windows 10/11 x64. We compile a tiny lab target, allocate a page with VirtualAlloc, watch the tree grow in WinDbg, then stomp a mapped image and see how Volatility catches the seam. Detection follows.
Contents
- 1 1. Virtual Memory in One Screen
- 2 2. The VAD as the Kernel’s Authoritative Ledger
- 3 3. Why AVL
- 4 4. The Structures, Bottom Up
- 5 5. _MMVAD_FLAGS: Where the Interesting Bits Live
- 6 6. What Each WinAPI Actually Does to the Tree
- 7 7. Lab Target
- 8 8. Walking the Tree Live in WinDbg
- 9 9. Volatility 3: The Same View Post-Mortem
- 10 10. Attacker Abuse: VAD Stomping and Hollowing
- 11 11. Common Attacker Techniques
- 12 12. Defensive Strategies and Detection
- 13 13. Tools for VAD Analysis
- 14 14. MITRE ATT&CK Mapping
- 15 15. Summary
- 16 Related Tutorials
- 17 References
1. Virtual Memory in One Screen
Every Windows x64 process gets a 128 TB user-mode virtual address space (0 to 0x7FFF_FFFF_FFFF), paired with a 128 TB kernel half. That space is sparse. Only fragments are ever reserved, fewer are committed, and only committed pages that have actually been touched have real page table entries backing them.
Two structures cooperate to track this:
| Layer | Structure | Owner | Granularity |
|---|---|---|---|
| Region ledger | VAD tree (_MMVAD nodes) | Kernel Memory Manager | Range of pages |
| Page mapping | Page tables (_MMPTE) | CPU + kernel | Single 4 KB page |
The PTEs describe individual pages that are currently in physical memory or on the pagefile. The VAD tree describes the intent: “process X has reserved pages 0x00007FF60000 through 0x00007FF60FFF as private RW, and no, no PTE exists yet because the process has not touched it.” A page fault on a reserved-but-uncommitted address is resolved by the VAD, not the PTE.
That separation is why the VAD matters for forensics. The tree exists whether the physical page is resident, paged out, or never touched.
2. The VAD as the Kernel’s Authoritative Ledger
A Virtual Address Descriptor is a kernel structure. One node per contiguous region of virtual address space that shares the same attributes: allocation type, protection, and backing. Each VirtualAlloc produces a node. Each MapViewOfFile produces a node. Each VirtualFree (with MEM_RELEASE) removes one.
The tree is per-process. Its root pointer sits in _EPROCESS.VadRoot, and only the kernel modifies it (under process-wide locks). Nothing in user space can rewrite the tree directly. You can call NtProtectVirtualMemory to update the Protection bitfield of a node, but you cannot forge a node, hide one, or make the kernel forget an allocation you made.
Contrast with the PEB’s Ldr list: user mode, walkable and writable by any thread with PROCESS_VM_WRITE. Every “hide my DLL from the module list” trick since NTIllusion is unlinking entries from that user-mode list. The VAD entry for the mapped image? Still there, unchanged, waiting for a forensic tool to enumerate it.

3. Why AVL
The tree is an AVL self-balancing binary search tree keyed on the region’s starting Virtual Page Number. Left child is a lower VPN, right child is a higher VPN. Insert, delete, and lookup are all O(log n) even with pathological allocation orders, and the kernel does a lot of lookups: every page fault, every VirtualQuery, every access violation dispatch walks the tree.
On x64 the wrapper is _RTL_AVL_TREE. Each node embeds a _RTL_BALANCED_NODE as its first member for the linkage (Left, Right, ParentValue with balance bits packed into the low two bits of the parent pointer).
4. The Structures, Bottom Up
4.1. _EPROCESS: The Entry Point
On Windows 10/11 x64:
+0x7d8 VadRoot : _RTL_AVL_TREE
+0x7e0 VadHint : Ptr64 Void
+0x7e8 VadCount : Uint8B
Offsets shift between builds, so always resolve with dt nt!_EPROCESS in the debugger rather than hardcoding. VadHint is a cached last-accessed node for locality (subsequent lookups near the same VA hit it first).
4.2. _MMVAD_SHORT: Private Allocations
For private memory (created by VirtualAlloc), the kernel uses _MMVAD_SHORT. It embeds the tree linkage as its first member and holds the range plus flags. Pool-tagged VadS.
typedef struct _MMVAD_SHORT {
_RTL_BALANCED_NODE VadNode; // Left / Right / ParentValue
ULONG StartingVpn; // low 32 bits
ULONG EndingVpn; // low 32 bits
UCHAR StartingVpnHigh;
UCHAR EndingVpnHigh;
// ...
MMVAD_FLAGS u; // 32-bit bitfield
LONG ReferenceCount;
// ...
} MMVAD_SHORT, *PMMVAD_SHORT;
StartingVpn and EndingVpn are page numbers, not addresses. To get the actual virtual address, multiply by 0x1000:
VA_start = (StartingVpnHigh << 32 | StartingVpn) * 0x1000
VA_end = ((EndingVpnHigh << 32 | EndingVpn) * 0x1000) + 0xFFF
The + 0xFFF at the end accounts for the fact that EndingVpn is inclusive of the last page.
4.3. _MMVAD: File-Backed and Image Maps
For mapped files or PE images, the kernel uses the full _MMVAD structure. Pool-tagged Vad (with a trailing space).
dt nt!_MMVAD
+0x000 Core : _MMVAD_SHORT
+0x040 u2 : <anonymous-tag>
+0x048 Subsection : Ptr64 _SUBSECTION
+0x050 FirstPrototypePte : Ptr64 _MMPTE
+0x058 LastContiguousPte : Ptr64 _MMPTE
+0x060 ViewLinks : _LIST_ENTRY
+0x070 VadsProcess : Ptr64 _EPROCESS
+0x080 FileObject : Ptr64 _FILE_OBJECT
The chain _MMVAD -> Subsection -> ControlArea -> FilePointer is what lets !vad print the filename for image-backed regions. When you inject a DLL by LoadLibrary, that entire chain is populated with references to the real on-disk file object. FileObject at +0x80 is the fast path to the same info.
FirstPrototypePte / LastContiguousPte bookend the prototype PTE array the Memory Manager uses to lazily materialize per-process PTEs for shared pages. When two processes map the same DLL, both _MMVAD nodes point into the same prototype range; the per-process PTEs are filled in on first access.
Pool tags to memorize:
| Tag | Structure | Meaning |
|---|---|---|
VadS | _MMVAD_SHORT | Private allocation (heap, stack, VirtualAlloc) |
Vad | _MMVAD | Mapped file or image |
VadF | _MMVAD | Large-page VAD (typically private) |
Vadm | _MMVAD | Large mapped section |

5. _MMVAD_FLAGS: Where the Interesting Bits Live
Four bytes, packed. This is what forensic tools decode to classify a region.
struct _MMVAD_FLAGS {
unsigned long VadType : 3; // 0..7
unsigned long Protection : 5; // encoded page prot
unsigned long PreferredNode : 6;
unsigned long NoChange : 1;
unsigned long PrivateMemory : 1; // 1 = private, 0 = section-backed
unsigned long Teb : 1;
unsigned long PrivateFixup : 1;
unsigned long ManySubsections : 1;
unsigned long Spare : 12;
unsigned long DeleteInProgress: 1;
};
The two fields you check first are always VadType and PrivateMemory.
VadType values:
| Value | Name | What it means |
|---|---|---|
| 0 | VadNone | Private commit / plain VirtualAlloc |
| 1 | VadDevicePhysicalMemory | Mapping over \Device\PhysicalMemory |
| 2 | VadImageMap | PE mapped as image (SEC_IMAGE) |
| 3 | VadAwe | Address Windowing Extensions |
| 4 | VadWriteWatch | Tracked writes |
| 5 | VadLargePages | Large page backing |
| 6 | VadRotatePhysical | Rotate physical (GPU/DirectComposition) |
| 7 | VadLargePageSection | Large-page mapped section |
Protection is not the Win32 PAGE_* constants. It is a 5-bit index into a kernel table. Common values you will see:
| Encoded | Approximate meaning |
|---|---|
| 1 | PAGE_READONLY |
| 2 | PAGE_EXECUTE |
| 3 | PAGE_EXECUTE_READ |
| 4 | PAGE_READWRITE |
| 5 | PAGE_WRITECOPY |
| 6 | PAGE_EXECUTE_READWRITE |
| 7 | PAGE_EXECUTE_WRITECOPY |
That table (MmProtectToValue) is defined in the kernel and has been stable across modern Windows 10/11 builds – index 0 is PAGE_NOACCESS, and the order shown above is the canonical one. The PAGE_GUARD and PAGE_NOCACHE modifiers are folded into other bit positions in _MMPTE, not here.
PrivateMemory = 1 on a region with Protection = 6 (RWX) in a process that has no reason to JIT is the single loudest signal in malfind. It is the “unbacked RWX private commit” hunt query.
6. What Each WinAPI Actually Does to the Tree
| API (user) | Nt* (kernel) | VAD effect |
|---|---|---|
VirtualAlloc (reserve) | NtAllocateVirtualMemory | Creates a _MMVAD_SHORT node, PrivateMemory=1, Protection=0 (no access) |
VirtualAlloc (commit) | same | Same node, Protection updated, no PTE yet |
VirtualProtect | NtProtectVirtualMemory | Updates Protection on existing node; may split a node if the range is partial |
MapViewOfFile | NtMapViewOfSection | Creates a _MMVAD node, PrivateMemory=0, Subsection populated, VadType=VadImageMap for SEC_IMAGE |
VirtualFree (MEM_RELEASE) | NtFreeVirtualMemory | Removes node, sets DeleteInProgress during teardown |
VirtualQuery[Ex] | NtQueryVirtualMemory | Read-only walk of the tree, returns MEMORY_BASIC_INFORMATION |
The kernel path lands in MiInsertVad (insert), MiRemoveVad (remove), and MiLocateAddress (lookup). Splitting a range with a partial VirtualProtect is genuinely nontrivial: the kernel allocates a new node, adjusts the tree, and rebalances. Attackers using RWX in small slivers can end up creating a suspicious number of adjacent VadS nodes with weird protection quilting; that itself is a hunt signal.
7. Lab Target
Create vad_lab_target.c. Compile with MSVC (cl /Zi vad_lab_target.c) or MinGW (x86_64-w64-mingw32-gcc -g vad_lab_target.c -o vad_lab_target.exe). The point is to produce a textbook VadS region that we can watch through each state transition.
// vad_lab_target.c - intentionally simple, for the VAD walkthrough
#include <windows.h>
#include <stdio.h>
int main(void) {
printf("[+] PID = %lu\n", GetCurrentProcessId());
printf("[+] Press ENTER to VirtualAlloc RW...\n");
getchar();
// (1) Reserve + commit a page as PAGE_READWRITE -> creates VadS node
LPVOID mem = VirtualAlloc(NULL, 0x1000,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
printf("[+] Alloc @ %p (Protection = PAGE_READWRITE)\n", mem);
getchar();
// (2) Copy in a harmless "payload": ret (0xC3) plus filler
unsigned char payload[16] = {
0x48, 0x31, 0xC0, // xor rax, rax
0xC3, // ret
0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90
};
memcpy(mem, payload, sizeof(payload));
printf("[+] Written 16 bytes. Press ENTER to VirtualProtect -> RX...\n");
getchar();
// (3) Flip protection to PAGE_EXECUTE_READ -> Protection bitfield updates
DWORD old = 0;
VirtualProtect(mem, 0x1000, PAGE_EXECUTE_READ, &old);
printf("[+] Now PAGE_EXECUTE_READ. Press ENTER to call it...\n");
getchar();
// (4) Call the region as a function pointer (returns 0)
int (*fn)(void) = (int(*)(void))mem;
int rv = fn();
printf("[+] Call returned %d. Press ENTER to VirtualFree...\n", rv);
getchar();
// (5) MEM_RELEASE -> node removed from VAD tree
VirtualFree(mem, 0, MEM_RELEASE);
printf("[+] Freed. Exiting.\n");
return 0;
}
Each getchar() is a debugger checkpoint. Between them the process is idle, so you can attach the kernel debugger, inspect the tree, and continue.
8. Walking the Tree Live in WinDbg
You need a kernel debugger attached to the lab VM. KDNET over the network is the least painful setup on modern Windows: run bcdedit /debug on and bcdedit /dbgsettings NET HOSTIP:<host> PORT:50000 KEY:... in the guest, then attach WinDbg Preview on the host.
Launch the target, note its PID, and break in.
8.1. Find the _EPROCESS
kd> !process 0 0 vad_lab_target.exe
PROCESS ffffab8a2d5b3080
SessionId: 1 Cid: 0e94 Peb: 3f4e3ff000
Image: vad_lab_target.exe
8.2. Locate VadRoot
kd> dt nt!_EPROCESS ffffab8a2d5b3080 VadRoot VadCount
+0x7d8 VadRoot : _RTL_AVL_TREE
+0x7e8 VadCount : 0x2a
The tree is under VadRoot.Root. Dump the whole thing:
First read the VadRoot.Root pointer (that is what !vad expects, not the address of the VadRoot field itself):
kd> dt nt!_RTL_AVL_TREE ffffab8a2d5b3080+7d8
+0x000 Root : 0xffffab8a`1c987654 Void
kd> !vad 0xffffab8a1c987654
VAD Level Start End Commit
ffffab8a1c123abc 4 7ff60000 7ff60000 1 Private READWRITE
ffffab8a1c987654 0 7ff700000 7ff7000ff 256 Mapped Exe EXECUTE_WRITECOPY vad_lab_target.exe
ffffab8a1c112233 3 7ffab0000 7ffab01ff 512 Mapped Exe EXECUTE_WRITECOPY ntdll.dll
...
Actual output varies. The Private READWRITE row on line 1 (single page committed, start VPN 7ff60000) is our VirtualAlloc. VPN to VA: 0x7ff60000 * 0x1000 = 0x7ff60000000 (matches the pointer the target printed, minus the offset the linker chose). Note that !vad prints the raw VPN in the Start/End columns, so the same node’s _MMVAD_SHORT.StartingVpn field will read as 0x7ff60000 when you dt it directly.
8.3. Inspect the Node
kd> dt nt!_MMVAD_SHORT ffffab8a1c123abc
+0x000 VadNode : _RTL_BALANCED_NODE
+0x018 StartingVpn : 0x7ff60000
+0x01c EndingVpn : 0x7ff60000
+0x020 StartingVpnHigh : 0
+0x021 EndingVpnHigh : 0
+0x030 u : <unnamed-tag>
+0x000 VadFlags : _MMVAD_FLAGS
Peel the flags:
kd> dt nt!_MMVAD_FLAGS ffffab8a1c123abc+30
+0x000 VadType : 0y000 (0 = VadNone)
+0x000 Protection : 0y00100 (4 = PAGE_READWRITE)
+0x000 PrivateMemory : 0y1
+0x000 DeleteInProgress: 0y0
Continue the target past the second checkpoint (VirtualProtect fires) and re-inspect the same node address:
kd> dt nt!_MMVAD_FLAGS ffffab8a1c123abc+30
+0x000 Protection : 0y00011 (3 = PAGE_EXECUTE_READ)
Same node, same VPN range, same tree position. Only the Protection bitfield changed. That is NtProtectVirtualMemory in action.
8.4. Verify the Pool Tag
kd> !pool ffffab8a1c123abc
Pool page ffffab8a1c123000 region is Nonpaged pool
...
*ffffab8a1c123ab0 size: 0x40 previous size: 0x30 (Allocated) *VadS
VadS. Private allocation. Matches PrivateMemory = 1.
Traverse manually if you want to see the tree shape:
kd> dt nt!_MMVAD_SHORT <node> VadNode.Left VadNode.Right
Follow Left for lower VAs, Right for higher. The bottom two bits of ParentValue encode the balance factor; mask them off (& ~0x3) before dereferencing.
9. Volatility 3: The Same View Post-Mortem
Take a memory dump of the lab VM (WinPmem, DumpIt, or .dump /f from WinDbg). Then:
vol.py -f lab.mem windows.pslist | grep vad_lab_target
vol.py -f lab.mem windows.vadinfo --pid 3732
Expected output for our region (abbreviated):
PID Process Start End Tag Protection File
3732 vad_lab_target.exe 0x7ff60000000 0x7ff60000fff VadS PAGE_EXECUTE_READ
3732 vad_lab_target.exe 0x7ff700000000 0x7ff70000ffff Vad PAGE_EXECUTE_WRITECOPY \Device\...\vad_lab_target.exe
Now the malfind plugin:
vol.py -f lab.mem windows.malfind --pid 3732
Malfind’s heuristic is straightforward: it flags VadS regions where the protection allows execution and the first bytes look like code (an MZ header, or valid x86/x64 instructions and no null header). Our lab region has a xor rax, rax; ret prologue with NOPs, so malfind will happily flag it and print a disassembly.
That is the shape of every reasonable EDR memory scanner: enumerate VADs, classify each region, focus on PrivateMemory=1 with executable protection, and disassemble the head.
10. Attacker Abuse: VAD Stomping and Hollowing
The reason the VAD matters offensively is exactly the reason it matters defensively: it is what the memory scanner reads.
Classic injection (T1055.001) is loud. VirtualAllocEx creates a fresh VadS node in the target. WriteProcessMemory fills it. CreateRemoteThread runs it. Every memory scanner that classifies private+executable as anomalous catches it.
VAD stomping / module stomping is the attacker’s answer. The technique:
- Attacker chooses a benign, signed DLL that the target already loads (or maps a fresh one with
LoadLibrary). - In its address range,
VirtualProtecta section (usually.text) toPAGE_EXECUTE_READWRITE. - Overwrite the code with shellcode.
- Flip protection back to
PAGE_EXECUTE_READ. - Redirect execution into the overwritten region (thread hijack, APC, etc.).
The VAD node still says VadImageMap, still references the on-disk FileObject with the real signed DLL path. To a scanner walking the tree, the region is “image-backed”, which most heuristics trust more than private commits. The only tell is a content hash of the in-memory pages versus the on-disk file. Modern EDRs do exactly that, but plenty of older tooling does not.
Sample lab code (self-targeting, run against the operator’s own process):
// vad_stomp_lab.c - self-target only; never against a process you don't own
#include <windows.h>
#include <stdio.h>
#include <string.h>
int main(void) {
// Load a benign helper DLL we already ship (any signed DLL in System32
// works; use one you're not otherwise depending on).
HMODULE h = LoadLibraryA("version.dll");
if (!h) return 1;
// Locate its .text region. In real ops you'd parse the PE headers;
// for the lab, grab an exported function as a target address.
FARPROC target = GetProcAddress(h, "GetFileVersionInfoA");
printf("[+] Overwriting @ %p (inside VadImageMap for version.dll)\n", target);
unsigned char payload[] = {
0x48, 0x31, 0xC0, // xor rax, rax
0xC3 // ret
};
DWORD old = 0;
VirtualProtect(target, sizeof(payload), PAGE_EXECUTE_READWRITE, &old);
memcpy(target, payload, sizeof(payload));
VirtualProtect(target, sizeof(payload), PAGE_EXECUTE_READ, &old);
printf("[+] Stomp complete. VAD still shows version.dll on disk.\n");
// Redirect: call our stomped export
((void(*)(void))target)();
return 0;
}
Dump the VM, run windows.vadinfo, and the region will still print with the version.dll file path attached. Run windows.malfind and it will not flag it (private+executable heuristic misses image-backed stomping). You need a hash-check plugin (windows.dlllist + windows.moddump, then compare hash to disk) to see the mismatch. That gap is exactly what stompers rely on.
Process hollowing (T1055.012) goes further: create a suspended child process, NtUnmapViewOfSection the primary image, allocate a fresh region at the same base, write a different PE, fix up the entry point in the thread context, resume. The VadImageMap node in the child either references the original PE (if you skipped the unmap and just overwrote), or has been recreated as VadS at the image base with no file backing (if you unmapped). Modern Sysmon (13+) catches this via Event ID 25 – ProcessTampering by comparing the mapped image to disk.

11. Common Attacker Techniques
| Technique | Description |
|---|---|
| Classic remote injection | VirtualAllocEx + WriteProcessMemory + CreateRemoteThread. Creates a fresh VadS in target. Loudest. |
| VAD / module stomping | Overwrite a mapped image’s committed pages while keeping the VadImageMap node intact. Region looks image-backed to naive scanners. |
| Process hollowing | Replace image contents (or unmap-and-remap) in a suspended child. VAD may show original file path with divergent content. |
| Process doppelganging | TxF-based; the section is created from a transacted image so the on-disk file “looks” legit while the section content differs. |
| DLL hollowing | Similar to stomping but on a legitimately loaded DLL’s data or code sections, avoiding creation of any new VAD entry. |
| Reflective DLL injection | Copy raw DLL bytes into a target VadS region and self-relocate. VAD shows private commit with executable protection: classic malfind hit. |
| PEB unlinking | Removes Ldr entries in user mode. Does not touch the VAD, so windows.vadinfo still shows the mapped image. |
12. Defensive Strategies and Detection
12.1. Sysmon Event IDs
| Event ID | Event | VAD relevance |
|---|---|---|
1 | Process Create | Baseline; correlate with unusual command lines that follow injection |
7 | ImageLoad | Correlate module load path with what windows.vadinfo reports later |
8 | CreateRemoteThread | Fires when a remote thread is created in another process; classic injection last step |
10 | ProcessAccess | Cross-process handle grab; GrantedAccess containing 0x0020 (VM_WRITE), 0x0008 (VM_OPERATION), 0x0002 (CREATE_THREAD) is the setup for VAD manipulation |
25 | ProcessTampering | Sysmon 13+; fires when the in-memory image diverges from the mapped file (hollowing) |
12.2. ETW Providers
| Provider | GUID / name | What it yields |
|---|---|---|
Microsoft-Windows-Kernel-Process | {22FB2CD6-0E7B-422B-A0C7-2FAD1FD0E716} | Process, thread, image events |
Microsoft-Windows-Kernel-Memory | {D1D93EF7-E1F2-4F45-9943-03D245FE6C00} | Memory allocation/free events |
Microsoft-Windows-Threat-Intelligence | ETW-TI (PPL-gated) | VirtualAlloc, VirtualProtect, MapViewOfSection, WriteProcessMemory page-level detail |
ETW-TI is what Microsoft Defender for Endpoint consumes for injection telemetry. If you want your own detections to reach parity with commercial EDR, this is the provider to subscribe to. Access requires PPL protection on the consumer, which is why hobbyist tooling rarely uses it.
12.3. Sigma Rules
Cross-process VAD write setup (setup for stomping or classic injection):
title: Cross-Process Handle With VM_WRITE Access
id: 7a3f8d90-4b0a-4b0e-8f24-1c9e4d6f3a11
logsource:
product: windows
service: sysmon
detection:
selection:
EventID: 10
GrantedAccess|contains:
- '0x1fffff' # PROCESS_ALL_ACCESS
- '0x1f3fff'
- '0x143a' # VM_WRITE | VM_OPERATION | CREATE_THREAD
filter_legit:
SourceImage|endswith:
- '\MsMpEng.exe'
- '\csrss.exe'
- '\wininit.exe'
condition: selection and not filter_legit
fields:
- SourceImage
- TargetImage
- GrantedAccess
- CallTrace
level: high
Remote thread creation (last step of most injections):
title: Remote Thread Created In Non-System Target
id: b81f0d21-eb9c-4a70-b0d3-3d2c1b7f5c02
logsource:
product: windows
service: sysmon
detection:
selection:
EventID: 8
filter_system:
SourceImage|startswith: 'C:\Windows\System32\'
TargetImage|startswith: 'C:\Windows\System32\'
condition: selection and not filter_system
level: high
Process tampering / hollowing:
title: Sysmon ProcessTampering Image Replaced
id: 3c1b2a5d-7f10-4b19-9a2c-08e8b1f4d221
logsource:
product: windows
service: sysmon
detection:
selection:
EventID: 25
Type: 'Image is replaced'
condition: selection
level: high
12.4. Hunt Queries Against VAD Snapshots
Run periodically on endpoint memory snapshots or via a Volatility-backed pipeline:
- Regions with
PrivateMemory = 1andProtectionin {PAGE_EXECUTE_READ,PAGE_EXECUTE_READWRITE,PAGE_EXECUTE_WRITECOPY} inside processes that have no JIT (anything that isn’t a browser,dotnet,java,node, PowerShell, etc.) VadImageMapregions whose mapped file hash does not match the on-disk hash (stomping signature).- Regions with
FileObjectpointing to\Device\PhysicalMemoryor unsigned paths outsideSystem32,SysWOW64, or the app install directory. - Sudden increase in adjacent
VadSnodes with alternating protections (indicative of small-sliver RWX flipping).
12.5. Hardening
- Enable Arbitrary Code Guard (ACG) on sensitive processes. ACG blocks
NtProtectVirtualMemoryfrom setting executable on a region that was writable, and blocksNtAllocateVirtualMemoryfor RWX. It kills the naive stomping and reflective loading paths outright. - Code Integrity Guard (CIG) blocks loading unsigned DLLs, which shuts down the “map arbitrary DLL as image, then stomp it” opener.
- Enable
ProcessCreationMitigationPolicyextension-point disable to prevent AppInit and legacy DLL injection. - Deploy Sysmon 13+ with a config that includes EIDs 7, 8, 10, 25 with sensible filters.
- Feed ETW-TI into your EDR/SIEM if the platform supports it.

13. Tools for VAD Analysis
| Tool | Description | Link |
|---|---|---|
| WinDbg Preview | Kernel debugger with !vad, !process, dt, !pool; primary live inspection tool | https://learn.microsoft.com/windows-hardware/drivers/debugger/ |
| Volatility 3 | Memory forensics; windows.vadinfo, windows.vadwalk, windows.malfind | https://www.volatilityfoundation.org/ |
| Process Hacker | Live per-process memory map with region protections and mapped files | https://processhacker.sourceforge.io/ |
| Sysinternals VMMap | User-mode view of a process’s VAD (via NtQueryVirtualMemory); great for visualising fragmentation | https://learn.microsoft.com/sysinternals/downloads/vmmap |
| WinPmem / DumpIt | Live memory acquisition for offline Volatility analysis | https://github.com/Velocidex/WinPmem |
| Sysmon (13+) | EID 8, 10, 25 for injection and tampering detection | https://learn.microsoft.com/sysinternals/downloads/sysmon |
| PE-sieve / Hollows Hunter | Scans running processes for hollowing, stomping, and reflective loads by comparing in-memory to disk | https://github.com/hasherezade/pe-sieve |
14. MITRE ATT&CK Mapping
| Technique | MITRE ID | Detection |
|---|---|---|
| Process Injection (parent) | T1055 | Sysmon EID 8, 10; ETW-TI; VAD anomalies |
| DLL Injection | T1055.001 | Correlate EID 10 (VM_WRITE) with EID 8; new VadS in target |
| Process Hollowing | T1055.012 | Sysmon EID 25; VAD file-hash mismatch |
| Process Doppelganging | T1055.013 | Transacted image loads; ETW-TI section create |
| Module Stomping (variant) | T1055.001 | Content hash of VadImageMap regions vs on-disk PE |
| Process Discovery | T1057 | NtQueryVirtualMemory traffic; EID 10 without follow-on writes |
15. Summary
- The VAD tree is the kernel’s authoritative per-process memory ledger: an AVL tree of
_MMVAD_SHORT/_MMVADnodes rooted at_EPROCESS.VadRoot, immune to user-mode forgery. - Each node encodes a region’s VPN range, protection, private-vs-mapped status, and file backing; the
VadS/Vadpool tags and_MMVAD_FLAGSbits are how you classify what a region really is. - Every
VirtualAlloc,MapViewOfFile, andVirtualProtecttranslates into a specific mutation of the tree, so the tree is a complete record of what the process has done to its address space. - Attackers cannot make VAD entries disappear, but they can camouflage: VAD stomping keeps the
VadImageMapnode intact while replacing page contents, and process hollowing relies on the same trick at PE-load time. - Detection is a two-front effort. Live telemetry (Sysmon EIDs 8, 10, 25; ETW-TI for page-level allocation events) plus periodic VAD hunting (Volatility
malfind, image-vs-disk hash comparison, RWX-in-private-commit queries).
Related Tutorials
- SIDs and Security Descriptors: Identity in Windows Security
- Memory Management Internals
- Access Tokens and Privileges: The Kernel’s Security Context
- Fibers: User-Mode Cooperative Threads
- Jobs and Silos: Process Grouping and Resource Limits
References
- bsodtutorials.wordpress.com
- imphash.medium.com
- rce4fun.blogspot.com
- lilxam.tuxfamily.org
- malicious.dev
- bsodtutorials.blogspot.com
- shasaurabh.blogspot.com
- dfrws.org
Get new drops in your inbox
Windows internals, exploit dev, and red-team write-ups - no spam, unsubscribe anytime.