While I was cruising along, taking in the views of the kernel landscape, I received a challenge …
1. Player 2 has entered the game
The past weeks I mostly experimented with existing tooling and got acquainted with the basics of kernel driver development. I managed to get a quick win versus $vendor1 but that didn’t impress our blue team, so I received a challenge to bypass $vendor2. I have to admit, after trying all week to get around the protections, $vendor2 is definitely a bigger beast to tame.
I foolishly tried to rely on blocking the kernel callbacks using the Evil driver from my first post and quickly concluded that wasn’t going to cut it. To win this fight, I needed bigger guns.
2. Know your enemy
$vendor2’s defenses consist of a number of driver modules:
- eamonm.sys (monitoring agent?)
- edevmon.sys (device monitor?)
- eelam.sys (early launch anti-malware driver)
- ehdrv.sys (helper driver?)
- ekbdflt.sys (keyboard filter?)
- epfw.sys (personal firewall driver?)
- epfwlwf.sys (personal firewall light-weight filter?)
- epfwwfp.sys (personal firewall filter?)
and a user mode service: ekrn.exe ($vendor2 kernel service) running as a System Protected Process (enabled by eelam.sys driver).
At this stage I am only guessing the roles and functionality of the different driver modules based on their names and some behaviour I have observed during various tests, mainly because I haven’t done any reverse-engineering yet. Since I am interested in running malicious binaries on the protected system, my initial attack vector is to disable the functionality of the ehdrv.sys
, epfw.sys
and epfwwfp.sys
drivers. As far as I can tell using WinObj and listing all loaded modules in WinDbg (lm
command), epfwlwf.sys
does not appear to be running and neither does eelam.sys
, which I presume is only used in the initial stages when the system is booting up to start ekrn.exe
as a System Protected Process.

In the context of my internship being focused on the kernel, I have not (yet) considered attacking the protected ekrn.exe
service. According to the Microsoft Documentation, a protected process is shielded from code injection and other attacks from admin processes. However, a quick Google search tells me otherwise 😉
3. Interceptor
With my eye on the ehdrv.sys
, epfw.sys
and epfwwfp.sys
drivers, I noticed they all have registered callbacks, either for process creation, thread creation, or both. I’m still working on expanding my own driver to include callback functionality, which will also look at image load callbacks, which are used to detect the loading of drivers and so on. Luckily, the Evil driver has got this angle (partially) covered for now.

Unfortunately, we cannot solely rely on blocking kernel callbacks. Other sources contacting the $vendor2 drivers and reporting suspicious activity should also be taken into consideration. In my previous post I briefly touched on IRP MajorFunction hooking, which is a good -although easy to detect- way of intercepting communications between drivers and other applications.
I wrote my own driver called Interceptor, which combines the ideas of @zodiacon’s Driver Monitor project and @fdiskyou’s Evil driver.
To gather information about all the loaded drivers on the system, I used the AuxKlibQueryModuleInformation()
function. Note that because I return output via pass-by-reference parameters, the calling function is responsible for cleaning up any allocated memory and preventing a leak.
NTSTATUS ListDrivers(PAUX_MODULE_EXTENDED_INFO& outModules, ULONG& outNumberOfModules) {
NTSTATUS status;
ULONG modulesSize = 0;
PAUX_MODULE_EXTENDED_INFO modules;
ULONG numberOfModules;
status = AuxKlibInitialize();
if(!NT_SUCCESS(status))
return status;
status = AuxKlibQueryModuleInformation(&modulesSize, sizeof(AUX_MODULE_EXTENDED_INFO), nullptr);
if (!NT_SUCCESS(status) || modulesSize == 0)
return status;
numberOfModules = modulesSize / sizeof(AUX_MODULE_EXTENDED_INFO);
modules = (AUX_MODULE_EXTENDED_INFO*)ExAllocatePoolWithTag(PagedPool, modulesSize, DRIVER_TAG);
if (modules == nullptr)
return STATUS_INSUFFICIENT_RESOURCES;
RtlZeroMemory(modules, modulesSize);
status = AuxKlibQueryModuleInformation(&modulesSize, sizeof(AUX_MODULE_EXTENDED_INFO), modules);
if (!NT_SUCCESS(status)) {
ExFreePoolWithTag(modules, DRIVER_TAG);
return status;
}
//calling function is responsible for cleanup
//if (modules != NULL) {
// ExFreePoolWithTag(modules, DRIVER_TAG);
//}
outModules = modules;
outNumberOfModules = numberOfModules;
return status;
}
Using this function, I can obtain information like the driver’s full path, its file name on disk and its image base address. This information is then passed on to the user mode application (InterceptorCLI.exe) or used to locate the driver’s DriverObject
and MajorFunction array so it can be hooked.
To hook the driver’s dispatch routines, I still rely on the ObReferenceObjectByName()
function, which accepts a UNICODE_STRING
parameter containing the driver’s name in the format \\Driver\\DriverName
. In this case, the driver’s name is derived from the driver’s file name on disk: mydriver.sys
–> \\Driver\\mydriver
.
However, it should be noted that this is not a reliable way to obtain a handle to the DriverObject
, since the driver’s name can be set to anything in the driver’s DriverEntry()
function when it creates the DeviceObject
and symbolic link.
Once a handle is obtained, the target driver will be stored in a global array and its dispatch routines hooked and replaced with my InterceptGenericDispatch()
function. The target driver’s DriverObject->DriverUnload
dispatch routine is separately hooked and replaced by my GenericDriverUnload()
function, to prevent the target driver from unloading itself without us knowing about it and causing a nightmare with dangling pointers.
NTSTATUS InterceptGenericDispatch(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
UNREFERENCED_PARAMETER(DeviceObject);
auto stack = IoGetCurrentIrpStackLocation(Irp);
auto status = STATUS_UNSUCCESSFUL;
KdPrint((DRIVER_PREFIX "GenericDispatch: call intercepted\n"));
//inspect IRP
if(isTargetIrp(Irp)) {
//modify IRP
status = ModifyIrp(Irp);
//call original
for (int i = 0; i < MaxIntercept; i++) {
if (globals.Drivers[i].DriverObject == DeviceObject->DriverObject) {
auto CompletionRoutine = globals.Drivers[i].MajorFunction[stack->MajorFunction];
return CompletionRoutine(DeviceObject, Irp);
}
}
}
else if (isDiscardIrp(Irp)) {
//call own completion routine
status = STATUS_INVALID_DEVICE_REQUEST;
return CompleteRequest(Irp, status, 0);
}
else {
//call original
for (int i = 0; i < MaxIntercept; i++) {
if (globals.Drivers[i].DriverObject == DeviceObject->DriverObject) {
auto CompletionRoutine = globals.Drivers[i].MajorFunction[stack->MajorFunction];
return CompletionRoutine(DeviceObject, Irp);
}
}
}
return CompleteRequest(Irp, status, 0);
}
void GenericDriverUnload(PDRIVER_OBJECT DriverObject) {
for (int i = 0; i < MaxIntercept; i++) {
if (globals.Drivers[i].DriverObject == DriverObject) {
if (globals.Drivers[i].DriverUnload) {
globals.Drivers[i].DriverUnload(DriverObject);
}
UnhookDriver(i);
}
}
NT_ASSERT(false);
}
4. Early bird gets the worm
Armed with my new Interceptor driver, I set out to try and defeat $vendor2 once more. Alas, no luck, mimikatz.exe
was still detected and blocked. This got me thinking, running such a well-known malicious binary without any attempts to hide it or obfuscate it is probably not realistic in the first place. A signature check alone would flag the binary as malicious. So, I decided to write my own payload injector for testing purposes.
Based on research presented in An Empirical Assessment of Endpoint Detection and Response Systems against Advanced Persistent Threats Attack Vectors by George Karantzas and Constantinos Patsakis, I chose for a shellcode injector using:
– the EarlyBird code injection technique
– PPID spoofing
– Microsoft’s Code Integrity Guard (CIG) enabled to prevent non-Microsoft DLLs from being injected into our process
– Direct system calls to bypass any user mode hooks.
The injector delivers shellcode to fetch a “windows/x64/meterpreter/reverse_tcp” payload from the Metasploit framework.
Using my shellcode injector, combined with the Evil driver to disable kernel callbacks and my Interceptor driver to intercept any IRPs to the ehdrv.sys
, epfw.sys
and epfwwfp.sys
drivers, the meterpreter payload is still detected but not blocked by $vendor2.

5. Conclusion
In this blogpost, we took a look at a more advanced Anti-Virus product, consisting of multiple kernel modules and better detection capabilities in both user mode and kernel mode. We took note of the different AV kernel drivers that are loaded and the callbacks they subscribe to. We then combined the Evil driver and the Interceptor driver to disable the kernel callbacks and hook the IRP dispatch routines, before executing a custom shellcode injector to fetch a meterpreter reverse shell payload.
Even when armed with a malicious kernel driver, a good EDR/AV product can still be a major hurdle to bypass. Combining techniques in both kernel and user land is the most effective solution, although it might not be the most realistic. With the current approach, the Evil driver does not (yet) take into account image load-, registry- and object creation callbacks, nor are the AV minifilters addressed.
About the authors
Sander (@cerbersec), the main author of this post, is a cyber security student with a passion for red teaming and malware development. He’s a two-time intern at NVISO and a future NVISO bird.
Jonas is NVISO’s red team lead and thus involved in all red team exercises, either from a project management perspective (non-technical), for the execution of fieldwork (technical), or a combination of both. You can find Jonas on LinkedIn.
One thought on “Kernel Karnage – Part 3 (Challenge Accepted)”