---
# System prepended metadata

title: Understanding the Virtual Addresses of ntoskrnl

---

# Understanding the Virtual Addresses of ntoskrnl

In my journey to better understand Windows Kernel, I have put significant time into learning about the virtual address layout of ```ntoskrnl```, the underlying executable image of the system. This information has been found before, but I hope in this post to consolidate it.

A goal I made for myself was to loop through the ```NonPagedPool``` and output information about its memory allocations; this would be useful to track and get rid of allocations made by my illegitimate driver. To do this, I had to start from the symbol ```nt!MmNonPagedPoolStart```, which just means the starting virtual address of the ```NonPagedPool```. Unfortunately, this symbol is no longer supported in modern versions of Windows. The value is still used internally, but it no longer has a symbol associated with it in ntoskrnl's documentation. I began to wonder how these memory ranges were assigned, and how I could find their bounds for myself. 

## MiInitalizeNonPagedPool
![image](https://hackmd.io/_uploads/Hkrm0NpGA.png)

Combing through ntoskrnl in IDA, the function ```MiInitializeNonPagedPool``` appears promising. Looking at the xrefs of ```qword_140C4F988``` reveals that it is used in functions associated with virtual address regions, like ```MiQuerySystemBase``` and ```MiAssignTopLevelRanges```. From this, we can infer that this qword is actually a pointer to an array of  ```MI_SYSTEM_VA_ASSIGNMENT``` structures called ```SystemVaRegions```. This array contains the base address of every virtual range in the kernel:

```
typedef struct _MI_SYSTEM_VA_ASSIGNMENT
{
	VOID* BaseAddress;                                                      //0x0
	ULONGLONG NumberOfBytes;                                                //0x8
} MI_SYSTEM_VA_ASSIGNMENT, * PMI_SYSTEM_VA_ASSIGNMENT;
```




## Locating SystemVaRegions
To proceed, we need to access ```SystemVaRegions``` and cast it to a ```PMI_SYSTEM_VA_ASSIGNMENT``` (array of MI_SYSTEM_VA_ASSIGNMENT structs), which we will iterate to read information about kernel memory ranges. We can find the variable's address dynamically by creating a signature for an instruction that accesses it, and then scanning the ntoskrnl image for that sequence of bytes. To make my experimentation easier, I have hardcoded the offset of the instruction I will resolve.  The variables address is: 
```
ntoskrnl base + offset + instruction length + the address resolved from instruction
```

We have to account for the length of the instruction because it is accessing the variables address relative to the end of the byte sequence rather than the beginning. These are the values for my machine and Windows version:

```
0x0000000140000000 + 0x35AC2C + 10 + 0x008F4D452 = 0x1492A8088
```

 I have written the code to resolve the address and assign our array of ```MI_SYSTEM_VA_ASSIGNMENT``` to ```VARegions``` like so:

```c
 UINT_PTR targetAddy = (UINT_PTR)ntoskrnlBase + 0x35AC2C; // base + offset
 PMI_SYSTEM_VA_ASSIGNMENT VARegions = (PMI_SYSTEM_VA_ASSIGNMENT)((PUCHAR)targetAddy + *(PULONG)((PUCHAR)targetAddy + 6) + 10);
// addy resolution
```

To know which region is which, we can rely on the ```MI_ASSIGNED_REGION_TYPES``` enumeration. Every index from 0 -> 13 (the max) has a corresponding label denoting the name of the region. 

```c=
enum _MI_ASSIGNED_REGION_TYPES
{
	AssignedRegionNonPagedPool = 0,
	AssignedRegionPagedPool = 1,
	AssignedRegionSystemCache = 2,
	AssignedRegionSystemPtes = 3,
	AssignedRegionUltraZero = 4,
	AssignedRegionPfnDatabase = 5,
	AssignedRegionCfg = 6,
	AssignedRegionHyperSpace = 7,
	AssignedRegionKernelStacks = 8,
	AssignedRegionPageTables = 9,
	AssignedRegionSession = 10,
	AssignedRegionSecureNonPagedPool = 11,
	AssignedRegionSystemImages = 12,
	AssignedRegionMaximum = 13
};
```



Finally, we can iterate through our ```VARegions``` array. For now, we are only interested in the base address of each entry.
``` c=
for (int i = 0; i < AssignedRegionMaximum; i++)
{
    msg("[%d]: %I64x\n",
        (i),
        VARegions[i].BaseAddress);
}
```
In the final version of my program, I adjusted this loop with a function to translate from the index # to its corresponding label. Putting these pieces together and running our kernel driver outputs the following:

![image](https://hackmd.io/_uploads/HJZ3KHpG0.png)

We have obtained the base address of every memory region!
## Conclusion

Truth be told, beyond some niche applications, this work is *not* that useful. This is the case with a huge amount of the kernel. However, understanding every  seemingly insignificant aspect together is how great exploits are discovered. Trying earnestly to learn instead of skipping to an exciting conclusion has always yielded me the best results.

I apologize for how rushed this writeup is. I did this exercise and began the writeup many months ago, but then lost motivation and neglected to finish it. With a burst of 2:00 AM inspiration, I have "finished" it now.