Pavel Yosifovich, co-author of “Windows Internals 7th Edition, Part 1” and author of “Windows Native API Programming”, follows up on his walkthrough of VMMap by building the same kind of information from code instead of reading it off a GUI. This is aimed at Windows developers and security researchers who want to understand, and eventually automate, exactly how a tool like VMMap gets its data, rather than treating it as a black box.
What Are We Building, and Why Code It by Hand?
The project is a small console application called MemMap. Give it a process ID and it walks that process’s entire address space, printing the same kind of block-by-block breakdown VMMap shows on screen: start and end address, size, type, state, protection, and whatever extra detail can be pulled out of a given block. It will not reach full VMMap parity in one video. Images, mapped files, thread stacks, and TEBs are still missing and are coming in Part 2. But by the end of this one, the tool already finds the Process Environment Block on its own, which VMMap also shows, just from a different angle.
How Do You Open a Process and Walk Its Address Space?
Everything starts with a process ID read from the command line and converted with strtoul, which has a nice side effect: pass it a base of zero and it accepts a 0x prefixed hex value along with plain decimal, so the tool takes either format for free. That PID goes straight into OpenProcess, and the access mask question turns out to have a clean answer: the function that actually reads the address space, VirtualQueryEx, only requires PROCESS_QUERY_INFORMATION. Nothing more invasive is needed. That said, PROCESS_QUERY_INFORMATION is still not universal access. Protected processes remain out of reach no matter what mask you ask for, the same limitation I ran into with SeDebugPrivilege and AdjustTokenPrivileges: a privilege or a generous access mask gets you a long way with ordinary processes, but a protected process is a different category of problem entirely.
What Does VirtualQueryEx Actually Return?
With a handle in hand, the walk itself is a loop: start at address zero, and repeatedly call VirtualQueryEx, which fills in a MEMORY_BASIC_INFORMATION structure describing the block at that address, then advance by RegionSize and query again. Address zero is never actually valid, but starting there and letting the function walk forward from it works fine in practice. The function itself is a little quirky: it wants the size of the output structure passed in as a SIZE_T, and on success it returns that same size back to you rather than something more conventional like a boolean. Once the address wanders into kernel space, the call fails, and that failure is the natural signal to stop and close the handle.
How Do You Decode the State, Type, and Protection of Each Block?
MEMORY_BASIC_INFORMATION hands back its state, type, and protection as raw constants, and turning those into something readable is most of the work. State is one of three values: MEM_FREE, MEM_COMMIT, or MEM_RESERVE. Type is MEM_IMAGE, MEM_MAPPED, or MEM_PRIVATE. Protection is the more annoying one, because it is not a single value but a set of flags that mostly boil down to a handful of read, write, and execute combinations, plus modifiers layered on top. I represent the base permissions with three characters, r, w, x, filled in or blanked out depending on the constant, then append a /G if the guard page flag is set (the same guard page mechanism behind thread stacks) or a C for copy-on-write. It is not glamorous work, and honestly the kind of repetitive mapping you could hand to AI these days, but writing it out by hand here keeps the logic transparent.
Size gets a small conversion too: shifting RegionSize right by 10 bits turns bytes into kilobytes, and since every region is guaranteed to be at least one 4 KB page, nothing meaningful is lost by rounding at that granularity.
$2,111
$1,478 or $150 X 10 payments
Windows Master Developer
Takes you from a “generic” C programmer to a master Windows programmer in user mode and kernel mode.
How Do You Verify the Output Against VMMap?
Running the tool against a live process, say Explorer, immediately runs into a practical wall: the console window truncates long output, so a big chunk of the address space just never shows up on screen. Redirecting output to a text file instead of printing it fixes that completely, and the resulting dump lines up with VMMap block for block once you open it in a text editor: the same free regions, the same committed regions, the same protections. Comparing the raw dump against VMMap running against the same PID is a fast way to sanity check that the loop, the state and type decoding, and the size math are all correct before adding anything more elaborate.
How Do You Identify the Reserved-Block Boundaries VMMap Shows as Separate Chunks?
VMMap does not just list every block VirtualQueryEx returns in a flat line; it groups blocks into the reserved regions they originally came from, which is exactly why a thread stack shows up in VMMap as one expandable region containing several sub-blocks rather than three unrelated entries. The trick to reproduce that grouping is a single comparison: MEMORY_BASIC_INFORMATION.BaseAddress equals AllocationBase exactly when a block is the start of a fresh reserved region. Marking that boundary with a star in the output.
How Do You Find the Process Environment Block (PEB) in Code?
VMMap can point at the PEB too, but getting there in code means stepping outside the documented Win32 API entirely. The Process Environment Block is undocumented, and the only practical way to locate it for an arbitrary process is NtQueryInformationProcess, a native API function Microsoft documents just enough to be usable, through winternl.h, without fully explaining. Passing ProcessBasicInformation as the information class and a PROCESS_BASIC_INFORMATION structure gets back a PebBaseAddress field, even though most of the rest of that structure is marked reserved in the public header. From there, checking whether that address falls between a block’s BaseAddress and BaseAddress + RegionSize identifies exactly which block in the walk contains the PEB.
There is one build wrinkle worth naming: NtQueryInformationProcess lives in ntdll.dll, and the default import libraries a new project links against do not include it, so the linker fails even though the compiler is perfectly happy. A #pragma comment(lib, "ntdll.lib") fixes that without having to touch project settings, which keeps the dependency visible directly in the source rather than buried in a properties page. Formatting the PEB address for output goes through std::format from C++20, which is picky enough about types that the PebBaseAddress pointer needs an explicit cast to void* before it will compile.
What This Means Practically
- Use
VirtualQueryExwith nothing more thanPROCESS_QUERY_INFORMATIONto walk any accessible process’s address space without needing debug-level privileges. - Redirect output to a file rather than trying to read a full address space dump in a console window; the console buffer will silently cut it off.
- Compare
MEMORY_BASIC_INFORMATION.BaseAddressagainstAllocationBaseto detect where a reserved region starts, which is how to reproduce VMMap’s block grouping instead of a flat, ungrouped list. - Add
#pragma comment(lib, "ntdll.lib")whenever calling anNt*native API function directly; the compiler will accept the call, but the linker will not without it.
Keep Learning
If you want to go further with the Native API and virtual memory internals behind this tool, these TrainSec courses go deeper:
- Windows Native API Programming: covers
NtQueryInformationProcessand the rest of the native API surface, including the PEB and TEB, in full - Windows System Programming 2: covers the virtual memory APIs this tool is built on, including heaps and memory-mapped files
Related reading in the knowledge library: VMMap Basics, the GUI-first walkthrough this video builds on, and Windows Privileges: SeDebugPrivilege and AdjustTokenPrivileges, on why some processes stay out of reach no matter which access mask you request.