Jump to content
Nytro

Exploring Windows virtual memory management

Recommended Posts

Exploring Windows virtual memory management

 
 
In a previous post, we discussed the IA-32e 64-bit paging structures, and how they can be used to turn virtual addresses into physical addresses. They're a simple but elegant way to manage virtual address mappings as well as page permissions with varying granularity of page sizes. All of which is provided by the architecture. But as one might expect, once you add an operating system like Windows into the mix, things get a little more interesting.
 

The problem of per-process memory

In Windows, a process is nothing more than a simple container of threads and metadata that represents a user-mode application. It has its own memory so that it can manage the different pieces of data and code that make the process do something useful. Let's consider, then, two processes that both try to read and write from the memory located at the virtual address 0x00000000`11223344. Based on what we know about paging, we expect that the virtual address is going to end up translating into the same physical address (let's say 0x00000001`ff003344 as an example) in both processes. There is, after all, only one CR3 register per processor, and the hardware dictates that the paging structure root is located in that register.
 
tf-article2-1.png
Figure 1: If the two process' virtual addresses would translate to the same physical address, then we expect that they would both see the same memory, right?
 
Of course, in reality we know that it can't work that way. If we use one process to write to a virtual memory address, and then use another process to read from that address, we shouldn't get the same value. That would be devastating from a security and stability standpoint. In fact, the same permissions may not even be applied to that virtual memory in both processes.
 
But how does Windows accomplish this separation? It's actually pretty straightforward: when switching threads in kernel-mode or user-mode (called a context switch), Windows stores off or loads information about the current thread including the state of all of the registers. Because of this, Windows is able to swap out the root of the paging structures when the thread context is switched by changing the value of CR3, effectively allowing it to manage an entirely separate set of paging structures for each process on the system. This gives each process a unique mapping of virtual memory to physical memory, while still using the same virtual address ranges as another process. The PML4 table pointer for each user-mode process is stored in the DirectoryTableBase member of an internal kernel structure called the EPROCESS, which also manages a great deal of other state and metadata about the process.
 
tf-article2-2.png
Figure 2: In reality, each process has its own set of paging structures, and Windows swaps out the value of the CR3 register when it executes within that process. This allows virtual addresses in each process to map to different physical addresses.
 
We can see the paging structure swap between processes for ourselves if we do a little bit of exploration using WinDbg. If you haven't already set up kernel debugging, you should check out this article to get yourself started. Then follow along below.
 
Let's first get a list of processes running on the target system. We can do that using the !process command. For more details on how to use this command, consider checking out the documentation using .hh !process. In our case, we pass parameters of zero to show all processes on the system.
 
0: kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
PROCESS fffffa801916b5d0
SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000
DirBase: 00187000 ObjectTable: fffff8a000001890 HandleCount: 560.
Image: System
 
PROCESS fffffa8028effb10
SessionId: none Cid: 0130 Peb: 7fffffd8000 ParentCid: 0004
DirBase: 7d9ed5000 ObjectTable: fffff8a000174d80 HandleCount: 36.
Image: smss.exe
 
PROCESS fffffa802949bb10
SessionId: 0 Cid: 01b8 Peb: 7fffffdf000 ParentCid: 0174
DirBase: 7cf890000 ObjectTable: fffff8a000b82010 HandleCount: 713.
Image: csrss.exe
 
...
 
PROCESS fffffa8019218b10
SessionId: 1 Cid: 02f0 Peb: 7fffffd5000 ParentCid: 0808
DirBase: 652e89000 ObjectTable: fffff8a00cacc270 HandleCount: 58.
Image: notepad.exe
 
We can use notepad.exe as our target process, but you should be able to follow along with virtually any process of your choice. The next thing we need to do is attach ourselves to this process - simply put, we need to be in this process' context. This lets us access the virtual memory of notepad.exe by remapping the paging structures. We can verify that the context switch is happening by watching what happens to the CR3 register. If the virtual memory we have access to is going to change, we expect that the value of CR3 will change to new paging structures that represent notepad.exe's virtual memory. Let's take a look at the value of CR3 before the context switch.
 
0: kd> r cr3
cr3=000000078b07a000


We know that this value should change to the DirectoryTableBase member of the EPROCESS structure that represents notepad.exe when we make the switch. As a matter of interest, we can take a look at that structure and see what it contains. The PROCESS fffffa8019218b10 line emitted by the debugger when we listed all processes is actually the virtual address of that process' EPROCESS structure.
 

0: kd> dt nt!_EPROCESS fffffa8019218b10 -b
+0x000 Pcb : _KPROCESS
+0x000 Header : _DISPATCHER_HEADER
+0x000 Type : 0x3 ''
+0x001 TimerControlFlags : 0 ''
+0x001 Absolute : 0y0
+0x001 Coalescable : 0y0
+0x001 KeepShifting : 0y0
+0x001 EncodedTolerableDelay : 0y00000 (0)
+0x001 Abandoned : 0 ''
+0x001 Signalling : 0 ''
+0x002 ThreadControlFlags : 0x58 'X'
+0x002 CpuThrottled : 0y0
+0x002 CycleProfiling : 0y0
+0x002 CounterProfiling : 0y0
+0x002 Reserved : 0y01011 (0xb)
+0x002 Hand : 0x58 'X'
+0x002 Size : 0x58 'X'
+0x003 TimerMiscFlags : 0 ''
+0x003 Index : 0y000000 (0)
+0x003 Inserted : 0y0
+0x003 Expired : 0y0
+0x003 DebugActive : 0 ''
+0x003 ActiveDR7 : 0y0
+0x003 Instrumented : 0y0
+0x003 Reserved2 : 0y0000
+0x003 UmsScheduled : 0y0
+0x003 UmsPrimary : 0y0
+0x003 DpcActive : 0 ''
+0x000 Lock : 0n5767171
+0x004 SignalState : 0n0
+0x008 WaitListHead : _LIST_ENTRY [ 0xfffffa80`19218b18 - 0xfffffa80`19218b18 ]
+0x000 Flink : 0xfffffa80`19218b18
+0x008 Blink : 0xfffffa80`19218b18
+0x018 ProfileListHead : _LIST_ENTRY [ 0xfffffa80`19218b28 - 0xfffffa80`19218b28 ]
+0x000 Flink : 0xfffffa80`19218b28
+0x008 Blink : 0xfffffa80`19218b28
+0x028 DirectoryTableBase : 0x00000006`52e89000
...


The fully expanded EPROCESS structure is massive, so everything after what we're interested in has been omitted from the results above. We can see, though, that the DirectoryTableBase is a member at +0x028 of the process control block (KPROCESS) structure that's embedded as part of the larger EPROCESS structure.

According to this output, we should expect that CR3 will change to 0x00000006`52e89000 when we switch to this process' context in WinDbg.

To perform the context swap, we use the .process command and indicate that we want an invasive swap (/i) which will remap the virtual address space and allow us to do things like set breakpoints in user-mode memory. Also, in order for the process context swap to complete, we need to allow the process to execute once using the g command. The debugger will then break again, and we're officially in the context of notepad.exe.
 

0: kd> .process /i /P fffffa8019218b10
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
0: kd> g
Break instruction exception - code 80000003 (first chance)
nt!DbgBreakPointWithStatus:
fffff800`02a73c70 cc int 3


Okay! Now that we're in the context we need to be in, let's check the CR3 register to verify that the paging structures have been changed to the DirectoryTableBase member we saw earlier.
 

6: kd> r cr3
cr3=0000000652e89000

Looks like it worked as we expected. We would find a unique set of paging structures at 0x00000006`52e89000 that represented the virtual to physical address mappings within notepad.exe. This is essentially the same kind of swap that occurs each time Windows switches to a thread in a different process.
 

Virtual address ranges

While each process gets its own view of virtual memory and can re-use the same virtual address range as another process, there are some consistent rules of thumb that Windows abides by when it comes to which virtual address ranges store certain kinds of information.

To start, each user-mode process is allowed a user-mode virtual address space ranging from 0x000`00000000 to 0x7ff`ffffffff, giving each process a theoretical maximum of 8TB of virtual memory that it can access. Then, each process also has a range of kernel-mode virtual memory that is split up into a number of different subsections. This much larger range gives the kernel a theoretical maximum of 248TB of virtual memory, ranging from 0xffff0800`00000000 to 0xffffffff`ffffffff. The remaining address space is not actually used by Windows, though, as we can see below.
 
tf-article2-3-revised.png
 
Figure 3: All possible virtual memory, divided into the different ranges that Windows enforces. The virtual addresses for the kernel-mode regions may not be true on Windows 10, where these regions are subject to address space layout randomization (ASLR). Credits to Alex Ionescu for specific kernel space mappings.

Currently, there is an extremely large “no man's land” of virtual memory space between the user-mode and kernel-mode ranges of virtual memory. This range of memory isn't wasted, though, it's just not addressable due to the current architecture constraint of 48-bit virtual addresses, which we discussed in our previous article. If there existed a system with 16EB of physical memory - enough memory to address all possible 64-bit virtual memory - the extra physical memory would simply be used to hold the pages of other processes, so that many processes' memory ranges could be resident in physical memory at once.

As an aside, one other interesting property of the way Windows handles virtual address mapping is being able to quickly tell kernel pointers from user-mode pointers. Memory that is mapped as part of the kernel has the highest order bits of the address (the 16 bits we didn't use as part of the linear address translation) set to 1, while user-mode memory has them set to 0. This ensures that kernel-mode pointers begin with 0xFFFF and user-mode pointers begin with 0x0000.
 

A tree of virtual memory: the VAD

We can see that the kernel-mode virtual memory is nicely divided into different sections. But what about user-mode memory? How does the memory manager know which portions of virtual memory have been allocated, which haven't, and details about each of those ranges? How can it know if a virtual address within a process is valid or invalid? It could walk the process' paging structures to figure this out every time the information was needed, but there is another way: the virtual address descriptor (VAD) tree.

Each process has a VAD tree that can be located in the VadRoot member of the aforementioned EPROCESS structure. The tree is a balanced binary search tree, with each node representing a region of virtual memory within the process.
 
tf-article2-4.png
Figure 4: The VAD tree is balanced with lower virtual page numbers to the left, and each node providing some additional details about the memory range.
 
Each node gives details about the range of addresses, the memory protection of that region, and some other metadata depending on the state of the memory it is representing.
 
We can use our friend WinDbg to easily list all of the entries in the VAD tree of a particular process. Let's have a look at the VAD entries from notepad.exe using !vad.
 
6: kd> !vad
VAD Level Start End Commit
fffffa8019785170 5 10 1f 0 Mapped READWRITE Pagefile section, shared commit 0x10
fffffa8019229650 4 20 26 0 Mapped READONLY Pagefile section, shared commit 0x7
fffffa802aec35c0 5 30 33 0 Mapped READONLY Pagefile section, shared commit 0x4
fffffa80291085a0 3 40 41 0 Mapped READONLY Pagefile section, shared commit 0x2
fffffa802b25c180 5 50 50 1 Private READWRITE
fffffa802b0b8940 4 60 c6 0 Mapped READONLY \Windows\System32\locale.nls
fffffa8019544940 5 d0 d1 0 Mapped READWRITE Pagefile section, shared commit 0x2
fffffa80193c5570 2 e0 e2 3 Mapped WRITECOPY \Windows\System32\en-US\notepad.exe.mui
fffffa802b499e00 5 f0 f0 1 Private READWRITE
fffffa802b4a6160 4 100 100 1 Private READWRITE
fffffa801954d3a0 5 110 110 0 Mapped READWRITE Pagefile section, shared commit 0x1
fffffa80197cf8c0 3 120 121 0 Mapped READONLY Pagefile section, shared commit 0x2
fffffa802b158240 4 160 16f 2 Private READWRITE
fffffa802b24f180 1 1a0 21f 20 Private READWRITE
fffffa802b1fc680 6 220 31f 104 Private READWRITE
fffffa802b44d110 5 320 41f 146 Private READWRITE
fffffa802910ece0 6 420 4fe 0 Mapped READONLY Pagefile section, shared commit 0xdf
fffffa802b354c60 4 540 54f 7 Private READWRITE
fffffa8029106660 6 550 6d7 0 Mapped READONLY Pagefile section, shared commit 0x6
fffffa802b4738b0 5 6e0 860 0 Mapped READONLY Pagefile section, shared commit 0x181
fffffa802942ea30 6 870 1c6f 0 Mapped READONLY Pagefile section, shared commit 0x23
fffffa802b242260 3 1cf0 1d6f 28 Private READWRITE
fffffa802aa66d60 5 1e10 1e8f 113 Private READWRITE
fffffa8019499560 4 3030 395f 0 Mapped READONLY \Windows\Fonts\StaticCache.dat
fffffa8019246370 5 3960 3c2e 0 Mapped READONLY \Windows\Globalization\Sorting\SortDefault.nls
fffffa802b184c50 6 3c30 3d2f 1 Private READWRITE
fffffa802b45f180 2 77420 77519 3 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\user32.dll
fffffa80192afa20 4 77520 7763e 4 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\kernel32.dll
fffffa802a8ba9c0 3 77640 777e9 14 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\ntdll.dll
fffffa802910a440 5 7efe0 7f0df 0 Mapped READONLY Pagefile section, shared commit 0x5
fffffa802b26d180 4 7f0e0 7ffdf 0 Private READONLY
fffffa802b4cb160 0 7ffe0 7ffef -1 Private READONLY
fffffa802b4e60d0 5 ffd50 ffd84 4 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\notepad.exe
fffffa801978d170 4 7fefa530 7fefa5a0 3 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\winspool.drv
fffffa80197e0970 5 7fefaab0 7fefab05 4 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\uxtheme.dll
fffffa802a6d9720 6 7fefafd0 7fefafe7 5 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\dwmapi.dll
fffffa80197ecc50 3 7fefb390 7fefb583 6 Mapped Exe EXECUTE_WRITECOPY \Windows\winsxs\amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.7601.18837_none_fa3b1e3d17594757\comctl32.dll
fffffa802a91e010 5 7fefc3c0 7fefc3cb 2 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\version.dll
fffffa80197cb010 6 7fefd1d0 7fefd1de 2 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\cryptbase.dll
fffffa80290fe9c0 4 7fefd440 7fefd4a9 3 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\KernelBase.dll
fffffa8029109e30 5 7fefd6f0 7fefd6fd 2 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\lpk.dll
fffffa8029522520 6 7fefd720 7fefd74d 3 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\imm32.dll
fffffa802910bce0 2 7fefd800 7fefd8da 7 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\advapi32.dll
fffffa80290d9500 5 7fefd8e0 7fefdadb 9 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\ole32.dll
fffffa802af4a0c0 4 7fefdae0 7fefe86a 13 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\shell32.dll
fffffa8019787170 5 7fefea50 7fefea6e 4 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\sechost.dll
fffffa802a6e8010 3 7fefeda0 7fefee36 6 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\comdlg32.dll
fffffa802a6ae010 5 7fefee50 7fefef58 4 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\msctf.dll
fffffa802910dac0 4 7feff0f0 7feff21c 3 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\rpcrt4.dll
fffffa801948e940 1 7feff2a0 7feff33e 7 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\msvcrt.dll
fffffa802aac1010 5 7feff340 7feff3b0 3 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\shlwapi.dll
fffffa8029156010 4 7feff3c0 7feff48a 4 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\usp10.dll
fffffa801956e170 5 7feff800 7feff8d9 4 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\oleaut32.dll
fffffa8019789170 3 7feff8e0 7feff946 3 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\gdi32.dll
fffffa801958e170 4 7feff960 7feff960 0 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\apisetschema.dll
fffffa80290f3c10 2 7fffffb0 7fffffd2 0 Mapped READONLY Pagefile section, shared commit 0x23
fffffa802af28110 3 7fffffd5 7fffffd5 1 Private READWRITE
fffffa802a714a30 4 7fffffde 7fffffdf 2 Private READWRITE
 
Total VADs: 58, average level: 5, maximum depth: 6
Total private commit: 0x228 pages (2208 KB)
Total shared commit: 0x2d3 pages (2892 KB)
 
The range of addresses supported by a given VAD entry are stored as virtual page numbers - similar to a PFN, but simply in virtual memory. This means that an entry representing a starting VPN of 0x7f and an ending VPN of 0x8f would actually be representing virtual memory from address 0x00000000`0007f000 to 0x00000000`0008ffff.

There are a number of complexities of the VAD tree that are outside the scope of this article. For example, each node in the tree can be one of three different types depending on the state of the memory being represented. In addition, a VAD entry may contain information about the backing PTEs for that region of memory if that memory is shared. We will touch more on that concept in a later section.
 

Let's get physical

So we now know that Windows maintains separate paging structures for each individual process, and some details about the different virtual memory ranges that are defined. But the operating system also needs a central mechanism to keep track of each individual page of physical memory. After all, it needs to know what's stored in each physical page, whether it can write that data out to a paging file on disk to free up memory, how many processes are using that page for the purposes of shared memory, and plenty of other details for proper memory management

That's where the page frame number (PFN) database comes in. A pointer to the base of this very large structure can be located at the symbol nt!MmPfnDatabase, but we know based on the kernel-mode memory ranges that it starts at the virtual address 0xfffffa80`00000000, except on Windows 10 where this is subject to ASLR. (As an aside, WinDbg has a neat extension for dealing with the kernel ASLR in Windows 10 - !vm 0x21 will get you the post-KASLR regions). For each physical page available on the system, there is an nt!_MMPFN structure allocated in the database to provide details about the page.
 
tf-article2-5.png
Figure 5: Each physical page in the system is represented by a PFN entry structure in this very large, contiguous data structure.
 
Though some of the bits of the nt!_MMPFN structure can vary depending on the state of the page, that structure generally looks something like this:
 
0: kd> dt nt!_MMPFN
+0x000 u1 : <unnamed-tag>
+0x008 u2 : <unnamed-tag>
+0x010 PteAddress : Ptr64 _MMPTE
+0x010 VolatilePteAddress : Ptr64 Void
+0x010 Lock : Int4B
+0x010 PteLong : Uint8B
+0x018 u3 : <unnamed-tag>
+0x01c UsedPageTableEntries : Uint2B
+0x01e VaType : UChar
+0x01f ViewCount : UChar
+0x020 OriginalPte : _MMPTE
+0x020 AweReferenceCount : Int4B
+0x028 u4 : <unnamed-tag>
 
A page represented in the PFN database can be in a number of different states. The state of the page will determine what the memory manager does with the contents of that page.

We won't be focusing on the different states too much in this article, but there are a few of them: active, transition, modified, free, and bad, to name several. It is definitely worth mentioning that for efficiency reasons, Windows manages linked lists that are comprised of all of the nt!_MMPFN entries that are in a specific state. This makes it much easier to traverse all pages that are in a specific state, rather than having to walk the entire PFN database. For example, it can allow the memory manager to quickly locate all of the free pages when memory needs to be paged in from disk.
 
tf-article2-6.png
Figure 6: Different linked lists make it easier to walk the PFN database according to the state of the pages, e.g. walk all of the free pages contiguously.

Another purpose of the PFN database is to help facilitate the translation of physical addresses back to their corresponding virtual addresses. Windows uses the PFN database to accomplish this during calls such as nt!MmGetVirtualForPhysical. While it is technically possible to search all of the paging structures for every process on the system in order to work backwards up the paging structures to get the original virtual address, the fact that the nt!_MMPFN structure contains a reference to the backing PTE coupled with some clever allocation rules by Microsoft allow them to easily convert back to a virtual address using the PTE and some bit shifting.

For a little bit of practical experience exploring the PFN database, let's find a region of memory in notepad.exe that we can take a look at. One area of memory that could be of interest is the entry point of our application. We can use the !dh command to display the PE header information associated with a given module in order to track down the address of the entry point.

Because we've switched into a user-mode context in one of our previous examples, WinDbg will require us to reload our symbols so that it can make sense of everything again. We can do that using the .reload /f command. Then we can look at notepad.exe's headers:
 
6: kd> !dh notepad.exe
 
File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
8664 machine (X64)
6 number of sections
559EA8BE time date stamp Thu Jul 9 10:00:46 2015
 
0 file pointer to symbol table
0 number of symbols
F0 size of optional header
22 characteristics
Executable
App can handle >2gb addresses
 
OPTIONAL HEADER VALUES
20B magic #
9.00 linker version
A800 size of code
25800 size of initialized data
0 size of uninitialized data
3ACC address of entry point
1000 base of code
----- new -----
00000000ffd50000 image base
1000 section alignment
200 file alignment
2 subsystem (Windows GUI)
6.01 operating system version
6.01 image version
6.01 subsystem version
35000 size of image
600 size of headers
36DA2 checksum
0000000000080000 size of stack reserve
0000000000011000 size of stack commit
0000000000100000 size of heap reserve
0000000000001000 size of heap commit
8140 DLL characteristics
Dynamic base
NX compatible
Terminal server aware
0 [ 0] address of Export Directory
CFF8 [ 12C] address of Import Directory
14000 [ 1F168] address of Resource Directory
13000 [ 6B4] address of Exception Directory
0 [ 0] address of Security Directory
34000 [ B8] address of Base Relocation Directory
B740 [ 38] address of Debug Directory
0 [ 0] address of Description Directory
0 [ 0] address of Special Directory
0 [ 0] address of Thread Storage Directory
0 [ 0] address of Load Configuration Directory
2E0 [ 138] address of Bound Import Directory
C000 [ 7F0] address of Import Address Table Directory
0 [ 0] address of Delay Import Directory
0 [ 0] address of COR20 Header Directory
0 [ 0] address of Reserved Directory


Again, the output is quite verbose, so the section information at the bottom is omitted from the above snippet. We're interested in the address of entry point member of the optional header, which is listed as 0x3acc. That value is called a relative virtual address (RVA), and it's the number of bytes from the base address of the notepad.exe image. If we add that relative address to the base of notepad.exe, we should see the code located at our entry point.
 

6: kd> u notepad.exe + 0x3acc L10
notepad!WinMainCRTStartup:
00000000`ffd53acc 4883ec28 sub rsp,28h
00000000`ffd53ad0 e80bf3ffff call notepad!_security_init_cookie (00000000`ffd52de0)
00000000`ffd53ad5 4883c428 add rsp,28h
00000000`ffd53ad9 eb09 jmp notepad!DisplayNonGenuineDlgWorker+0x14c (00000000`ffd53ae4)
00000000`ffd53adb 90 nop
00000000`ffd53adc 90 nop
00000000`ffd53add 90 nop
00000000`ffd53ade 90 nop
00000000`ffd53adf 90 nop
00000000`ffd53ae0 90 nop
00000000`ffd53ae1 90 nop
00000000`ffd53ae2 90 nop
00000000`ffd53ae3 90 nop
00000000`ffd53ae4 4889742408 mov qword ptr [rsp+8],rsi
00000000`ffd53ae9 48897c2410 mov qword ptr [rsp+10h],rdi
00000000`ffd53aee 4154 push r12


And we do see that the address resolves to notepad!WinMainCRTStartup, like we expected. Now we have the address of our target process' entry point: 00000000`ffd53acc.

While the above steps were a handy exercise in digging through parts of a loaded image, they weren't actually necessary since we had symbols loaded. We could have simply used the ? qualifier in combination with the symbol notepad!WinMainCRTStartup, as demonstrated below, or gotten the value of a handy pseudo-register that represents the entry point with r $exentry.

 
6: kd> ? notepad!WinMainCRTStartup
Evaluate expression: 4292164300 = 00000000`ffd53acc

In any case, we now have the address of our entry point, which from here on we'll refer to as our “target” or the “target page”. We can now start taking a look at the different paging structures that support our target, as well as the PFN database entry for it.

Let's first take a look at the PFN database. We know the virtual address where this structure is supposed to start, but let's look for it the long way, anyway. We can easily find the beginning of this structure by using the ? qualifier and poi on the symbol name. The poi command treats its parameter as a pointer and retrieves the value located at that pointer.
 
6: kd> ? poi(nt!MmPfnDatabase)
Evaluate expression: -6047313952768 = fffffa80`00000000
 
Knowing that the PFN database begins at 0xfffffa80`00000000, we should be able to index easily to the entry that represents our target page. First we need to figure out the page frame number in physical memory that the target's PTE refers to, and then we can index into the PFN database by that number.

Looking back on what we learned from the previous article, we can grab the PTE information about the target page very easily using the handy !pte command.
 
6: kd> !pte 00000000`ffd53acc
VA 00000000ffd53acc
PXE at FFFFF6FB7DBED000 PPE at FFFFF6FB7DA00018 PDE at FFFFF6FB40003FF0 PTE at FFFFF680007FEA98
contains 02D0000654195867 contains 4D00000654D16867 contains 02F0000654D97867 contains 32C000065207B025
pfn 654195 ---DA--UWEV pfn 654d16 ---DA--UWEV pfn 654d97 ---DA--UWEV pfn 65207b ----A--UREV


The above result would indicate that the backing page frame number for the target is 0x65207b. That should be the index into the PFN database that we'll need to use. Remember that we'll need to multiply that index by the size of an nt!_MMPFN structure, since we're essentially trying to skip that many PFN entries.
 

6: kd> ?? sizeof(nt!_MMPFN)
unsigned int64 0x30
6: kd> dt !_MMPFN (0xfffffa80`00000000 + (0x65207b * 0x30))
nt!_MMPFN
+0x000 u1 : <unnamed-tag>
+0x008 u2 : <unnamed-tag>
+0x010 PteAddress : 0xfffff8a0`09e25a00 _MMPTE
+0x010 VolatilePteAddress : 0xfffff8a0`09e25a00 Void
+0x010 Lock : 0n165829120
+0x010 PteLong : 0xfffff8a0`09e25a00
+0x018 u3 : <unnamed-tag>
+0x01c UsedPageTableEntries : 0
+0x01e VaType : 0 ''
+0x01f ViewCount : 0 ''
+0x020 OriginalPte : _MMPTE
+0x020 AweReferenceCount : 0n-333970336
+0x028 u4 : <unnamed-tag>


This looks like a valid PFN entry. We can verify that we've done everything correctly by first doing the manual calculation to figure out what the address of the PFN entry should be, and then comparing it to where WinDbg thinks it should be.
 

6: kd> ? (0xfffffa80`00000000 + (0x65207b * 0x30))
Evaluate expression: -6046995835120 = fffffa80`12f61710


So based on the above, we know that the nt!_MMPFN entry for the page we're interested in it should be located at 0xfffffa80`12f61710, and we can use a nice shortcut to verify if we're correct. As always in WinDbg, there is an easier way to obtain information from the PFN database. This can be done by using the !pfn command with the page frame number.
 

6: kd> !pfn 0x65207b
PFN 0065207B at address FFFFFA8012F61710
flink 0000032C blink / share count 00000001 pteaddress FFFFF8A009E25A00
reference count 0001 used entry count 0000 Cached color 0 Priority 1
restore pte FA80194CEC180460 containing page 693603 Active P
Shared

Here we can see that WinDbg also indicates that the PFN entry is at 0xfffffa8012f61710, just like our calculation, so it looks like we did that correctly.
 

An interlude about working sets

Phew - we've done some digging around in the PFN database now, and we've seen how each entry in that database stores some information about the physical page itself. Let's take a step back for a moment, back into the world of virtual memory, and talk about working sets.

Each process has what's called a working set, which represents all of the process' virtual memory that is subject to paging and is accessible without incurring a page fault. Some parts of the process' memory may be paged to disk in order to free up RAM, or in a transition state, and therefore accessing those regions of memory will generate a page fault within that process. In layman's terms, a page fault is essentially the architecture indicating that it can't access the specified virtual memory, because the PTEs needed for translation weren't found inside the paging structures, or because the permissions on the PTEs restrict what the application is attempting to do. When a page fault occurs, the page fault handler must resolve it by adding the page back into the process' working set (meaning it also gets added back into the process' paging structures), mapping the page back into memory from disk and then adding it back to the working set, or indicating that the page being accessed is invalid.
 
tf-article2-7.png
Figure 7: An example working set of a process, where some rarely accessed pages were paged out to disk to free up physical memory.

It should be noted that other regions of virtual memory may be accessible to the process which do not appear in the working set, such as Address Windowing Extensions (AWE) mappings or large pages; however, for the purposes of this article we will be focusing on memory that is part of the working set.

Occasionally, Windows will trim the working set of a process in response to (or to avoid) memory pressure on the system, ensuring there is memory available for other processes.

If the working set of a process is trimmed, the pages being trimmed have their backing PTEs marked as “not valid” and are put into a transition state while they await being paged to disk or given away to another process. In the case of a “soft” page fault, the page described by the PTE is actually still resident in physical memory, and the page fault handler can simply mark the PTE as valid again and resolve the fault efficiently. Otherwise, in the case of a “hard” page fault, the page fault handler needs to fetch the contents of the page from the paging file on disk before marking the PTE as valid again. If this kind of fault occurs, the page fault handler will likely also have to alter the page frame number that the PTE refers to, since the page isn't likely to be loaded back into the same location in physical memory that it previously resided in.
 

Sharing is caring

It's important to remember that while two processes do have different paging structures that map their virtual memory to different parts of physical memory, there can be portions of their virtual memory which map to the same physical memory. This concept is called shared memory, and it's actually quite common within Windows. In fact, even in our previous example with notepad.exe's entry point, the page of memory we looked at was shared. Examples of regions in memory that are shared are system modules, shared libraries, and files that are mapped into memory with CreateFileMapping() and MapViewOfFile().

In addition, the kernel-mode portion of a process' memory will also point to the same shared physical memory as other processes, because a shared view of the kernel is typically mapped into every process. Despite the fact that a view of the kernel is mapped into their memory, user-mode applications will not be able to access pages of kernel-mode memory as Windows sets the UserSupervisor bit in the kernel-mode PTEs. The hardware uses this bit to enforce ring0-only access to those pages.
 
tf-article2-8.png
Figure 8: Two processes may have different views of their user space virtual memory, but they get a shared view of the kernel space virtual memory.

In the case of memory that is not shared between processes, the PFN database entry for that page of memory will point to the appropriate PTE in the process that owns that memory.
 
 
tf-article2-9.png
Figure 9: When not sharing memory, each process will have PTE for a given page, and that PTE will point to a unique member of the PFN database.

When dealing with memory that is shareable, Windows creates a kind of global PTE - known as a prototype PTE - for each page of the shared memory. This prototype always represents the real state of the physical memory for the shared page. If marked as Valid, this prototype PTE can act as a hardware PTE just as in any other case. If marked as Not Valid, the prototype will indicate to the page fault handler that the memory needs to be paged back in from disk. When a prototype PTE exists for a given page of memory, the PFN database entry for that page will always point to the prototype PTE.
 
tf-article2-10.png
Figure 10: Even though both processes still have a valid PTE pointing to their shared memory, Windows has created a prototype PTE which points to the PFN entry, and the PFN entry now points to the prototype PTE instead of a specific process.

Why would Windows create this special PTE for shared memory? Well, imagine for a moment that in one of the processes, the PTE that describes a shared memory location is stripped out of the process' working set. If the process then tries to access that memory, the page fault handler sees that the PTE has been marked as Not Valid, but it has no idea whether that shared page is still resident in physical memory or not.

For this, it uses the prototype PTE. When the PTE for the shared page within the process is marked as Not Valid, the Prototype bit is also set and the page frame number is set to the location of the prototype PTE for that page.
 
tf-article2-11.png
Figure 11: One of the processes no longer has a valid PTE for the shared memory, so Windows instead uses the prototype PTE to ascertain the true state of the physical page.
 
This way, the page fault handler is able to examine the prototype PTE to see if the physical page is still valid and resident or not. If it is still resident, then the page fault handler can simply mark the process' version of the PTE as valid again, resolving the soft fault. If the prototype PTE indicates it is Not Valid, then the page fault handler must fetch the page from disk.
 
We can continue our adventures in WinDbg to explore this further, as it can be a tricky concept. Based on what we know about shared memory, that should mean that the PTE referenced by the PFN entry for the entry point of notepad.exe is a prototype PTE. We can already see that it's a different address (0xfffff8a0`09e25a00) than the PTE that we were expecting from the !pte command (0xfffff680007fea98). Let's look at the fully expanded nt!_MMPTE structure that's being referenced in the PFN entry.
 
6: kd> dt !_MMPTE 0xfffff8a0`09e25a00 -b
nt!_MMPTE
+0x000 u : <unnamed-tag>
+0x000 Long : 0x00000006`5207b121
+0x000 VolatileLong : 0x00000006`5207b121
+0x000 Hard : _MMPTE_HARDWARE
+0x000 Valid : 0y1
+0x000 Dirty1 : 0y0
+0x000 Owner : 0y0
+0x000 WriteThrough : 0y0
+0x000 CacheDisable : 0y0
+0x000 Accessed : 0y1
+0x000 Dirty : 0y0
+0x000 LargePage : 0y0
+0x000 Global : 0y1
+0x000 CopyOnWrite : 0y0
+0x000 Unused : 0y0
+0x000 Write : 0y0
+0x000 PageFrameNumber : 0y000000000000011001010010000001111011 (0x65207b)
+0x000 reserved1 : 0y0000
+0x000 SoftwareWsIndex : 0y00000000000 (0)
+0x000 NoExecute : 0y0
...
+0x000 Proto : _MMPTE_PROTOTYPE
+0x000 Valid : 0y1
+0x000 Unused0 : 0y0010000 (0x10)
+0x000 ReadOnly : 0y1
+0x000 Unused1 : 0y0
+0x000 Prototype : 0y0
+0x000 Protection : 0y10110 (0x16)
+0x000 ProtoAddress : 0y000000000000000000000000000001100101001000000111 (0x65207)
...
 
We can compare that with the nt!_MMPTE entry that was referenced when we did the !pte command on notepad.exe's entry point.
 
6: kd> dt nt!_MMPTE 0xfffff680007fea98 -b
+0x000 u : <unnamed-tag>
+0x000 Long : 0x32c00006`5207b025
+0x000 VolatileLong : 0x32c00006`5207b025
+0x000 Hard : _MMPTE_HARDWARE
+0x000 Valid : 0y1
+0x000 Dirty1 : 0y0
+0x000 Owner : 0y1
+0x000 WriteThrough : 0y0
+0x000 CacheDisable : 0y0
+0x000 Accessed : 0y1
+0x000 Dirty : 0y0
+0x000 LargePage : 0y0
+0x000 Global : 0y0
+0x000 CopyOnWrite : 0y0
+0x000 Unused : 0y0
+0x000 Write : 0y0
+0x000 PageFrameNumber : 0y000000000000011001010010000001111011 (0x65207b)
+0x000 reserved1 : 0y0000
+0x000 SoftwareWsIndex : 0y01100101100 (0x32c)
+0x000 NoExecute : 0y0
...
+0x000 Proto : _MMPTE_PROTOTYPE
+0x000 Valid : 0y1
+0x000 Unused0 : 0y0010010 (0x12)
+0x000 ReadOnly : 0y0
+0x000 Unused1 : 0y0
+0x000 Prototype : 0y0
+0x000 Protection : 0y10110 (0x16)
+0x000 ProtoAddress : 0y001100101100000000000000000001100101001000000111 (0x32c000065207)
...

It looks like the Prototype bit is not set on either of them, and they're both valid. This makes perfect sense. The shared page still belongs to notepad.exe's working set, so the PTE in the process' paging structures is still valid; however, the operating system has proactively allocated a prototype PTE for it because the memory may be shared at some point and the state of the page will need to be tracked with the prototype PTE. The notepad.exe paging structures also point to a valid hardware PTE, just not the same one as the PFN database entry.

The same isn't true for a region of memory that can't be shared. For example, if we choose another memory location that was allocated as MEM_PRIVATE, we will not see the same results. We can use the !vad command to give us all of the virtual address regions (listed by virtual page frame) that are mapped by the current process.
 
6: kd> !vad
VAD Level Start End Commit
fffffa8019785170 5 10 1f 0 Mapped READWRITE Pagefile section, shared commit 0x10
fffffa8019229650 4 20 26 0 Mapped READONLY Pagefile section, shared commit 0x7
fffffa802aec35c0 5 30 33 0 Mapped READONLY Pagefile section, shared commit 0x4
fffffa80291085a0 3 40 41 0 Mapped READONLY Pagefile section, shared commit 0x2
fffffa802b25c180 5 50 50 1 Private READWRITE
fffffa802b0b8940 4 60 c6 0 Mapped READONLY \Windows\System32\locale.nls
fffffa8019544940 5 d0 d1 0 Mapped READWRITE Pagefile section, shared commit 0x2
fffffa80193c5570 2 e0 e2 3 Mapped WRITECOPY \Windows\System32\en-US\notepad.exe.mui
fffffa802b499e00 5 f0 f0 1 Private READWRITE
fffffa802b4a6160 4 100 100 1 Private READWRITE
fffffa801954d3a0 5 110 110 0 Mapped READWRITE Pagefile section, shared commit 0x1
fffffa80197cf8c0 3 120 121 0 Mapped READONLY Pagefile section, shared commit 0x2
fffffa802b158240 4 160 16f 2 Private READWRITE
fffffa802b24f180 1 1a0 21f 20 Private READWRITE
fffffa802b1fc680 6 220 31f 104 Private READWRITE
fffffa802b44d110 5 320 41f 146 Private READWRITE
fffffa802910ece0 6 420 4fe 0 Mapped READONLY Pagefile section, shared commit 0xdf
fffffa802b354c60 4 540 54f 7 Private READWRITE
fffffa8029106660 6 550 6d7 0 Mapped READONLY Pagefile section, shared commit 0x6
fffffa802b4738b0 5 6e0 860 0 Mapped READONLY Pagefile section, shared commit 0x181
fffffa802942ea30 6 870 1c6f 0 Mapped READONLY Pagefile section, shared commit 0x23
fffffa802b242260 3 1cf0 1d6f 28 Private READWRITE
fffffa802aa66d60 5 1e10 1e8f 113 Private READWRITE
fffffa8019499560 4 3030 395f 0 Mapped READONLY \Windows\Fonts\StaticCache.dat
fffffa8019246370 5 3960 3c2e 0 Mapped READONLY \Windows\Globalization\Sorting\SortDefault.nls
fffffa802b184c50 6 3c30 3d2f 1 Private READWRITE
fffffa802b45f180 2 77420 77519 3 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\user32.dll
fffffa80192afa20 4 77520 7763e 4 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\kernel32.dll
fffffa802a8ba9c0 3 77640 777e9 14 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\ntdll.dll
fffffa802910a440 5 7efe0 7f0df 0 Mapped READONLY Pagefile section, shared commit 0x5
fffffa802b26d180 4 7f0e0 7ffdf 0 Private READONLY
fffffa802b4cb160 0 7ffe0 7ffef -1 Private READONLY
fffffa802b4e60d0 5 ffd50 ffd84 4 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\notepad.exe
fffffa801978d170 4 7fefa530 7fefa5a0 3 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\winspool.drv
fffffa80197e0970 5 7fefaab0 7fefab05 4 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\uxtheme.dll
fffffa802a6d9720 6 7fefafd0 7fefafe7 5 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\dwmapi.dll
fffffa80197ecc50 3 7fefb390 7fefb583 6 Mapped Exe EXECUTE_WRITECOPY \Windows\winsxs\amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.7601.18837_none_fa3b1e3d17594757\comctl32.dll
fffffa802a91e010 5 7fefc3c0 7fefc3cb 2 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\version.dll
fffffa80197cb010 6 7fefd1d0 7fefd1de 2 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\cryptbase.dll
fffffa80290fe9c0 4 7fefd440 7fefd4a9 3 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\KernelBase.dll
fffffa8029109e30 5 7fefd6f0 7fefd6fd 2 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\lpk.dll
fffffa8029522520 6 7fefd720 7fefd74d 3 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\imm32.dll
fffffa802910bce0 2 7fefd800 7fefd8da 7 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\advapi32.dll
fffffa80290d9500 5 7fefd8e0 7fefdadb 9 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\ole32.dll
fffffa802af4a0c0 4 7fefdae0 7fefe86a 13 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\shell32.dll
fffffa8019787170 5 7fefea50 7fefea6e 4 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\sechost.dll
fffffa802a6e8010 3 7fefeda0 7fefee36 6 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\comdlg32.dll
fffffa802a6ae010 5 7fefee50 7fefef58 4 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\msctf.dll
fffffa802910dac0 4 7feff0f0 7feff21c 3 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\rpcrt4.dll
fffffa801948e940 1 7feff2a0 7feff33e 7 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\msvcrt.dll
fffffa802aac1010 5 7feff340 7feff3b0 3 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\shlwapi.dll
fffffa8029156010 4 7feff3c0 7feff48a 4 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\usp10.dll
fffffa801956e170 5 7feff800 7feff8d9 4 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\oleaut32.dll
fffffa8019789170 3 7feff8e0 7feff946 3 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\gdi32.dll
fffffa801958e170 4 7feff960 7feff960 0 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\apisetschema.dll
fffffa80290f3c10 2 7fffffb0 7fffffd2 0 Mapped READONLY Pagefile section, shared commit 0x23
fffffa802af28110 3 7fffffd5 7fffffd5 1 Private READWRITE
fffffa802a714a30 4 7fffffde 7fffffdf 2 Private READWRITE
 
Total VADs: 58, average level: 5, maximum depth: 6
Total private commit: 0x228 pages (2208 KB)
Total shared commit: 0x2d3 pages (2892 KB)
 
We can take a look at a MEM_PRIVATE page, such as 0x1cf0, and see if the PTE from the process' paging structures matches the PTE from the PFN database.
 
6: kd> ? 1cf0 * 0x1000
Evaluate expression: 30343168 = 00000000`01cf0000
6: kd> !pte 00000000`01cf0000
VA 0000000001cf0000
PXE at FFFFF6FB7DBED000 PPE at FFFFF6FB7DA00000 PDE at FFFFF6FB40000070 PTE at FFFFF6800000E780
contains 02D0000654195867 contains 0320000656E18867 contains 4F20000653448867 contains CF30000651EC9867
pfn 654195 ---DA--UWEV pfn 656e18 ---DA--UWEV pfn 653448 ---DA--UWEV pfn 651ec9 ---DA--UW-V
 
6: kd> !pfn 651ec9
PFN 00651EC9 at address FFFFFA8012F5C5B0
flink 000004F3 blink / share count 00000001 pteaddress FFFFF6800000E780
reference count 0001 used entry count 0000 Cached color 0 Priority 5
restore pte 00000080 containing page 653448 Active M
Modified

As we can see, it does match, with both addresses referring to 0xfffff680`0000e780. Because this memory is not shareable, the process' paging structures are able to manage the hardware PTE directly. In the case of shareable pages allocated with MEM_MAPPED, though, the PFN database maintains its own copy of the PTE.

It's worth exploring different regions of memory this way, just to see how the paging structures and PFN entries are set up in different cases. As mentioned above, the VAD tree is another important consideration when dealing with user-mode memory as in many cases, it will actually be a VAD node which indicates where the prototype PTE for a given shared memory region resides. In these cases, the page fault handler will need to refer to the process' VAD tree and walk the tree until it finds the node responsible for the shared memory region.
 
tf-article2-12.png
Figure 12: If the invalid PTE points to the process' VAD tree, a VAD walk must be performed to locate the appropriate _MMVAD node that represents the given virtual memory.

The FirstPrototypePte member of the VAD node will indicate the starting virtual address of a region of memory that contains prototype PTEs for each shared page in the region. The list of prototype PTEs is terminated with the LastContiguousPte member of the VAD node. The page fault handler must then walk this list of prototype PTEs to find the PTE that backs the specific page that has faulted.
 
tf-article2-13.png
Figure 13: The FirstPrototypePte member of the VAD node points to a region of memory that has a contiguous block of prototype PTEs that represent shared memory within that virtual address range.
 

One more example to bring it all together

It would be helpful to walk through each of these scenarios with a program that we control, and that we can change, if needed. That's precisely what we're going to do with the memdemo project. You can follow along by compiling the application yourself, or you can simply take a look at the code snippets that will be posted throughout this example.

To start off, we'll load our memdemo.exe and then attach the kernel debugger. We then need to get a list of processes that are currently running on the system.
 
kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
PROCESS ffffa50dcea99040
SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000
DirBase: 001aa000 ObjectTable: ffffd385360012c0 HandleCount: 2376.
Image: System
 
PROCESS ffffa50dcee27140
SessionId: none Cid: 01f4 Peb: dbfb731000 ParentCid: 0004
DirBase: 138ab8000 ObjectTable: ffffd38536339d40 HandleCount: 52.
Image: smss.exe
 
PROCESS ffffa50dcfb467c0
SessionId: 0 Cid: 0248 Peb: 5faaf3c000 ParentCid: 0240
DirBase: 138f83000 ObjectTable: ffffd385363d0f00 HandleCount: 531.
Image: csrss.exe
 
...
 
PROCESS ffffa50dd1070380
SessionId: 1 Cid: 097c Peb: 6f29798000 ParentCid: 02b4
DirBase: 1500d000 ObjectTable: ffffd3853d7ed380 HandleCount: 37.
Image: memdemo.exe
 
Let's quickly switch back to the application so that we can let it create our initial buffer. To do this, we're simply allocating some memory and then accessing it to make sure it's resident.
 
// Allocate a buffer within our process.
PVOID Private = VirtualAlloc(NULL, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
 
// Use the memory to make sure it's resident.
reinterpret_cast<PBYTE>(Private)[0] = 0xFF;


Upon running the code, we see that the application has created a buffer for us (in the current example) at 0x000001fe`151c0000. Your buffer may differ.

We should hop back into our debugger now and check out that memory address. As mentioned before, it's important to remember to switch back into the process context of memdemo.exe when we break back in with the debugger. We have no idea what context we could have been in when we interrupted execution, so it's important to always do this step.
 

kd> .process /i /P ffffa50dd1070380
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
kd> g
Break instruction exception - code 80000003 (first chance)
nt!DbgBreakPointWithStatus:
fffff801`d9df7a40 cc int 3


When we wrote memdemo.exe, we could have used the __debugbreak() compiler intrinsic to avoid having to constantly switch back to our process' context. It would ensure that when the breakpoint was hit, we were already in the correct context. For the purposes of this article, though, it's best to practice swapping back into the correct process context, as during most live analysis we would not have the liberty of throwing int3 exceptions during the program's execution.

We can now check out the memory at 0x000001fe`151c0000 using the db command.
 

kd> db 0x000001fe`151c0000
000001fe`151c0000 ff 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000001fe`151c0010 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000001fe`151c0020 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000001fe`151c0030 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000001fe`151c0040 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000001fe`151c0050 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000001fe`151c0060 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000001fe`151c0070 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................


Looks like that was a success - we can even see the 0xff byte that we wrote to it. Let's have a look at the backing PTE for this page using the !pte command.
 

6: kd> !pte 0x000001fe`151c0000
VA 000001fe151c0000
PXE at FFFFED76BB5DA018 PPE at FFFFED76BB403FC0 PDE at FFFFED76807F8540 PTE at FFFFED00FF0A8E00
contains 0A0000001A907867 contains 0A0000001B008867 contains 0A00000016609867 contains 80000000A1DD0867
pfn 1a907 ---DA--UWEV pfn 1b008 ---DA--UWEV pfn 16609 ---DA--UWEV pfn a1dd0 ---DA--UW-V


That's good news. It seems like the Valid (V) bit is set, which is what we expect. The memory is Writeable (W), as well, which makes sense based on our PAGE_READWRITE permissions. Let's look at the PFN database entry using !pfn for page 0xa1dd0.
 

kd> !pfn 0xa1dd0
PFN 000A1DD0 at address FFFFE70001E59700
flink 00000002 blink / share count 00000001 pteaddress FFFFED00FF0A8E00
reference count 0001 used entry count 0000 Cached color 0 Priority 5
restore pte 00000080 containing page 016609 Active M
Modified


We can see that the PFN entry points to the same PTE structure we were just looking at. We can go to the address of the PTE at 0xffffed00ff0a8e00 and cast it as an nt!_MMPTE.
 

kd> dt nt!_MMPTE 0xffffed00ff0a8e00 -b
+0x000 u : <unnamed-tag>
+0x000 Long : 0x80000000`a1dd0867
+0x000 VolatileLong : 0x80000000`a1dd0867
+0x000 Hard : _MMPTE_HARDWARE
+0x000 Valid : 0y1
+0x000 Dirty1 : 0y1
+0x000 Owner : 0y1
+0x000 WriteThrough : 0y0
+0x000 CacheDisable : 0y0
+0x000 Accessed : 0y1
+0x000 Dirty : 0y1
+0x000 LargePage : 0y0
+0x000 Global : 0y0
+0x000 CopyOnWrite : 0y0
+0x000 Unused : 0y0
+0x000 Write : 0y1
+0x000 PageFrameNumber : 0y000000000000000010100001110111010000 (0xa1dd0)
+0x000 ReservedForHardware : 0y0000
+0x000 ReservedForSoftware : 0y0000
+0x000 WsleAge : 0y0000
+0x000 WsleProtection : 0y000
+0x000 NoExecute : 0y1


We see that it's Valid, Dirty, Accessed, and Writeable, which are all things that we expect. The Accessed bit is set by the hardware when the page table entry is used for translation. If that bit is set, it means that at some point the memory has been accessed because the PTE was used as part of an address translation. Software can reset this value in order to track accesses to certain memory. Similarly, the Dirty bit shows that the memory has been written to, and is also set by the hardware. We see that it's set for us because we wrote our 0xff byte to the page.

Now let's let the application execute using the g command. We're going to let the program page out the memory that we were just looking at, using the following code:
 

// Unlock the virtual memory (just in case).
VirtualUnlock(Private, 4096);
 
// Flush the process' working set.
SetProcessWorkingSetSize(GetCurrentProcess(), (SIZE_T)-1, (SIZE_T)-1);


Once that's complete, don't forget to switch back to the process context again. We need to do that every time we go back into the debugger! Now let's check out the PTE with the !pte command after the page has been supposedly trimmed from our working set.
 

kd> !pte 0x000001fe`151c0000
VA 000001fe151c0000
PXE at FFFFED76BB5DA018 PPE at FFFFED76BB403FC0 PDE at FFFFED76807F8540 PTE at FFFFED00FF0A8E00
contains 0A0000001A907867 contains 0A0000001B008867 contains 0A00000016609867 contains 00000000A1DD0880
pfn 1a907 ---DA--UWEV pfn 1b008 ---DA--UWEV pfn 16609 ---DA--UWEV not valid
Transition: a1dd0
Protect: 4 - ReadWrite


We see now that the PTE is no longer valid, because the page has been trimmed from our working set; however, it has not been paged out of RAM yet. This means it is in a transition state, as shown by WinDbg. We can verify this for ourselves by looking at the actual PTE structure again.
 

kd> dt nt!_MMPTE 0xffffed00ff0a8e00 -b
+0x000 u : <unnamed-tag>
+0x000 Long : 0xa1dd0880
+0x000 VolatileLong : 0xa1dd0880
+0x000 Hard : _MMPTE_HARDWARE
+0x000 Valid : 0y0
+0x000 Dirty1 : 0y0
+0x000 Owner : 0y0
+0x000 WriteThrough : 0y0
+0x000 CacheDisable : 0y0
+0x000 Accessed : 0y0
+0x000 Dirty : 0y0
+0x000 LargePage : 0y1
+0x000 Global : 0y0
+0x000 CopyOnWrite : 0y0
+0x000 Unused : 0y0
+0x000 Write : 0y1
+0x000 PageFrameNumber : 0y000000000000000010100001110111010000 (0xa1dd0)
+0x000 ReservedForHardware : 0y0000
+0x000 ReservedForSoftware : 0y0000
+0x000 WsleAge : 0y0000
+0x000 WsleProtection : 0y000
+0x000 NoExecute : 0y0
...
+0x000 Trans : _MMPTE_TRANSITION
+0x000 Valid : 0y0
+0x000 Write : 0y0
+0x000 Spare : 0y00
+0x000 IoTracker : 0y0
+0x000 Protection : 0y00100 (0x4)
+0x000 Prototype : 0y0
+0x000 Transition : 0y1
+0x000 PageFrameNumber : 0y000000000000000010100001110111010000 (0xa1dd0)
+0x000 Unused : 0y0000000000000000 (0)
...

In the _MMPTE_TRANSITION version of the structure, the Transition bit is set. So because the memory hasn't yet been paged out, if our program were to access that memory, it would cause a soft page fault that would then simply mark the PTE as valid again. If we examine the PFN entry with !pfn, we can see that the page is still resident in physical memory for now, and still points to our original PTE.
 
kd> !pfn 0xa1dd0
PFN 000A1DD0 at address FFFFE70001E59700
flink 0001614A blink / share count 0013230A pteaddress FFFFED00FF0A8E00
reference count 0000 used entry count 0000 Cached color 0 Priority 5
restore pte 00000080 containing page 016609 Modified M
Modified


Now let's press g again and let the app continue. It'll create a shared section of memory for us. In order to do so, we need to create a file mapping and then map a view of that file into our process.
 

// Create a section object to demonstrate shared memory.
HANDLE Mapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4096, L"memdemosec");
 
// Map the section into our process.
PVOID Shared = MapViewOfFile(Mapping, FILE_MAP_ALL_ACCESS, 0, 0, 4096);
 
// Use the memory to make sure it's resident.
reinterpret_cast<PBYTE>(Shared)[0] = 0xFF;


Let's take a look at the shared memory (at 0x000001fe`151d0000 in this example) using db. Don't forget to change back to our process context when you switch back into the debugger.
 

kd> db 0x000001fe`151d0000
000001fe`151d0000 ff 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000001fe`151d0010 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000001fe`151d0020 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000001fe`151d0030 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000001fe`151d0040 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000001fe`151d0050 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000001fe`151d0060 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000001fe`151d0070 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................


And look! There's the 0xff that we wrote to this memory region as well. We're going to follow the same steps that we did with the previous allocation, but first let's take a quick look at our process' VAD tree with the !vad command.
 

kd> !vad
VAD Level Start End Commit
ffffa50dcf7a1660 4 7ffe0 7ffe0 1 Private READONLY
ffffa50dcf78c450 3 7ffe1 7ffef -1 Private READONLY
ffffa50dcf8a3240 4 6f29440 6f2953f 7 Private READWRITE
ffffa50dcf78b280 2 6f29600 6f297ff 3 Private READWRITE
ffffa50dd1bd9180 3 1fe15080 1fe1508f 0 Mapped READWRITE Pagefile section, shared commit 0
ffffa50dcf74b890 4 1fe15090 1fe15096 1 Private READWRITE
ffffa50dcfb58620 1 1fe150a0 1fe150b7 0 Mapped READONLY Pagefile section, shared commit 0
ffffa50dcf7595f0 3 1fe150c0 1fe150c3 0 Mapped READONLY Pagefile section, shared commit 0
ffffa50dcf669330 2 1fe150d0 1fe150d0 0 Mapped READONLY Pagefile section, shared commit 0
ffffa50dd027cdc0 0 1fe150e0 1fe150e0 1 Private READWRITE
ffffa50dd16580b0 4 1fe150f0 1fe151b4 0 Mapped READONLY \Windows\System32\locale.nls
ffffa50dd15f0470 3 1fe151c0 1fe151c0 1 Private READWRITE
ffffa50dd2313a20 4 1fe151d0 1fe151d0 0 Mapped READWRITE Pagefile section, shared commit 0
ffffa50dcfd121a0 2 1fe15280 1fe1537f 21 Private READWRITE
ffffa50dcf791350 3 7ff7f5c60 7ff7f5d5f 0 Mapped READONLY Pagefile section, shared commit 0
ffffa50dcf74fd50 1 7ff7f5d60 7ff7f5d82 0 Mapped READONLY Pagefile section, shared commit 0
ffffa50dcf721450 4 7ff7f62e0 7ff7f643d 95 Mapped Exe EXECUTE_WRITECOPY \Users\Michael\Desktop\memdemo.exe
ffffa50dcf7a5780 3 7ffc3d340 7ffc3d3bd 5 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\apphelp.dll
ffffa50dcf7515c0 4 7ffc3f6d0 7ffc3f918 9 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\KernelBase.dll
ffffa50dd1708390 2 7ffc40120 7ffc401cd 6 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\kernel32.dll
ffffa50dcf74fdf0 3 7ffc42750 7ffc4292a 12 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\ntdll.dll
 
Total VADs: 21, average level: 3, maximum depth: 4
Total private commit: 0xa2 pages (648 KB)
Total shared commit: 0 pages (0 KB)


You can see the first allocation we did, starting at virtual page number 0x1fe151c0. It's a Private region that has the PAGE_READWRITE permissions applied to it. You can also see the shared section allocated at VPN 0x1fe151d0. It has the same permissions as the non-shared region; however, you can see that it's Mapped rather than Private.

Let's take a look at the PTE information that's backing our shared memory.
 

kd> !pte 0x000001fe`151d0000
VA 000001fe151d0000
PXE at FFFFED76BB5DA018 PPE at FFFFED76BB403FC0 PDE at FFFFED76807F8540 PTE at FFFFED00FF0A8E80
contains 0A0000001A907867 contains 0A0000001B008867 contains 0A00000016609867 contains C1000000A76CC867
pfn 1a907 ---DA--UWEV pfn 1b008 ---DA--UWEV pfn 16609 ---DA--UWEV pfn a76cc ---DA--UW-V

This region, too, is Valid and Writeable, just like we'd expect. Now let's take a look at the !pfn.
 
kd> !pfn a76cc
PFN 000A76CC at address FFFFE70001F64640
flink 00000002 blink / share count 00000001 pteaddress FFFFD3853DA57B60
reference count 0001 used entry count 0000 Cached color 0 Priority 5
restore pte 00000080 containing page 002DCB Active MP
Modified Shared


We see that the Share Count now actually shows us how many times the page has been shared, and the page also has the Shared property. In addition, we see that the PTE address referenced by the PFN entry is not the same as the PTE that we got from the !pte command. That's because the PFN database entry is referencing a prototype PTE, while the PTE within our process is acting as a hardware PTE because the memory is still valid and mapped in.

Let's take a look at the PTE structure that's in our process' paging structures, that was originally found with the !pte command.
 

kd> dt nt!_MMPTE FFFFED00FF0A8E80 -b
+0x000 u : <unnamed-tag>
+0x000 Long : 0xc1000000`a76cc867
+0x000 VolatileLong : 0xc1000000`a76cc867
+0x000 Hard : _MMPTE_HARDWARE
+0x000 Valid : 0y1
+0x000 Dirty1 : 0y1
+0x000 Owner : 0y1
+0x000 WriteThrough : 0y0
+0x000 CacheDisable : 0y0
+0x000 Accessed : 0y1
+0x000 Dirty : 0y1
+0x000 LargePage : 0y0
+0x000 Global : 0y0
+0x000 CopyOnWrite : 0y0
+0x000 Unused : 0y0
+0x000 Write : 0y1
+0x000 PageFrameNumber : 0y000000000000000010100111011011001100 (0xa76cc)
+0x000 ReservedForHardware : 0y0000
+0x000 ReservedForSoftware : 0y0000
+0x000 WsleAge : 0y0001
+0x000 WsleProtection : 0y100
+0x000 NoExecute : 0y1
...


We can see that it's Valid, so it will be used by the hardware for address translation. Let's see what we find when we take a look at the prototype PTE being referenced by the PFN entry.
 

kd> dt nt!_MMPTE FFFFD3853DA57B60 -b
+0x000 u : <unnamed-tag>
+0x000 Long : 0x8a000000`a76cc921
+0x000 VolatileLong : 0x8a000000`a76cc921
+0x000 Hard : _MMPTE_HARDWARE
+0x000 Valid : 0y1
+0x000 Dirty1 : 0y0
+0x000 Owner : 0y0
+0x000 WriteThrough : 0y0
+0x000 CacheDisable : 0y0
+0x000 Accessed : 0y1
+0x000 Dirty : 0y0
+0x000 LargePage : 0y0
+0x000 Global : 0y1
+0x000 CopyOnWrite : 0y0
+0x000 Unused : 0y0
+0x000 Write : 0y1
+0x000 PageFrameNumber : 0y000000000000000010100111011011001100 (0xa76cc)
+0x000 ReservedForHardware : 0y0000
+0x000 ReservedForSoftware : 0y0000
+0x000 WsleAge : 0y1010
+0x000 WsleProtection : 0y000
+0x000 NoExecute : 0y1
...


This PTE is also valid, because it's representing the true state of the physical page. Something interesting to note, though, is that you can see that the Dirty bit is not set. Because this bit is only set by the hardware in the context of whatever process is doing the writing, you can theoretically use this bit to actually detect which process on a system wrote to a shared memory region.

Now let's run the app more and let it page out the shared memory using the same technique we used with the private memory. Here's what the code looks like:
 

// Unlock the virtual memory (just in case).
VirtualUnlock(Shared, 4096);
 
// Flush the process' working set.
SetProcessWorkingSetSize(GetCurrentProcess(), (SIZE_T)-1, (SIZE_T)-1);


Let's take a look at the memory with db now.
 

kd> db 0x000001fe`151d0000
000001fe`151d0000 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ????????????????
000001fe`151d0010 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ????????????????
000001fe`151d0020 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ????????????????
000001fe`151d0030 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ????????????????
000001fe`151d0040 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ????????????????
000001fe`151d0050 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ????????????????
000001fe`151d0060 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ????????????????
000001fe`151d0070 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? ????????????????


We see now that it's no longer visible in our process. If we do !pte on it, let's see what we get.
 

kd> !pte 0x000001fe`151d0000
VA 000001fe151d0000
PXE at FFFFED76BB5DA018 PPE at FFFFED76BB403FC0 PDE at FFFFED76807F8540 PTE at FFFFED00FF0A8E80
contains 0A0000001A907867 contains 0A0000001B008867 contains 0A00000016609867 contains FFFFFFFF00000480
pfn 17f59 ---DA--UWEV pfn 7b5a ---DA--UWEV pfn 9a476 ---DA--UWEV not valid
Proto: VAD
Protect: 4 - ReadWrite


The PTE that's backing our page is no longer valid. We still get an indication of what the page permissions were, but the PTE now tells us to refer to the process' VAD tree in order to get access to the prototype PTE that contains the real state. If you recall from when we used the !vad command earlier in our example, the address of the VAD node for our shared memory is 0xffffa50d`d2313a20. Let's take a look at that memory location as an nt!_MMVAD structure.
 

kd> dt nt!_MMVAD ffffa50dd2313a20
+0x000 Core : _MMVAD_SHORT
+0x040 u2 : <unnamed-tag>
+0x048 Subsection : 0xffffa50d`d1db63e0 _SUBSECTION
+0x050 FirstPrototypePte : 0xffffd385`3da57b60 _MMPTE
+0x058 LastContiguousPte : 0xffffd385`3da57b60 _MMPTE
+0x060 ViewLinks : _LIST_ENTRY [ 0xffffa50d`d1db6368 - 0xffffa50d`d1db6368 ]
+0x070 VadsProcess : 0xffffa50d`d1070380 _EPROCESS
+0x078 u4 : <unnamed-tag>
+0x080 FileObject : (null)


The FirstPrototypePte member contains a pointer to a location in virtual memory that stores contiguous prototype PTEs for the region of memory represented by this VAD node. Since we only allocated (and subsequently paged out) one page, there's only one prototype PTE in this list. The LastContiguousPte member shows that our prototype PTE is both the first and last element in the list. Let's take a look at this prototype PTE as an nt!_MMPTE structure.
 

kd> dt nt!_MMPTE 0xffffd385`3da57b60 -b
+0x000 u : <unnamed-tag>
+0x000 Long : 0xa7fad880
+0x000 VolatileLong : 0xa7fad880
+0x000 Hard : _MMPTE_HARDWARE
+0x000 Valid : 0y0
+0x000 Dirty1 : 0y0
+0x000 Owner : 0y0
+0x000 WriteThrough : 0y0
+0x000 CacheDisable : 0y0
+0x000 Accessed : 0y0
+0x000 Dirty : 0y0
+0x000 LargePage : 0y1
+0x000 Global : 0y0
+0x000 CopyOnWrite : 0y0
+0x000 Unused : 0y0
+0x000 Write : 0y1
+0x000 PageFrameNumber : 0y000000000000000010100111111110101101 (0xa7fad)
+0x000 ReservedForHardware : 0y0000
+0x000 ReservedForSoftware : 0y0000
+0x000 WsleAge : 0y0000
+0x000 WsleProtection : 0y000
+0x000 NoExecute : 0y0


We can see that the prototype indicates that the memory is no longer valid. So what can we do to force this page back into memory? We access it, of course. Let's let the app run one more step so that it can try to access this memory again.
 

// Use the memory one more time.
reinterpret_cast<PBYTE>(Shared)[0] = 0xFF;

Remember to switch back into the context of the process after the application has executed the next step, and then take a look at the PTE from the PFN entry again.
 
kd> dt nt!_MMPTE 0xffffd385`3da57b60 -b
+0x000 u : <unnamed-tag>
+0x000 Long : 0x8a000000`a7fad963
+0x000 VolatileLong : 0x8a000000`a7fad963
+0x000 Hard : _MMPTE_HARDWARE
+0x000 Valid : 0y1
+0x000 Dirty1 : 0y1
+0x000 Owner : 0y0
+0x000 WriteThrough : 0y0
+0x000 CacheDisable : 0y0
+0x000 Accessed : 0y1
+0x000 Dirty : 0y1
+0x000 LargePage : 0y0
+0x000 Global : 0y1
+0x000 CopyOnWrite : 0y0
+0x000 Unused : 0y0
+0x000 Write : 0y1
+0x000 PageFrameNumber : 0y000000000000000010100111111110101101 (0xa7fad)
+0x000 ReservedForHardware : 0y0000
+0x000 ReservedForSoftware : 0y0000
+0x000 WsleAge : 0y1010
+0x000 WsleProtection : 0y000
+0x000 NoExecute : 0y1

Looks like it's back, just like we expected!

Exhausted yet? Compared to the 64-bit paging scheme we talked about in our last article, Windows memory management is significantly more complex and involves a lot of moving parts. But at it's core, it's not too daunting. Hopefully, now with a much stronger grasp of how things work under the hood, we can put our memory management knowledge to use in something practical in a future article.

If you're interested in getting your hands on the code used in this article, you can check it out on GitHub and experiment on your own with it.


Further reading and attributions

Consider picking up a copy of "Windows Internals, 7th Edition" or "What Makes It Page?" to get an even deeper dive on the Windows virtual memory manager. 
 
Thank you to Alex Ionescu for additional tips and clarification. Thanks to irqlnotdispatchlevel for pointing out an address miscalculation.

 

Sursa: http://www.triplefault.io/2017/08/exploring-windows-virtual-memory.html

  • Upvote 1
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.



×
×
  • Create New...