Tuesday, February 17, 2015

A WinDbg extension to print the kernel memory layout

WinDbg is an awesome debugger, but I always missed the nice, compact and tidy view of the process memory layout that you have in OllyDbg (in View->Memory). Obviously WinDbg is capable of showing information about the virtual memory of a process (e.g. with !vad) or of the kernel (e.g. with !address), but I don't really like the output format of its commands. I wanted a fast-to-read output, thus I decided to experiment with WinDbg's interfaces to write my own extension capable of printing a convenient map of all the kernelmode virtual memory.

I chose to develop a DbgEng-style extension (see this documentation for more information about the extension styles, and how to write an extension in general) that basically provides one main command that does the job. I wrote it for 32bit Windows machines, but I am planning to extend it to 64bit platforms as well. I tested it on Windows XP 32bit with PAE enabled, and in theory it should work on other 32bit Windows versions (with or without PAE), but I have not had time to run further tests yet.

The strategy of the command is simple: it iterates over all the possible virtual addresses of 4k pages in the kernel space (that is, from 0x80000000 to 0xFFFFFFFF, for now I ignore the /3GB configuration option), it retrieves their corresponding PTEs and prints the attributes that they contain. Adjacent pages that have the same attributes are joined together and printed as a range. The output also includes some relevant symbols, e.g. it locates important regions identified by kernel variables like MmNonPagedPoolStart, MmNonPagedPoolEnd0 etc., and it associates the names of loaded drivers to the regions of memory to which they are mapped.
Here are some excerpts from the output:

 P = present  W = writable  X = executable  L = large
 U/K = user/kernel  T = transition  Y = prototype  S = swapped out/zero demand
 VA               Size     Attributes
-------------------------------------
 0000000080000000 --------             - nt!MmSystemRangeStart 
 0000000080004000 0000b000 P W X   K  
 0000000080010000 0000e000 P W X   K  
 0000000080039000 0000c000 P W X   K  
 000000008009f000 00062000 P W X   K  
 0000000080400000 00400000 P W X L K   - nt - hal
 0000000080a02000 00170000 P W X   K  
 0000000080fb1000 0000f000 P W X   K  
 0000000081000000 01600000 P W X L K   - nt!MmNonPagedPoolStart
 0000000082600000 --------             - nt!MmNonPagedPoolEnd0 
 0000000082600000 --------             - nt!MiExtraResourceStart 
 00000000b1f96000 00001000 P W X   K   - kmixer
 00000000b1f97000 00004000 P   X   K   - kmixer
 ...
 00000000f888a000 00001000 P W X   K   - Cdfs
 00000000f888b000 00001000 P   X   K   - Cdfs
 00000000f888c000 00001000 P W X   K   - Cdfs
 00000000f888d000 0000a000  T          - Cdfs
 00000000f8897000 00001000 P W X   K   - Cdfs
 00000000f88b9000 00001000  T         
 00000000f88c9000 00001000  T         
 00000000f88d7000 00003000 P W     K  
 ...

The VA and Size fields identify the memory range, then Attributes shows the properties of the pages it contains, and in the most right part of the output there are the symbols contained in such range. A VA with an invalid size (identified by "--------") means that the VA is not allocated, but there is a symbol associated to it nonetheless.

It is clear from the output that VA 80400000 is the beginning of a buffer, composed of two large pages (2Mb each), that contains the modules nt and hal. The NonPagedPool is also visible at VA 81000000 (11 large pages).
If we have a look at VA f888a000, we can see that this region of memory contains the module Cdfs.sys. Interestingly, the second page at VA f888b000 is read only (probably related to the .text section), while VA f888d000 is the starting of a set of pages that are not present and that are marked as transition PTEs (probably related to the .INIT or .PAGE section).
The following four files are the full source code of the extension.

file 1: exts.cpp
 #include "dbgexts.h"  
   
 char NameBuffer[1024];  
 char NameBufferPrevious[1024];  
   
 // symbol variables  
   
 bool MemSymbolsOk = false;  
 ULONG64 MemSymbols[][3] = {  
     // [api name] [symbol address] [symbol data]  
     (ULONG64)"nt!MmNonPagedPoolStart", 0, 0,  
     (ULONG64)"nt!MmNonPagedPoolEnd0", 0, 0,  
     (ULONG64)"nt!MmPagedPoolStart", 0, 0,  
     (ULONG64)"nt!MmPagedPoolEnd", 0, 0,  
     (ULONG64)"nt!MmNonPagedPoolExpansionStart", 0, 0,  
     (ULONG64)"nt!MmNonPagedPoolEnd", 0, 0,  
     (ULONG64)"nt!MmSystemRangeStart", 0, 0,  
     (ULONG64)"nt!MiExtraResourceStart", 0, 0,  
     (ULONG64)"nt!MiExtraResourceEnd", 0, 0,  
     (ULONG64)"nt!MiSystemViewStart", 0, 0,  
     (ULONG64)"nt!MiSessionPoolStart", 0, 0,  
     (ULONG64)"nt!MiSessionPoolEnd", 0, 0,  
     (ULONG64)"nt!MiSessionViewStart", 0, 0,  
     (ULONG64)"nt!MmSessionSpace", 0, 0,  
     (ULONG64)"nt!MiSessionImageStart", 0, 0,  
     (ULONG64)"nt!MiSessionImageEnd", 0, 0,  
     (ULONG64)"nt!MiSessionSpaceEnd", 0, 0,  
     (ULONG64)"nt!MmSystemPteBase", 0, 0,  
     (ULONG64)"nt!MmSystemPtesStart", 0, 0,  
     (ULONG64)"nt!MmSystemCacheStart", 0, 0,  
     (ULONG64)"nt!MmSystemCacheEnd", 0, 0,  
     (ULONG64)"nt!MmNonPagedSystemStart", 0, 0,  
     0, 0, 0  
 };  
   
 // passing ULONG64 as parameter is not going to work for some reason  
 // the only way is to pass 32bit numbers. %016llx does not work with printf like functions  
 char *Print64(ULONG32 HighPart, ULONG32 LowPart, char *String)  
 {  
     wsprintf(String, "%08x%08x", HighPart, LowPart);  
     return String;  
 }  
   
 // avoid 64bit parameters...  
 void PrintRange(ULONG32 BasePageAddress, char *BasePage, ULONG32 BaseSize, ULONG32 Attribs)  
 {  
     char AttribString[12];  
     UINT32 i;  
     HRESULT Result;  
   
     // string with attribs: PTWYXSL U/K   
   
     memset(AttribString, ' ', sizeof(AttribString));  
     if(Attribs & 1)  
     {  
         // page is valid, print hardware information  
         AttribString[0] = 'P';  
   
         if(Attribs & 2)    // RW  
         {  
             AttribString[2] = 'W';  
         }  
         if(!(Attribs & 0x80000000))    // NX  
         {  
             AttribString[4] = 'X';  
         }  
         if(Attribs & 0x80)  
         {  
             AttribString[6] = 'L';  
         }  
         if(Attribs & 4)        // User/Kernel  
         {  
             AttribString[8] = 'U';  
         }  
         else  
         {  
             AttribString[8] = 'K';  
         }  
     }  
     else  
     {  
         // Page is not valid, print additionally information aboout Prototype, Transition or Software pages  
         // taken from http://rekall-forensic.blogspot.ie/2014/10/windows-virtual-address-translation-and.html  
         // the windbg command "dt -r _MMPTE" shows all the PTE formats  
   
         if( !(Attribs & 0x400) && (Attribs & 0x800) )    // prototype = 0  transition = 1  
         {  
             // Transition PTE  
             AttribString[1] = 'T';  
         }  
         else if(Attribs & 0x400) // prototype = 1  
         {      
             // Prototype PTE  
             AttribString[3] = 'Y';  
         }  
         else if( !(Attribs & 0x400) && !(Attribs & 0x800) )    // prototype = 0  transition = 0  
         {  
             // Software PTE (paged out/zero demand)  
             AttribString[5] = 'S';  
         }  
   
     }  
     AttribString[10] = 0;  
     ExtPrintf(" %s %08x %s ", BasePage, BaseSize, AttribString);  
   
     // print the symbols associated to this VA range  
   
     i = 0;  
     if(MemSymbolsOk)  
     {  
         while(MemSymbols[i][0] != 0)  
         {  
             if((ULONG32)(MemSymbols[i][2]) >= BasePageAddress &&  
                 (ULONG32)(MemSymbols[i][2]) < BasePageAddress + BaseSize)  
             {  
                 ExtPrintf(" - %s", MemSymbols[i][0]);  
             }  
             i++;  
         }  
     }  
   
     // print the module names associated with this VA range  
     UINT32 j;  
     NameBufferPrevious[0] = 0;  
     for(i = BasePageAddress; i < BasePageAddress + BaseSize; i += 0x1000)  
     {  
         // try to locate the nearest symbol  
         Result = g_DebugSymbols->GetNearNameByOffset((ULONG64)(LONG)(i), 0, NameBuffer, 1024, NULL, NULL);  
         if(Result == S_OK || Result == S_FALSE)  
         {  
             NameBuffer[1023] = 0;  
             for(j = 0; j < 1024; j++)  
             {  
                 if(NameBuffer[j] == '!')  
                 {  
                     NameBuffer[j] = 0;  
                     break;  
                 }  
             }  
   
             if(j < 1024)  
             {  
                 // only print the name if it was not printed before  
                 if(strcmp(NameBufferPrevious, NameBuffer) != 0)  
                 {  
                     ExtPrintf(" - %s", NameBuffer);  
                     strcpy_s(NameBufferPrevious, 1024, NameBuffer);  
                     NameBufferPrevious[1023] = 0;  
                 }  
             }  
         }  
     }  
   
     ExtPrintf("\n");  
 }  
   
 #define PARAM64(__number, __numstring)    Print64((unsigned)__number >> 32, __number, __numstring)      
   
 HRESULT CALLBACK exthelp(PDEBUG_CLIENT4 Client, PCSTR args)  
 {  
     INIT_API();  
   
     ExtPrintf("\nUse print_symbol to load the symbols required by the extension \n");  
     ExtPrintf("Then use print_layout to print the whole memory layout of the kernelspace. \n");  
   
     EXIT_API();  
     return S_OK;  
 }  
   
 HRESULT CALLBACK print_layout(PDEBUG_CLIENT4 Client, PCSTR args)  
 {  
     INIT_API();  
   
     ULONG64 PteAddress, PteEntry, BasePage;  
     ULONG32 BaseAttributes, CurrentAttribs, BaseSize, i;  
     ULONG64 Tables[10];  
     ULONG Levels;  
     HRESULT Result;  
     char TempString1[20];  
   
     // flags 0-based  
     // RW bit 1, 0 = read only  
     // U/S bit 2, 0 = kernelmode, 1 = usermode  
     // PS bit 7, 0 = 4k, 1 = 4mb  
     // NX bit 63, 1 = no execute  
     // W X L U  
   
     ExtPrintf("\n");  
     ExtPrintf(" P = present W = writable X = executable L = large\n");  
     ExtPrintf(" U/K = user/kernel T = transition Y = prototype S = swapped out/zero demand\n");  
     ExtPrintf(" VA        Size   Attributes\n");  
     ExtPrintf("-------------------------------------\n");  
     BasePage = 0xFFFFFFFFFFFFFFFF;  
     BaseAttributes = 0xFFFFFFFF;  
     BaseSize = 0;  
   
     for(ULONG64 VAddress = 0x80000000; VAddress < 0xFFFFF000; VAddress += 0x1000)  
     {  
         Result = g_DataSpaces2->GetVirtualTranslationPhysicalOffsets(VAddress, Tables, 10, &Levels);  
         if(Result != S_OK)  
         {  
             // if there was a previous buffer, print it out  
             if(BasePage != 0xFFFFFFFFFFFFFFFF)  
             {  
                 PrintRange((ULONG32)BasePage, PARAM64(BasePage, TempString1), BaseSize, BaseAttributes);  
             }  
   
             // if the symbol refers to a non allocated page, print it here  
             i = 0;  
             while(MemSymbols[i][0] != 0 && MemSymbolsOk)  
             {  
                 if((ULONG32)(MemSymbols[i][2]) >= (ULONG32)VAddress &&  
                     (ULONG32)(MemSymbols[i][2]) < (ULONG32)VAddress + 0x1000)  
                 {  
                     ExtPrintf(" %s --------       - %s \n", PARAM64(VAddress, TempString1), MemSymbols[i][0]);  
                 }  
                 i++;  
             }  
   
             BasePage = 0xFFFFFFFFFFFFFFFF;  
             BaseAttributes = 0xFFFFFFFF;  
             BaseSize = 0;  
             continue;  
         }  
   
         PteAddress = Tables[Levels - 2];  
         Result = g_DataSpaces->ReadPhysical(PteAddress, &PteEntry, 8, NULL);  
   
         if(BasePage == 0xFFFFFFFFFFFFFFFF)  
         {  
             // case first page of buffer  
             BasePage = VAddress;  
             BaseAttributes = (PteEntry & 0x7FFFFFFF);  
             if(PteEntry & 0x8000000000000000)    // NX bit  
             {  
                 BaseAttributes |= 0x80000000;  
             }  
             BaseSize = 0x1000;  
         }  
         else  
         {  
             CurrentAttribs = (PteEntry & 0x7FFFFFFF);  
             if(PteEntry & 0x8000000000000000)  
             {  
                 CurrentAttribs |= 0x80000000;  
             }  
   
             bool new_buf = true;  
             if((BaseAttributes & 1) && (CurrentAttribs & 1))  
             {  
                 // if P bit is set in both  
                 if( (BaseAttributes & 0x80000087) == (CurrentAttribs & 0x80000087) )  
                 {  
                     // and other interesting bits are equal in both, the buffer is continuing  
                     new_buf = false;  
                 }  
             }  
             else if(!(BaseAttributes & 1) && !(CurrentAttribs & 1))  
             {  
                 // if P bit is not set in both  
                 if( (BaseAttributes & 0x00000C00) == (CurrentAttribs & 0x00000C00) )  
                 {  
                     // and other interesting bits are equal in both, the buffer is continuing  
                     new_buf = false;  
                 }  
             }  
             // if P is different in both, break is obviously necessary  
   
             if(new_buf)  
             {  
                 // if the protection is different:  
                 // print the buffer and continue  
                 PrintRange((ULONG32)BasePage, PARAM64(BasePage, TempString1), BaseSize, BaseAttributes);  
   
                 // break to a new buffer  
                 BasePage = VAddress;  
                 BaseAttributes = (PteEntry & 0x7FFFFFFF);  
                 if(PteEntry & 0x8000000000000000)  
                 {  
                     BaseAttributes |= 0x80000000;  
                 }  
   
                 BaseSize = 0;  
             }  
   
             // case following pages  
             BaseSize += 0x1000;  
         }  
     }  
   
     EXIT_API();  
     return S_OK;  
 }  
   
 HRESULT CALLBACK print_symbol(PDEBUG_CLIENT4 Client, PCSTR args)  
 {  
     INIT_API();  
   
     char TempString1[20];  
     char TempString2[20];  
     UINT32 i;  
     HRESULT Result;  
   
     i = 0;  
     while(MemSymbols[i][0] != 0)  
     {  
         Result = g_DebugSymbols->GetOffsetByName((char*)(MemSymbols[i][0]), &(MemSymbols[i][1]));  
         if(Result != S_OK)  
         {  
             ExtPrintf("Error retrieving symbol %s \n", (char*)MemSymbols[i][0]);  
             return Result;  
         }  
   
         Result = g_DataSpaces->ReadVirtual(MemSymbols[i][1], &(MemSymbols[i][2]), 8, NULL);  
         if(Result != S_OK)  
         {  
             ExtPrintf("Error reading symbol data for %s \n", (char*)MemSymbols[i][0]);  
             return Result;  
         }  
   
         ExtPrintf("Symbol retrieved %s, offset: %s data: %s \n", (char*)(MemSymbols[i][0]), PARAM64(MemSymbols[i][1], TempString1), PARAM64(MemSymbols[i][2], TempString2));  
   
         i++;  
     }  
   
     MemSymbolsOk = true;  
   
     EXIT_API();  
     return S_OK;  
 }  
   

file 2: dbgexts.h
 #include <windows.h>  
 #include <stdio.h>  
 #include <stdlib.h>  
 #include <string.h>  
   
 #define KDEXT_64BIT  
 #include <wdbgexts.h>  
 #include <dbgeng.h>  
   
 #pragma warning(disable:4201) // nonstandard extension used : nameless struct  
 #include <extsfns.h>  
   
 #ifdef __cplusplus  
 extern "C" {  
 #endif  
   
   
 #define INIT_API()               \  
   HRESULT Status;              \  
   if ((Status = ExtQuery(Client)) != S_OK) return Status;  
   
 #define EXT_RELEASE(Unk) \  
   ((Unk) != NULL ? ((Unk)->Release(), (Unk) = NULL) : NULL)  
   
 #define EXIT_API   ExtRelease  
   
 // Global variables initialized by query.  
 extern PDEBUG_DATA_SPACES2  g_DataSpaces2;  
 extern PDEBUG_DATA_SPACES  g_DataSpaces;  
 extern PDEBUG_SYMBOLS    g_DebugSymbols;  
   
 HRESULT ExtQuery(PDEBUG_CLIENT4 Client);  
 void ExtRelease(void);  
 HRESULT NotifyOnTargetAccessible(PDEBUG_CONTROL Control);  
 void __cdecl ExtPrintf(PCSTR Format, ...);  
   
 #ifdef __cplusplus  
 }  
 #endif  
   

file 3: dbgexts.cpp
 #include "dbgexts.h"  
   
 PDEBUG_CLIENT4    g_ExtClient;  
 PDEBUG_CONTROL    g_ExtControl;  
 PDEBUG_DATA_SPACES2  g_DataSpaces2;  
 PDEBUG_DATA_SPACES  g_DataSpaces;  
 PDEBUG_SYMBOLS    g_DebugSymbols;  
 PDEBUG_SYMBOLS2    g_ExtSymbols;  
   
 extern "C" HRESULT ExtQuery(PDEBUG_CLIENT4 Client)  
 {  
   HRESULT Status;  
   
   if ((Status = Client->QueryInterface(__uuidof(IDebugControl), (void **)&g_ExtControl)) != S_OK)  
   {  
     goto Fail;  
   }  
   if ((Status = Client->QueryInterface(__uuidof(IDebugSymbols2), (void **)&g_ExtSymbols)) != S_OK)  
   {  
         goto Fail;  
   }  
   if ((Status = Client->QueryInterface(__uuidof(IDebugDataSpaces2), (void **)&g_DataSpaces2)) != S_OK)  
   {  
         goto Fail;  
   }  
   if ((Status = Client->QueryInterface(__uuidof(IDebugDataSpaces), (void **)&g_DataSpaces)) != S_OK)  
   {  
         goto Fail;  
   }  
   if ((Status = Client->QueryInterface(__uuidof(IDebugSymbols), (void **)&g_DebugSymbols)) != S_OK)  
   {  
         goto Fail;  
   }  
   
   g_ExtClient = Client;  
   
   return S_OK;  
   
 Fail:  
   ExtRelease();  
   return Status;  
 }  
   
 void ExtRelease(void)  
 {  
   g_ExtClient = NULL;  
   EXT_RELEASE(g_ExtControl);  
   EXT_RELEASE(g_ExtSymbols);  
 }  
   
 void __cdecl ExtPrintf(PCSTR Format, ...)  
 {  
   va_list Args;  
   
   va_start(Args, Format);  
   g_ExtControl->OutputVaList(DEBUG_OUTCTL_ALL_CLIENTS, Format, Args);  
   va_end(Args);  
 }  
   
 extern "C" HRESULT CALLBACK DebugExtensionInitialize(PULONG Version, PULONG Flags)  
 {  
   *Version = DEBUG_EXTENSION_VERSION(1, 0);  
   *Flags = 0;  
   return S_OK;  
 }  
   
 extern "C" void CALLBACK DebugExtensionNotify(ULONG Notify, ULONG64 Argument)  
 {  
   return;  
 }  
   
 extern "C" void CALLBACK DebugExtensionUninitialize(void)  
 {  
   return;  
 }  
   
 extern "C" HRESULT CALLBACK KnownStructOutput(ULONG Flag, ULONG64 Address, PSTR StructName, PSTR Buffer, PULONG BufferSize)  
 {  
   return S_OK;  
 }  
     

file 4: dbgexts.def
 
 ;--------------------------------------------------------------------  
 ;  Copyright (c) 2000 Microsoft Corporation  
 ;  
 ;Module:  
 ;  dbgexts.def  
 ;--------------------------------------------------------------------  
   
 EXPORTS  
   
 ;--------------------------------------------------------------------  
 ; These are the extensions exported by dll  
 ;--------------------------------------------------------------------  
   
      exthelp  
      print_layout  
      print_symbol  
   
 ;--------------------------------------------------------------------  
 ;  
 ; these are the extension service functions provided for the debugger  
 ;  
 ;--------------------------------------------------------------------  
   
      DebugExtensionNotify  
      DebugExtensionInitialize  
      DebugExtensionUninitialize  
      KnownStructOutput  
   

I took some macros from one of the source code templates that is available in the WinDbg SDK. I had problems when passing 64bit integers as function parameters (the extension was compiled for 32bit), therefore I used a quick and ugly macro (PARAM64) to solve the problem.

The core of the functionality is inside the print_layout function/command in exts.cpp:

...
for(ULONG64 VAddress = 0x80000000; VAddress < 0xFFFFF000; VAddress += 0x1000)
{
Result = g_DataSpaces2->GetVirtualTranslationPhysicalOffsets(VAddress, Tables, 10, &Levels);
...
PteAddress = Tables[Levels - 2];
Result = g_DataSpaces->ReadPhysical(PteAddress, &PteEntry, 8, NULL);
...

This is the main loop that iterates over every possible page, performing a virtual-to-physical translation by using GetVirtualTranslationPhysicalOffsets. This function is very interesting because it returns all the entries from all the steps used to perform the translation: the physical address of the PDPT, PDE, PTE and of the page itself (the translations steps change according to the features supported by the CPU). Then, the code uses ReadPhysical to read the data contained in the PTE and extracts all the attributes from it. The rest of the function simply recognizes ranges of pages that share the same attributes and, for every one being identified, PrintRange is invoked.

As the name suggests, PrintRange is in charge of displaying the gathered information for every range, and takes as its arguments a range's virtual address, size and attributes. In addition, it is also responsible for determining if one of the supported symbols (that are stored in the array MemSymbols) is contained within the range and if a driver module is associated to the range by using GetNearNameByOffset. In case it does, it prints them too. Of course, these two last capabilities only work if the debug symbols are loaded in WinDbg.

Note that the array of the supported symbols contains the names of internal kernel variables that identify interesting areas of memory (e.g. the paged pool, the non paged pool, etc.), and two empty entries that will be filled at run-time with the virtual address of the symbol and the data it points to. This functionality is implemented in the print_symbol function/command: you should call it before print_layout in order to displays the supported symbols.

The source files dbgext.cpp and dbgexts.h contain macros and initialization code that are required by the extension, while dbgexts.def contains the definitions of the exported functions (that will become the actual commands to be invoked from WinDbg commandline).

To compile the extension, you need to add WinDbg's include and library paths, normally located in:

 C:\Program Files\Debugging Tools for Windows (x64)\sdk\inc
 C:\Program Files\Debugging Tools for Windows (x64)\sdk\lib\i386

Make sure that you add the .def file via project->properties->linker->input->module definition file (this works on Visual Studio 2010).
Once the extension is compiled, place it in WinDbg's extension folder:

 C:\Program Files (x86)\Debugging Tools for Windows (x86)\winext

and load it from WinDbg with:

.load <extensionfilename>

Also make sure you have Windows debug symbols installed and loaded. At this point, using !print_symbol initializes the supported symbols, and !print_layout produces the final output.

This is a POC, it was a good exercise to make practice with WinDbg extensions, I am planning to rework this source code to make it compatible with all versions of Windows on 32 and 64 bit. At the moment it is not very fast, it takes few minutes to print the whole layout, but I think it is possible to speed up the processing avoiding the brute force loop on every page, and handling in a smarter way the pages based on the contents of the PDEs and PTEs (basically if a PDE is invalid I can exclude a lot of memory addresses from the loop).

No comments:

Post a Comment