Kayaker
Using nt!_MiSystemVaType to navigate dynamic kernel address space in Windows7
by
, January 30th, 2011 at 16:23 (69635 Views)
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
http://www.nynaeve.net/?p=261
Inside the Windows Vista Kernel: Part 2
http://technet.microsoft.com/en-us/magazine/2007.03.vistakernel.aspx
Windows® Internals, Fifth Edition
9.5.7 Dynamic System Virtual Address Space Management
http://www.microsoft.com/learning/en/us/book.aspx?ID=12069&locale=en-us
MiSystemVaType:
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:
Code: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:
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.Code:kd> dt -v nt!_MMPTE struct _MMPTE, 1 elements, 0x8 bytes
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.
In the registry the PAE status can be read fromPHP 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
if(SharedUserData->ProcessorFeatures[PF_PAE_ENABLED])
{
DbgPrint ("PAE enabled\n");
sizeof_MMPTE = 8;
} else {
DbgPrint ("PAE not enabled\n");
sizeof_MMPTE = 4;
}
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:
PAE is enabled by default in Windows 7, if you wish to test the included code in non-PAE mode use BCDEdit as follows: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
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
http://msdn.microsoft.com/en-us/library/aa366796(v=vs.85).aspx
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.
Code:_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.
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.Code: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 ................ 82955170
Our program output will list that as follows:
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.Code:### 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 ...
CodeMachine Debugger Extension DLL (CMKD.dll)
http://www.codemachine.com/tool_cmkd.html
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.
It's a nice WinDbg extension nonetheless with other useful commands, just take note of this error if using it.Code: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
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:
Code:### 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
http://uninformed.org/?v=4&a=2&t=txt
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
http://blogs.technet.com/b/markrussinovich/archive/2009/03/26/3211216.aspx
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.
In XP the same command will search between the system values of MmPagedPoolStart (0xe1000000) and MmPagedPoolEnd (0xf0ffffff).Code: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 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".Code:XP: kd> !poolfind Cbrb 1 Scanning large pool allocation table for Tag: Cbrb (823ec000 : 823f8000) Searching Paged pool (e1000000 : f1000000) for Tag: Cbrb
Finding some non-exported kernel variables in Windows XP
http://www.rootkit.com/vault/Opc0de/GetVarXP.pdf
Getting Kernel Variables from KdVersionBlock, Part 2
http://www.rootkit.com/newsread.php?newsid=153
Finding Kernel Global Variables in Windows
http://moyix.blogspot.com/2008/04/finding-kernel-global-variables-in.html
Finding Object Roots in Vista (KPCR)
http://blog.schatzforensic.com.au/2010/07/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.
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.Code: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
Kernel Pool Exploitation on Windows 7
http://www.mista.nu/research/
Parsing MM_SESSION_SPACE we see that the full kernel address space is defined as paged session pool memory:
Code:kd> !sprocess Dumping Session 1 _MM_SESSION_SPACE 9007a000 kd> dt nt!_MM_SESSION_SPACE 9007a000 +0x02c PagedPoolStart : 0x80000000 +0x030 PagedPoolEnd : 0xffbfffff
Conclusion:
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:
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.Code:### 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
A Visual Studio project with complete source is included, driver and application binaries are under /bin/i386.
Kayaker
![]()