Building Our Navigation System in the Kernel

Automation is key.

After being familiar with the relevant structures, and after paving all the malicious paths we would like to take using our vulnerable driver, we already realized that a lot of the specific objects we would like to access or modify would be contained in a different verity of offsets in different types of objects, as part of a list or a doubly linked list etc.

For example, in order to access a specific handle inside the handle table, we would need to first find an EPROCESS address, use its object in order to iterate all EPROCESS objects via ActiveProcessLinks, while manually sampling the EPROCESS PID while doing so in order to find the desired process object.

After finding the EPROCESS, we would need to enter the ObjectTable and iterate over all _HANDLE_TABLE_ENTRY object while sampling the handle index value in order to confirm we have found the right one. Now to the major problem, how a user mode app can navigate and successfully enumerate objects in a space where it not belongs to Where the only access it’s got is by using vulnerable driver?

Kernel modules usually are written with the correct WinApi libs which allow for a driver to be written, those API calls provided by Microsoft to driver developers are not available to an actor in the user mode, all we have is read /write! The answer was not straight forward since it relay on important aspects of our attacks: Most of the values modified are related to a process, and we remember that each EPROCESS struct is linked with all the other existing _EPROCESS, so once we’ve the address of only one EPROCESS struct, we can iterate through all of them, and extract / modify whatever we want. But the way to get an _EPROCESS address to begin with is also not so clear as the sky when you’re attacking through user mode app. We’ll continue this section by explaining how we managed to build a set of functions that allowed us to navigate and locate our target objects.

A key point to remember about moving inside the memory: since we do not hold pointers to objects per say, rather navigate through the memory and access the structs in an abstract way, and in order to always begin reading/ writing from the correct location, it is important to understand the concept of Addresses + Offsets. Each object has a starting address, and in each version of the kernel that object will have a fixed size, fixed list of members, and those members have fixed sizes as well.

As such, if one knows the starting address of an object, and the offset inside the object in which its desired member resides, and also the member size, he will be able to read or write from it correctly.

Failing to be precise on any of those said parameters would result in BSOD almost always. In our GitHub repo you would find a python script which will take a copy of the relevant ntoskrnl.exe , download its PDB , and extract a CSV of the relevant offsets needed for that navigation.

Getting the System Process _EPROCESS Address:

Our first target by building a navigation system is to get the main object that will be used during most attacks, _EPROCESS. While researching methods to get an _EPROCESS, we encountered with the following exported symbol PsInitialSystemProcess. This symbol is a kernel global variable which points to the EPROCESS of the system process.

After checking the export table of ntoskrnl.exe we realized that this symbol is exported, and as such we can always get its offset programmatically by using LoadLibrary and GetProcAddress just like every exported symbol on an executable.

Now to the code and the bottom-line, the function indeed return the address of System Process (PID 4), let’s jump to the code to get clear picture of how we did it:

In Order to receive the absolute address of PsInitialSystemProcess in memory, and not only the offset, we have implemented the following calculation: The symbol address in user mode – ntoskrnl address in user mode to get a clean offset + the Kernel base address is kernel mode. As you may guess the variable kernelBase is the base address of the kernel memory, and we can get it with another function implemented as part of our navigation system:

These 5 lines function return to us the base address of the kernel memory. The WinApi function EnumDeviceDrivers is used to get a list of all the base address of the drivers running in our machine, and fun fact, the base address of the first driver is the same base address of the kernel memory. Chain everything together with a read operation from the kernel and we get the address of System Process (PID 4) _EPROCESS in the kernel.

Locating _EPROCESS by PID:

Once we got the address of the _EPROCESS object related to system process, we can start iterating through all the _EPROCESS existing in our kernel. As explained earlier, EPROCESS is part of a bidirectional list, therefore when we get one _EPROCESS we can iterate through each one of them, by using the struct _LIST_ENTRY. return to the Important Structures and Attributes section if you’re not familiar with the structure. Back to our iteration method, we know that the structure _LIST_ENTRY in _EPROCESS is implemented as a member called ActiveProcessLinks, so in theory, we can get the next process on the list, check its PID, and return its base address if it is indeed the required process. If not, the code will extract the next process in a similar way and do the test, this iteration will be done until the target process will be found by its PID:

In the snippet above we start from the System Process (called papaProc), and by checking the UniqueProcessId member within the EPROCESS determined if it is our target process.

Note how we subtracted the offset of ActiveProcessLinks on each iteration to acquire the base of the current EPROCESS structure.

Locating _ETHREAD by CID:

As done with iterating through all the existing _EPROCESS, within the structure we can find a member that was mentioned earlier and just did us an “easy” work, ThreadListHead, holds the pointer to the first _ETHREAD object of the process, and as mentioned earlier, the member itself is a _LIST_ENTRY struct which linked together all the process threads. So by getting the address of the first thread, we managed to iterate through all threads and get our target thread:

Notice that in the function above we used a struct called CLIENT_ID, this structure used as an identifier of a thread and contains 2 members: - UniqueProcess – the PID which own the thread - UniqueThread – The TID of the thread used to identify it between all the process threads. And as can be seen in the snippet above, we iterate through each thread of the process, checking if it actually related to the process we targeting and if the TID is the same of the thread we also target. Using the function above we now able to iterate through all process’s threads.

Locating Specific Handles Within the _HANDLE_TABLE Using Undocumented Functions:

After long research and attempts to iterate over handles of a process by only using the _LIST_ENTRY struct implemented in each _HANDLE_TABLE_ENTRY, we noticed that we don’t reaching the correct address in memory. After a lot of hunting for the correct method, we encountered with an undocumented and un-exported function that is an internal procedure inside the kernel for locating a handle. After implementing the function in our code and make some small changes in the decompiled version of the function, we manage to get back in return the addresses of the _HANDLE_TABLE_ENTRYs, with the _HANDLE_TABLE of a process using only a pointer to the Table, and the Handle index:

This snippet eventually allowed us to iterate through the handle table just like the kernel itself does while doing so from the user mode without accessing that un-exported function.

To summarize this section, by acquiring the _EPROCESS of System process (PID 4) we manage to iterate on all processes and their threads, with the functionality also to locate specific handles within the kernel. You’ll notice furthermore that most of the navigation we need to do during our attacks will be based on iterating and getting the correct _EPROCESS, and most of the time adding the offset of the required member to the base address (the address of the target _EPROCESS) will get us to the correct place. Now it sound pretty easy, just grab the offset and the base address, but getting to those conclusions and methods required from us to read a lot of code from different objects, understanding internals of structures in the kernel, how they operate and what is the correct method to access or read values stored in the structures members.

Last updated