Using nt!_MiSystemVaType to navigate dynamic kernel address space in Windows7

Rating: 5 votes, 2.20 average.
32-bit Windows Vista and later use a feature known as Dynamic Kernel Address Space. To quote from a technical article - the Memory Manager dynamically manages the kernel's address space, allocating and deallocating space to various uses to meet the needs of the system. As a result, the amount of virtual memory being used for internal components, device drivers, the file system cache, kernel stacks, system PTE's, per-session code data structures as well as paged and nonpaged pool memory will grow and shrink based on system activity.

The key to keeping track of all this dynamic memory lies in the unexported pointer nt!_MiSystemVaType, a mapped array of byte values that describes both the type of memory allocation, and by virtue of the indexed position within the array, the location and size of the memory block. Each time there is a new memory allocation, the MiSystemVaType array is updated.

In this code project I will try to show how to use MiSystemVaType to navigate the dynamic kernel address space to get a complete mapping of the various allocation types. In addition, I'll give an example of how to use it to find and identify loaded drivers, as well as discuss how it might be used to conduct efficient memory pool searches.

Here are a few background articles on the subject at hand:

Understanding the kernel address space on 32-bit Windows Vista

Inside the Windows Vista Kernel: Part 2

Windows® Internals, Fifth Edition
9.5.7 Dynamic System Virtual Address Space Management


nt!_MiSystemVaType is a pointer to an array of byte values of enum type MI_SYSTEM_VA_TYPE. Each byte in the array describes a single Large Page and maps, in sequential order, the entire upper 2GB of logical address space from 0x80000000 (MmSystemRangeStart) - 0xFFFFFFFF. The size of the byte array is either 0x400 when PAE is enabled, where the default size of a Large Page is 2MB, or 0x200 in non-PAE mode, which uses a Large Page size of 4MB.

The enum type values can be listed with WinDbg/LiveKd:

kd> dt nt!_MI_SYSTEM_VA_TYPE

   MiVaUnused = 0n0
   MiVaSessionSpace = 0n1
   MiVaProcessSpace = 0n2
   MiVaBootLoaded = 0n3
   MiVaPfnDatabase = 0n4
   MiVaNonPagedPool = 0n5
   MiVaPagedPool = 0n6
   MiVaSpecialPoolPaged = 0n7
   MiVaSystemCache = 0n8
   MiVaSystemPtes = 0n9
   MiVaHal = 0n10
   MiVaSessionGlobalSpace = 0n11
   MiVaDriverImages = 0n12
   MiVaSpecialPoolNonPaged = 0n13
   MiVaMaximumType = 0n14

PAE mode:

The Physical Address Extension (PAE) processor feature enables use of 64-bit page table entries for physical addresses that are wider than 32 bits. If PAE is enabled, the size of page table entries (PTEs) are increased from 32 to 64 bits (4 to 8 bytes). Consequently, the size of a Large Page is reduced from 4MB to 2MB in PAE mode. One can determine the size of the PTE data structure, nt!_MMPTE, (and hence if PAE is enabled or not) with the command:

kd> dt -v nt!_MMPTE
struct _MMPTE, 1 elements, 0x8 bytes
To determine if PAE is enabled programmatically we can read the ProcessorFeatures field of KUSER_SHARED_DATA, a shared memory structure mapped to all processes and located at 0x7FFE0000 in usermode. This is equivalent to what the IsProcessorFeaturePresent API does.

KUSER_SHARED_DATA is duplicated at 0xFFDF0000 in kernelmode. Fortunately ntddk.h gives us a handy macro with which to work with it. The snippet below will give us (by inference) the size of nt!_MMPTE, from which we can derive the size of a large page and the size of the MiSystemVaType array.

PHP Code:
#define KI_USER_SHARED_DATA         0xffdf0000
#define SharedUserData  ((KUSER_SHARED_DATA * const)   KI_USER_SHARED_DATA)

// Determine if PAE is enabled from  KI_USER_SHARED_DATA.ProcessorFeatures

DbgPrint ("PAE enabled\n");

sizeof_MMPTE 8;

} else {

DbgPrint ("PAE not enabled\n");

sizeof_MMPTE 4;

In the registry the PAE status can be read from
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\PhysicalAddressExtension

Here is a summary of the differences between PAE and non-PAE mode which are relevant to our code:

(PAGE_SIZE = 0x1000)

PAE enabled:

    nt kernel version:
         ntkrnlpa.exe: 1 CPU, PAE
         ntkpamp.exe:  n CPU, SMP, PAE

    sizeof_MMPTE = 8
    LARGE_PAGE_SIZE = PAGE_SIZE * PAGE_SIZE / sizeof_MMPTE = 0x200000  (2MB)
    sizeof MiSystemVaType array = (0xFFFFFFFF+1 -  (ULONG)MmSystemRangeStart) / LARGE_PAGE_SIZE = 0x400

    0x400 * 0x200000 = 0x80000000 = (0x80000000 / 1024 /1024 /1024) =  2GB
PAE disabled:

    nt kernel version:
        ntoskrnl.exe: 1 CPU
        ntkrnlmp.exe: n CPU, SMP

    sizeof_MMPTE = 4
    LARGE_PAGE_SIZE = PAGE_SIZE * PAGE_SIZE / sizeof_MMPTE = 0x400000  (4MB)
    sizeof MiSystemVaType array = (0xFFFFFFFF+1 -  (ULONG)MmSystemRangeStart) / LARGE_PAGE_SIZE = 0x200

    0x200 * 0x400000 = 0x80000000 = 2GB
PAE is enabled by default in Windows 7, if you wish to test the included code in non-PAE mode use BCDEdit as follows:

If DEP is enabled, PAE cannot be disabled. Use the following BCDEdit /set commands to disable both DEP and PAE:

bcdedit /set nx AlwaysOff
bcdedit /set pae ForceDisable

To restore:

bcdedit /set nx Optout (or one of [Optin |OptOut | AlwaysOn])
bcdedit /set pae ForceEnable


Finding the unexported pointer nt!_MiSystemVaType:

We need to programmatically find the offset to nt!_MiSystemVaType. Since this is an unexported pointer we'll have to parse a known kernel function which makes use of the variable. Uh Oh. Production code need not apply . Oh well, this is an RCE forum, right? At least that's better than using a hard-coded value, not as good as using symbols.

Rather than using a classic byte-pattern search that is often used to find unexported variables, I made use of a clever idea Zairon mentioned to me, that of looking for cross references between code and data sections in order to pick up instances of data variable usage. In essence, derive XREFS similar to IDA.

I really like Zairon's idea of using XREF analysis over a byte-pattern search method because it's simple, highly adaptable, and is less susceptible to changing byte patterns between different OS kernel versions.

The function I chose to parse for the offset of MiSystemVaType was the exported MmIsNonPagedSystemAddressValid procedure. The simple algorithm logic I used was: "Scan for the first data XREF to the section called '.data''"

See the source code for the specific algorithm I implemented, plus a few suggestions for creating a more rigorous algorithm if desired, such as using a length disassembly engine (LDE) to avoid the possibility a false XREF could occur across instructions.

The simple logic above should be valid for all current 32-bit nt* kernel versions in Windows 7 / Vista / Server 2008. Even better, MmIsNonPagedSystemAddressValid has been deemed to be obsolete and is exported to support existing drivers only, so it's more unlikely to change anytime soon.

_MmIsNonPagedSystemAddressValid@4 proc
8B FF                             mov     edi, edi
55                                push    ebp
8B EC                             mov     ebp, esp
53                                push    ebx
56                                push    esi
8B 35 18 57 97 82                 mov     esi, ds:_MmSystemRangeStart //  xref to ALMOSTRO
57                                push    edi
8B 7D 08                          mov     edi, [ebp+VirtualAddress]
BB F8 3F 00 00                    mov     ebx, 3FF8h
3B FE                             cmp     edi, esi
72 25                             jb      short loc_828F17A8
8B C6                             mov     eax, esi
C1 E8 12                          shr     eax, 12h
8B CF                             mov     ecx, edi
C1 E9 12                          shr     ecx, 12h
23 C3                             and     eax, ebx
23 CB                             and     ecx, ebx
2B C8                             sub     ecx, eax
C1 F9 03                          sar     ecx, 3
8A 81 60 51 95 82                 mov     al, _MiSystemVaType[ecx]  // xref to .data

Making sense of MiSystemVaType:

Now that we've got the pointer to nt!_MiSystemVaType, what do we do with it? The first obvious thing is just to list everything out.

Let's take a look at the first 0x10 bytes of the MiSystemVaType array. Each byte maps a logical address block of LARGE_PAGE_SIZE, beginning at MmSystemRangeStart.

kd> x nt!MiSystemVaType

82955160 nt!MiSystemVaType = <no type information>

kd> db 82955160
82955160 03 03 09 09 03 03 03 03-03 03 03 03 03 03 03 06   ................
We see that the first byte is 0x03, which is the nt!_MI_SYSTEM_VA_TYPE enum type MiVaBootLoaded. It describes the logical address block from 0x80000000 - 0x801fffff (PAE enabled, Large Page size = 2MB). The second byte is also 0x03 and maps 0x80200000 - 0x803fffff. The 3rd and 4th bytes are MiVaSystemPtes, the next 11 bytes are again MiVaBootLoaded, and so forth.

Our program output will list that as follows:

### Start    End        Length (  MB) Count Type    
001 80000000 803fffff   400000 (   4)    2 BootLoaded
002 80400000 807fffff   400000 (   4)    2 SystemPtes
003 80800000 81dfffff  1600000 (  22)   11 BootLoaded
004 81e00000 825fffff   800000 (   8)    4 PagedPool
At this point I'll mention a very nice WinDbg extension cmkd!kvas which uses the known symbolic offset value of nt!_MiSystemVaType to produce the same output.

CodeMachine Debugger Extension DLL (CMKD.dll)

Unfortunately, there's a bug in the code and the Length and MB columns give incorrect values for every entry except the first one. It's just a small implementation bug, a counter used incorrectly. Here is the same output as above, from cmkd. It seems apparent to me that there's an extra 200000 bytes (large page size with PAE enabled) added to the Length calculation from the second entry onwards.

kd> .load cmkd
kd> !cmkd.kvas
### Start    End        Length (  MB)    Count Type
000 80000000 803fffff   400000 (   4)        2 BootLoaded
001 80400000 807fffff   600000 (   6)        2 SystemPtes
002 80800000 81dfffff  1800000 (  24)       11 BootLoaded
003 81e00000 825fffff   a00000 (  10)        4 PagedPool
It's a nice WinDbg extension nonetheless with other useful commands, just take note of this error if using it.

For consistency, comparison, and in recognition of the cmkd author, I have used the same logical output format and features in my code.

Enumerating Driver Modules:

Another feature I added to the code, just to see what else could be done, was an option to scan all memory blocks of type MiVaBootLoaded and MiVaDriverImages for MZ headers in order to identify the modules contained within them. To name the modules I matched the base address with the results from ZwQuerySystemInformation (SystemInformationClass). Any modules not matching might be considered as hidden drivers.

For interest, here are the modules classified as MiVaBootLoaded:

### Base     Size     ImageName
001 80bc1000 00008000 kdcom.dll
002 82817000 00037000 halmacpi.dll
003 8284e000 00410000 ntkrnlpa.exe

Pool Searching:

The following section is not directly related to the code and is probably of limited interest. It mainly details the differences in some kernel global pool variables between Windows 7 and XP.

For background reference on some reasons why we'd be interested in pool searching, see

GREPEXEC: Grepping Executive Objects from Pool Memory

On the one hand we have what seems like a very nice mechanism in MiSystemVaType for searching through the various memory allocation types. Want to search the Paged Pool? Just parse the MiSystemVaType array for large pages presently tagged for that allocation type and search through them for valid pool headers.

On the other hand, that's not the way Windows seems to view it.

In the following article, Mark Russinovich describes how in 32-bit Windows Vista and later with dynamic kernel address space, the paged pool limit is simply set to 2GB, and will run out either when the system address space is full or the system commit limit is reached. Similarly, the nonpaged pool limit is set at ~75% of RAM or 2GB, whichever is smaller.

Pushing the Limits of Windows: Paged and Nonpaged Pool

Evidence for the above can be seen in the WinDbg !poolfind command, used to find all instances of a specific pool tag in either nonpaged or paged memory pools (as used by ExAllocatePoolWithTag).

In Windows 7, !poolfind sets by default the pool limits for each [PoolType] flag it supports to almost the full upper 2GB address range, 80000000 - ffc00000 (the address range between 0xffc00000-0xffffffff is reserved for HAL, i.e. the last 2 bytes of the MiSystemVaType array are always enum type MiVaHal).

Here is an example when searching the Paged Pool for the tag 'Cbrb'. This tag is used for allocations by the system callbacks PspCreateProcessNotifyRoutine, PspLoadImageNotifyRoutine, PspCreateThreadNotifyRoutine, and in XP, CmRegisterCallback.

Windows 7:

kd> !poolfind Cbrb 1

Scanning large pool allocation table for Tag: Cbrb (b5800000 : b5c00000)

Searching Paged pool (80000000 : ffc00000) for Tag: Cbrb
In XP the same command will search between the system values of MmPagedPoolStart (0xe1000000) and MmPagedPoolEnd (0xf0ffffff).


kd> !poolfind Cbrb 1

Scanning large pool allocation table for Tag: Cbrb (823ec000 : 823f8000)

Searching Paged pool (e1000000 : f1000000) for Tag: Cbrb
In Windows 7 many of the global variables such as nt!MmPagedPoolStart, nt!MmPagedPoolEnd and related NonPagedPool variables mentioned in the GREPEXEC article are no longer valid. We can see this by parsing the (PKDDEBUGGER_DATA64)KdDebuggerDataBlock structure, which is accessible through the Kernel Processor Control Region (KPCR). See the following articles for background on this well known "KPCR trick".

Finding some non-exported kernel variables in Windows XP

Getting Kernel Variables from KdVersionBlock, Part 2

Finding Kernel Global Variables in Windows

Finding Object Roots in Vista (KPCR)

I made up a small driver to retrieve the offset of KdDebuggerDataBlock and loaded up the driver symbols in LiveKd so the KDDEBUGGER_DATA64 structure would be defined in order to get the following output.

You can see that several of the fields that in XP would normally be pointers to global pool variables are now zeroed out, having been made redundant in Window 7/Vista by Dynamic Kernel Address Space and the MiSystemVaType mechanism.

kd> dt -b k_kpcr!dummy 82976be8 //  (PKDDEBUGGER_DATA64)KdDebuggerDataBlock

   +0x0a8 MmSystemCacheStart : 0
   +0x0b0 MmSystemCacheEnd : 0

   +0x0c8 MmSystemPtesStart : 0
   +0x0d0 MmSystemPtesEnd  : 0
   +0x108 MmNonPagedSystemStart : 0
   +0x110 MmNonPagedPoolStart : 0x829b612c => 0x8b971000 // not  relevant
   +0x118 MmNonPagedPoolEnd : 0
   +0x120 MmPagedPoolStart : 0
   +0x128 MmPagedPoolEnd   : 0x829b6098 => 0
   +0x278 MmSessionBase    : 0
   +0x280 MmSessionSize    : 0
Another place we can see the use of the maximized pool limits, which again differs from XP, is in the per-session nt!_MM_SESSION_SPACE structure. Session pool memory (used by win32k) is used for session space allocations and is unique to each user session. While non-paged session memory use the global non-paged pool descriptor(s), paged session pool memory has its own pool descriptor defined in _MM SESSION SPACE.

Kernel Pool Exploitation on Windows 7

Parsing MM_SESSION_SPACE we see that the full kernel address space is defined as paged session pool memory:

kd> !sprocess
Dumping Session 1


kd> dt nt!_MM_SESSION_SPACE 9007a000

   +0x02c PagedPoolStart   : 0x80000000
   +0x030 PagedPoolEnd     : 0xffbfffff


So far we've seen that Windows 7 defines the same extended upper and lower pool limits for at least paged, nonpaged and session memory. WinDbg !poolfind assumes the same thing and unfortunately it significantly slows down pool-specific searches (try timing the difference between XP and Windows 7 for the same search). Chances are however that there's a very good reason for doing it that way that is not immediately apparent.

From a reversers perspective however, we could use MiSystemVaType to narrow down the search limits rather than enumerating the entire system address space. For example, using the code from this project we can find that MiVaNonPagedPool and MiVaSessionSpace type memory is isolated within the following regions:

### Start    End        Length (  MB) Count Type    
001 8b600000 8bbfffff   600000 (   6)    3 NonPagedPool
002 8c000000 8c1fffff   200000 (   2)    1 NonPagedPool
003 8c400000 8d9fffff  1600000 (  22)   11 NonPagedPool
004 b5800000 b5bfffff   400000 (   4)    2 NonPagedPool

### Start    End        Length (  MB) Count Type    
001 fda00000 fdbfffff   200000 (   2)    1 SessionSpace
002 fde00000 ffbfffff  1e00000 (  30)   15 SessionSpace
Ultimately, it seems like any algorithm one might develop for pool searching would come down to using nt!_MiSystemVaType for the efficiency of being able to identify pool-specific regions, or searching the entire system address space, a much slower proposition, for the simplicity of not having to write those extra procedures.

A Visual Studio project with complete source is included, driver and application binaries are under /bin/i386.


Name:  sysvas.jpg
Views: 3326
Size:  65.4 KB
Attached Thumbnails Attached Files

Submit "Using nt!_MiSystemVaType to navigate dynamic kernel address space in Windows7" to Digg Submit "Using nt!_MiSystemVaType to navigate dynamic kernel address space in Windows7" to del.icio.us Submit "Using nt!_MiSystemVaType to navigate dynamic kernel address space in Windows7" to StumbleUpon Submit "Using nt!_MiSystemVaType to navigate dynamic kernel address space in Windows7" to Google