
免责声明: 本代码仅供教育和防御性研究目的使用。编写本代码的目的是加深对内核利用的理解,并帮助防御者防范类似漏洞。严禁任何未经授权、非法或恶意使用本项目的行为。
哈希值: ab01485bb7c8bc1a9c86096eeea6d31d8fad557bf4d44072b46373d2203faa6e
驱动名称: pstrip64.sys
CVE: CVE-2026-29923
“自带易受攻击驱动”(BYOVD)攻击是一种古老但非常有效的攻击方式,攻击者利用操作系统仍然官方信任的旧版驱动程序来绕过现代 Windows 安全保护。一旦驱动被加载,攻击者就会利用其缺陷,在标准非特权进程与完全的系统级控制之间架起桥梁。
本周早些时候,pstrip64.sys 驱动中披露了一个新漏洞,编号为 CVE-2026-29923。本文详细介绍了该漏洞利用的整个生命周期:从最初的漏洞研究、概念验证(PoC)的开发,到防御者为保护其环境而应采取的可操作缓解策略。
pstrip64.sys 驱动是一个与 EnTech Taiwan PowerStrip(最高版本 3.90.736)相关的旧版内核模式组件。虽然其合法用途是启用高级显卡显示调整,但其深度系统权限使其成为攻击者的高价值目标。
当该漏洞首次披露时,我开始分析其 DriverEntry 函数。该函数是内核驱动程序的主要初始化例程,用于创建 \Device\PSTRIP64 设备对象,并通过 \DosDevices\PSTRIP64 符号链接将其暴露给用户模式应用程序。更重要的是,它配置了驱动程序的分派表。我立即注意到索引 14(IRP_MJ_DEVICE_CONTROL)处的入口,它将所有用户提供的 IOCTL 请求直接路由到 sub_11340 处理函数,这是我们的主要关注区域。
sub_11340 函数作为主要的 IOCTL 分发器,解释来自用户模式的请求。
在所有暴露的 IOCTL 中,0x80002008 无疑是最有趣的。默认情况处理微小的 I/O 端口交互,而 0x80002008 则充当通往 sub_11000 的门户,通过直接将 SystemBuffer 传递给该函数。
这个 sub_11000 例程是关键。首先,它使用 HalTranslateBusAddress 获取我们用户提供的地址,并将其转换为有效的系统物理地址。然后,它打开 \Device\PhysicalMemory,并使用 ZwMapViewOfSection 将其映射。通过将目标进程句柄硬编码为 (HANDLE)0xFFFFFFFFFFFFFFFFLL(代表 ZwCurrentProcess()),该驱动将此物理内存直接映射到我们调用进程的虚拟地址空间中。关键的是,它随后将这个新映射的虚拟地址写回 SystemBuffer 以返回给用户,从而正式向我们的应用程序提供一个可以直接读写物理内存的指针。
在充分理解漏洞并获得物理读写原语后,我已经拥有了所需的所有拼图碎片。现在,是时候开始编写概念验证(PoC)了。
注意: 该 PoC 专门在 Windows 10 22H2 环境下开发和测试。由于利用依赖于原始物理内存操作,内核结构偏移量和物理内存边界目前针对我的设置是硬编码的。要在你自己的机器上测试,你必须更新 Windows 内核偏移量,并调整物理地址扫描范围,以匹配你的特定操作系统版本和 RAM 配置。
利用的第一步是与驱动程序建立通信。我通过调用 CreateFileA 并传入驱动程序的符号链接(\\.\PSTRIP64)来实现这一点。获得有效句柄后,我需要一种干净的方式来滥用我之前分析的 0x80002008 IOCTL。我创建了一个名为 MapPhysicalMemory() 的包装函数。该函数使用目标物理地址和要读取的内存块长度填充我自定义的 PSTRIP_MAP_REQUEST 结构体。
然后,我通过 DeviceIoControl 将该结构体直接发送给驱动程序。如果成功,驱动程序将该物理内存直接映射到我的用户模式应用程序中,并在 OutputResult 字段中返回虚拟基地址。现在,我可以将这个返回的地址转换为标准 C++ 指针,从而获得对系统物理 RAM 的原始、无特权的访问权限。
在物理读写原语完全运行后,我的目标是找到包含进程权限的内核数据结构。在 Windows 中,每个正在运行的进程都由一个 EPROCESS 结构体表示。
Windows 使用一个特定的 4 字节标识符(称为池标记)在内核池中分配 EPROCESS 结构体。对于进程,该标记是字符串 Proc(十六进制为 0x636F7250)。通过扫描系统的物理 RAM,我可以搜索这个确切的字符串。
我的利用过程从 0x10000000 到 0x140000000 的物理内存空间循环,以 2MB 块(STEP_SIZE = 0x200000)进行映射。我将每个映射的块转换为原始字节数组,并以 16 字节(sizeof(_POOL_HEADER))为步长进行扫描。
然而,仅仅在物理内存中找到 Proc 标记是不够的。内存是混乱的,该标记可能是已终止进程留下的残留物,或者只是恰好匹配该十六进制值的随机数据。如果我盲目地假设每个 Proc 标记都是有效的 EPROCESS 结构体并开始修改内存,我会立即导致蓝屏死机(BSOD)。
为了确保稳定性,我不得不使用启发式方法来验证结构体。首先,我计算 EPROCESS 结构体的起始位置(它位于池标记偏移之后不远处)。然后,我检查运行进程的一些已知常量:
0x2(正常优先级)。0x0。如果所有启发式检查均通过,我可以高度确信我正在查看一个有效的活动进程。然后,我读取其唯一进程 ID(PID)。如果 PID 与我自己的利用进程匹配,我保存其令牌指针的物理地址。如果 PID 为 4(Windows System 进程),我提取并保存其高权限令牌的实际值。
最后,我将我的进程令牌指针的物理地址对齐到最近的 4KB 边界,并使用 MapPhysicalMemory() 最后一次映射该特定页面。
接下来,我导航到确切的偏移量,并用 System 令牌值覆盖我的令牌。瞬间,Windows 内核将我的利用进程视为 NT AUTHORITY\SYSTEM。
在取消映射页面以确保系统稳定性后,我只需调用 CreateProcessA 来生成 cmd.exe。由于我当前进程已提升权限,新的命令提示符继承了这些顶级特权,从而成功完成攻击!
注意: 我在早期调试阶段发现的一个关键细节是驱动程序如何处理映射的指针。通过执行 SystemBuffer->LowPart = (unsigned int)BaseAddress;,驱动程序在返回之前将 64 位虚拟基地址转换为 32 位值。这种截断丢失了地址的高位部分,导致我在 64 位利用中尝试解引用时立即出现访问冲突。为了干净地绕过这个问题,我简单地将用户模式 PoC 编译为 32 位应用程序,从而确保返回的指针保持完全有效。
注意: 在初始测试期间,我遇到了一个有趣的边缘情况:我的 PoC 成功在内存中找到了我的利用进程,但未能找到 System 进程(PID 4)。
为了理解原因,我需要直接检查物理内存。我附加了一个内核调试器(WinDbg),并使用命令检索 System 进程的虚拟地址和目录基址。然后,我使用 !vtop 将该虚拟地址转换为其在 RAM 中的确切物理地址。
我切换回附加到 PoC 的用户模式调试器。我在内存扫描循环上设置了一个条件断点,指示它在我的 MapPhysicalMemory() 函数抓取包含 System 进程物理地址的 2MB 块时暂停执行。
一旦断点被触发,我开始手动检查映射内存的原始字节。在这里,我发现了关于 Windows 内核池分配的一个关键细节。
当 Windows 为进程分配内存时,它以 _POOL_HEADER(包含我们的 Proc 标记)开头,然后是 _OBJECT_HEADER,最后是 EPROCESS 结构体本身。对于标准用户模式应用程序,这些头部包含额外的跟踪数据,这意味着实际的 EPROCESS 结构体在池标记之后 0x80 字节处开始。
然而,检查 System 进程的内存揭示了不同的布局。System 进程缺少一些标准跟踪头部。从 Proc 标记到 EPROCESS 结构体起始位置的偏移仅为 0x40 字节!
修复方法很简单。我更新了 PoC,使其在处理 Proc 标记时,通过循环遍历一个可能的偏移量数组(0x40 和 0x80)来处理两种池头部大小。
网络安全是一场攻击者与防御者之间永无止境的猫鼠游戏。虽然攻击者不断寻找易受攻击的驱动程序,但现代安全产品和蓝队有多种可靠的方法来检测和阻止这种精确操作。
阻止 BYOVD(自带易受攻击驱动)攻击最有效的方法是首先阻止驱动程序加载。
如果驱动程序已加载,安全产品仍可在令牌操作阶段检测到利用行为。
NT AUTHORITY\SYSTEM 是一个巨大的危险信号。cmd.exe)的情况,尤其是当父进程没有理由以 SYSTEM 身份运行时。