Hiding Drivers on Windows 10

Disclaimer: All experiments and development were performed on Windows 10 x64 (Version 1703, Build 15063.540). Any attempt to replicate results on a different version or operating system may yield inconsistent results.

—–

It’s a pretty common objective in the game hacking community to load and attempt to hide their kernel modules from prying eyes. The majority of methods leave artifacts, or cause bugchecks at random times which makes for an unreliable evasion of an anti-cheat.

Let’s cover the details of driver loading, initialization, and what happens underneath the hood. That way when we detail the techniques used to remove entries from protected resources, hide initialization, and bypass certain verification flags it all makes sense.

Flow of NtLoadDriver

 

Figure 1: Simplified flow of NtLoadDriver
NtLoadDriver is a system routine defined in ntoskrnl that loads a driver into the system by using the DriverServiceName as specified in the registry path to initialize and load. In Figure 1 above you’ll notice that once NtLoadDriver is called it directly wraps IopLoadDriverImage (no API reference since it’s a private routine.) Inside of IopLoadDriverImage we see various system routines used, most notably the use of IopLoadDriver which contains the system routine of interest: MmLoadSystemImageEx. This is where the magic happens. Inside of MmLoadSystemImageEx the system creates a driver section, the driver section is the section object that your drive occupies and can be referenced by using the DriverSection field in _DRIVER_OBJECT. After the driver section is created, a loader entry is created – much like the loader entries in usermode that are stored in the PEB and obtained by iterating a linked list the kernel stores loader entries using the _KLDR_DATA_TABLE_ENTRY structure in a global structure called PsLoadedModuleList. The loader entry constructed for a driver is actually what is mapped into the driver section, so you can cast the DriverSection field to PKLDR_DATA_TABLE_ENTRY and modify fields in the driver loader entry. This is important to note because some modifications in the driver loader entry allow us to play tricks on the OS.

Following the figure, after the loader entry is constructed and driver section mapped, the OS inserts the driver loader entry pointer into the PsLoadedModuleList. The PsLoadedModuleList is a kernel global, specifically an array of pointers to all loaded kernel modules on the system. It’s protected by PatchGuard, so any direct modifications the list will result in a structure corruption bugcheck. After the entry is inserted the OS maps the system image, resolves imports, and other references, and then cleans up any unused resources.

Now how does knowing the flow of NtLoadDriver help us with hiding our drivers? As mentioned above there are multiple global information structures that receive information during driver initialization and variables that determine if certain operations are performed following driver load and initialization. The most important of which are the PsLoadedModuleList, MmVerifierData, and PspNotifyEnableMask. These globals can be modified indirectly through undocumented functionality on Windows 8.1+. The details on these three globals are provided below, which is followed by methods of abusing these globals using internal system routines and modifying driver related data.


Important NT Globals

PsLoadedModuleList
A global array of pointers to currently loaded kernel modules on the given system. This list is protected by PatchGuard and cannot be directly modified without triggering a bugcheck. All entries in this list are of type _KLDR_DATA_TABLE_ENTRY.

PspNotifyEnableMask
This is a 32-bit integer mask that is used to determine what types of callbacks will be called depending on bits set in the lower byte of the mask. Bits 0, 1, and 3 determine if CreateProcess, CreateThread, and LoadImage callbacks are fired. This variable is not protected by PatchGuard and is subject to modification.

MmVerifierData
Global array of driver information stored as _MI_VERIFIER_DRIVER_ENTRY by the routine MiApplyDriverVerifier. This data is stored so that NTOS can verify that all drivers are valid and able to load. This is a simplified explanation of it’s use because it’s all we’ll need to know for what we’ll be doing.

 

Abusing Globals
There’s a simple way to allow process notification callbacks to be registered, but never fire. I’m sure after reading the purpose of the mask above and the bits which determine functionality you’re aware of what to do now.

A simple zero’ing of the PspNotifyEnableMask will prevent all process notification callbacks from firing without tampering with the callbacks themselves.

*( ULONG * )( PspNotifyEnableMask ) = 0; // Game over for process notification callbacks

The verifier data array will require you to traverse the array and find your drivers base name, once you’ve found the index of the entry in MmVerifierData, remove it, shift all entries down an index, and move along to the next global commonly accessed for driver information: PsLoadedModuleList.

There are various ways to remove loaded driver information from globals, the most effective approach I’ve used was simply taking advantage of an already existing routine in the kernel, MiProcessLoaderEntry. This routine is called as part of MmLoadSystemImageEx and is responsible for inserting the loader entry (or removing it) from the PsLoadedModuleList. If a developer’s objective is to remove the entry from the global array then all he/she has to do is call MiProcessLoaderEntry the following way:

MiProcessLoaderEntry( ( PKLDR_DATA_TABLE_ENTRY )( DriverLdrEntry ), FALSE );

The second parameter of MiProcessLoaderEntry determines if the entry will be added or removed from the PsLoadedModuleList.

There are other areas that may contain artifacts of driver loading or initialization. To completely hide it would be wise to seek them out and remove any traces. However, most kernel AC’s are not that advanced and only utilize the common kernel structures that are supposed to be unmodifiable. To hide in a more complete manner simply destroy driver object features by simply NULL’ing the following DriverObject fields:

  1. DriveSection
  2. DriverStart
  3. DriverUnload
  4. DriverInit
  5. DeviceObject

 

Note: NULL’ing specific driver object fields can result in system instability. Primarily zeroing the DriverSection field because it will cause an exception handling issue as well as prevent loads of system routines from working.

There are plenty of other exploits that I’ll be writing about since I’ve picked back up on research and development. There’s a new one that seems to be present on all editions from Windows 7 and up, it allows for a specific override of an otherwise important field for all drivers on a machine allowing modification of other drivers at any level. I’m excited to write an article on it and hope it will prove useful to other driver authors.

Bonus: MmUnloadedDrivers
Though not new or unheard of at this point since it was publicized on a forum less than a week ago I had been removing entries from the global MmUnloadedDrivers which is an array of base driver names that have since been unloaded from the system. In the event you have unloaded a driver from your system that may trigger an anti-cheats system integrity checks (TDL, specifically) you iterate the unloaded drivers array and perform a search for the target driver name, remove the entry at the specific index, and shift all entries down an index which makes it appear as if it was never there.

for (i = 0; i < MI_UNLOADED_DRIVERS; i += 1) {
    if (Index >= MI_UNLOADED_DRIVERS) {
        Index = MI_UNLOADED_DRIVERS - 1;
    }
    Entry = &MmUnloadedDrivers[ Index ];
    if ( !wcscmp( Entry->Name.Buffer, MmUnloadedDrivers[ Index ].Buffer ) ) {
        // remove entry from MmUnloadedDrivers
        }
    }
}

Other Detection Vectors
There are tons of other detection vectors (though unreliable) that may be used by kernel anti-cheats. The following list is a few I’ve thought of and will likely do testing on in the future.

  1. ObjectDirectory
  2. ServiceDatabase
  3. Registry Service Database
  4. Signatures
  5. Pool-tag scanning
  6. High entropy symbol blocks

As always leave a comment, question, or feedback! I’m always interested in new ideas or thoughts on a topic.

7 thoughts on “Hiding Drivers on Windows 10

Leave a Reply