-
-
[原创]Windows不附加进程从而读写物理地址
-
发表于: 2小时前 79
-
最近回顾学习,复习到了这个知识点。
在 Windows 内核安全与反作弊对抗(驱动开发)领域,内存读写是最基础也是最核心的技术。传统的读写方式通常依赖于 KeStackAttachProcess 将当前线程附加到目标进程的上下文中,然后直接使用 memcpy 或 RtlCopyMemory 进行数据拷贝。
然而,在高级的安全对抗中,“附加进程”这一操作会留下明显的痕迹(如修改跨进程上下文、触发某些回调等),极易被安全软件或反作弊系统(EAC/BE等)拦截或记录。
那么,有没有一种方法可以完全不改变进程上下文,静悄悄地读取目标进程的内存呢?答案就是:手动解析多级页表,直接读取物理内存(Physical Memory)。
本文将带你从零开始,详细解析并实现两套完整的“无附加物理内存读写”方案。
一、核心思路:从虚拟地址转换为物理地址
在 64 位 Windows 系统中,每个进程都有自己独立的虚拟地址空间。我们平时在 Cheat Engine 或代码中看到的地址(如 0x7ff600000000),都是虚拟地址(Virtual Address, VA)。
虚拟地址本身并不存储数据,它必须通过 CPU 的页表机制翻译成内存条上的物理地址(Physical Address, PA)。这个翻译过程由 CPU 的 CR3 寄存器(保存了当前进程页表的物理根地址)主导,经过四级页表层层深入:
PML4 (Page Map Level 4) - 第 1 级
PDPT (Page Directory Pointer Table) - 第 2 级
PDE (Page Directory Entry) - 第 3 级
PTE (Page Table Entry) - 第 4 级
我们的整体思路就是:在内核驱动中,获取目标进程的 CR3 寄存器值,然后模仿 CPU 的行为,手动一层一层地拨开这四级页表,算出最终的物理地址,最后直接对这块物理内存进行读写!
二、 源码解析:四大核心模块
为了彻底掌握这项技术,我这里将代码分为四个关键部分进行拆解。
模块 1:如何找到目标进程?
要操作内存,首先要获取目标进程的 ID。虽然系统提供了 ZwQuerySystemInformation 等 API,但为了底层和隐蔽,我们通过遍历内核的 EPROCESS 链表来实现。
// 辅助函数:通过进程名获取 PID
int GetProcessIdByImageFileName(char* TargetImageName) {
// 1. 获取当前系统进程的 EPROCESS 对象
PEPROCESS ProcessObject = PsGetCurrentProcess();
// 2. 根据硬编码偏移,找到 ActiveProcessLinks(活动进程双向链表)
// 注意:0x188 是特定 Windows 版本的偏移,不同系统版本可能不同
PLIST_ENTRY ListStart = (PLIST_ENTRY)((PUCHAR)ProcessObject + 0x188);
PLIST_ENTRY ListCurrent = ListStart;
do {
// 3. 通过链表节点地址,减去偏移,反推当前进程 EPROCESS 的首地址
PEPROCESS Entry = (PEPROCESS)((PUCHAR)ListCurrent - 0x188);
// 4. 获取进程名 (偏移 0x2e0) 并比较
if (_stricmp((char*)((PUCHAR)Entry + 0x2e0), TargetImageName) == 0) {
// 5. 如果名字匹配,返回对应的 PID (偏移 0x180)
return (int)*(PULONG64)((PUCHAR)Entry + 0x180);
}
// 移动到下一个链表节点
ListCurrent = ListCurrent->Flink;
} while (ListCurrent != ListStart); // 遍历一圈回到起点则结束
return 0; // 没找到
}Windows 内核将所有正在运行的进程用一个双向链表(ActiveProcessLinks)串了起来。我们只需要拿到其中任意一个进程,顺藤摸瓜遍历一圈,对比 ImageFileName,就能找到我们的目标(如 calc.exe)。
模块 2:方案一:基于 MmMapIoSpace 的优雅解析法
这是官方推荐且最稳定的物理内存映射方法。它的核心思想是:拿到一段物理地址后,调用 MmMapIoSpace 将它临时映射到内核的虚拟地址空间供我们读取,读完立刻用 MmUnmapIoSpace 释放。
ULONG64 GetPhysicalAddressByMmMap(int ProcessId, ULONG64 VirtualAddress) {
PEPROCESS ProcessObject = NULL;
// 1. 根据进程ID获取进程对象
NTSTATUS Status = PsLookupProcessByProcessId((HANDLE)ProcessId, &ProcessObject);
if (!NT_SUCCESS(Status)) {
return 0;
}
// 2. 获取该进程的页目录基地址 (Directory Table Base),也就是 CR3 寄存器的值
// 偏移量 0x28 对应的是 64 位系统下 EPROCESS 结构中的 DirectoryTableBase
ULONG64 DirectoryTableBase = *(PULONG64)((PUCHAR)ProcessObject + 0x28);
// 使用完 ProcessObject 后,减少引用计数
ObDereferenceObject(ProcessObject);
// 3. 拆解虚拟地址。将 64 位虚拟地址拆解为 4 级索引和 1 个页内偏移
// 每级索引占据虚拟地址中的 9 位 (掩码为 0x1FF)
unsigned short Pml4Index = (VirtualAddress >> 39) & 0x1FF; // 第1级:Page Map Level 4
unsigned short DirectoryPointerIndex = (VirtualAddress >> 30) & 0x1FF; // 第2级:Page Directory Pointer
unsigned short DirectoryIndex = (VirtualAddress >> 21) & 0x1FF; // 第3级:Page Directory
unsigned short TableIndex = (VirtualAddress >> 12) & 0x1FF; // 第4级:Page Table
ULONG64 PageOffset = VirtualAddress & 0xFFF; // 最后 12 位:页内偏移量
PHYSICAL_ADDRESS TargetPhysicalAddress;
// --- 第一步:映射并读取 PML4 表 (第1级) ---
TargetPhysicalAddress.QuadPart = DirectoryTableBase;
PULONG64 Pml4Table = (PULONG64)MmMapIoSpace(TargetPhysicalAddress, PAGE_SIZE, MmNonCached);
// 提取指向下一级表(Directory Pointer Table)的物理基地址,并抹除属性位
ULONG64 DirectoryPointerTablePhysicalAddress = Pml4Table[Pml4Index] & 0x0000FFFFFFFFF000;
MmUnmapIoSpace(Pml4Table, PAGE_SIZE); // 映射完必须立即释放
if (DirectoryPointerTablePhysicalAddress == 0) return 0;
// --- 第二步:映射并读取 Page Directory Pointer 表 (第2级) ---
TargetPhysicalAddress.QuadPart = DirectoryPointerTablePhysicalAddress;
PULONG64 DirectoryPointerTable = (PULONG64)MmMapIoSpace(TargetPhysicalAddress, PAGE_SIZE, MmNonCached);
// 提取指向下一级表(Page Directory Table)的物理基地址
ULONG64 DirectoryTablePhysicalAddress = DirectoryPointerTable[DirectoryPointerIndex] & 0x0000FFFFFFFFF000;
MmUnmapIoSpace(DirectoryPointerTable, PAGE_SIZE);
if (DirectoryTablePhysicalAddress == 0) return 0;
// --- 第三步:映射并读取 Page Directory 表 (第3级) ---
TargetPhysicalAddress.QuadPart = DirectoryTablePhysicalAddress;
PULONG64 DirectoryTable = (PULONG64)MmMapIoSpace(TargetPhysicalAddress, PAGE_SIZE, MmNonCached);
// 提取指向下一级表(Page Table)的物理基地址
ULONG64 PageTablePhysicalAddress = DirectoryTable[DirectoryIndex] & 0x0000FFFFFFFFF000;
MmUnmapIoSpace(DirectoryTable, PAGE_SIZE);
if (PageTablePhysicalAddress == 0) return 0;
// --- 第四步:映射并读取 Page Table 表 (第4级) ---
TargetPhysicalAddress.QuadPart = PageTablePhysicalAddress;
PULONG64 PageTable = (PULONG64)MmMapIoSpace(TargetPhysicalAddress, PAGE_SIZE, MmNonCached);
// 提取最终的物理页基地址
ULONG64 FinalPagePhysicalAddress = PageTable[TableIndex] & 0x0000FFFFFFFFF000;
MmUnmapIoSpace(PageTable, PAGE_SIZE);
if (FinalPagePhysicalAddress == 0) return 0;
// 最终物理地址 = 物理页基地址 + 虚拟地址中的页内偏移
return FinalPagePhysicalAddress + PageOffset;
}这段代码模拟了 CPU 寻址过程。需要注意的是 & 0x0000FFFFFFFFF000 这个按位与操作。因为页表项里不仅存了物理地址,低 12 位还存了这个页的属性(是否只读、是否在内存中等),我们必须把它“屏蔽”掉,才能拿到纯净的物理地址。
模块 3:方案二:基于 ZwMapViewOfSection 的底层硬核法
除了使用 API 映射,Windows 还把整个物理内存抽象成了一个名为 \Device\PhysicalMemory 的设备对象(Section段对象)。我们可以打开它,并像操作文件一样映射物理内存。
ULONG64 GetPhysicalAddressBySection(int ProcessId, ULONG64 VirtualAddress) {
PEPROCESS ProcessObject = NULL;
PsLookupProcessByProcessId((HANDLE)ProcessId, &ProcessObject);
ULONG64 DirectoryTableBase = *(PULONG64)((PUCHAR)ProcessObject + 0x28);
ObDereferenceObject(ProcessObject);
// 1. 初始化物理内存对象路径
HANDLE SectionHandle = NULL;
UNICODE_STRING PhysicalMemoryDeviceName;
OBJECT_ATTRIBUTES ObjectAttributes;
RtlInitUnicodeString(&PhysicalMemoryDeviceName, L"\\Device\\PhysicalMemory");
// 注意:OBJ_KERNEL_HANDLE 保证了句柄在内核中是安全的
InitializeObjectAttributes(&ObjectAttributes, &PhysicalMemoryDeviceName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
// 2. 打开物理内存段对象的只读权限
NTSTATUS Status = ZwOpenSection(&SectionHandle, SECTION_MAP_READ, &ObjectAttributes);
if (!NT_SUCCESS(Status)) return 0;
// 3. 准备索引数组(展开所有缩写)
ULONG64 Indices[4];
Indices[0] = (VirtualAddress >> 39) & 0x1FF; // Page Map Level 4 Index
Indices[1] = (VirtualAddress >> 30) & 0x1FF; // Page Directory Pointer Index
Indices[2] = (VirtualAddress >> 21) & 0x1FF; // Page Directory Index
Indices[3] = (VirtualAddress >> 12) & 0x1FF; // Page Table Index
ULONG64 PageOffset = VirtualAddress & 0xFFF;
ULONG64 CurrentPhysicalAddress = DirectoryTableBase;
PVOID MappedBaseAddress = NULL;
SIZE_T ViewSize = PAGE_SIZE;
LARGE_INTEGER SectionOffset;
// 4. 开始逐级映射并解析
for (int i = 0; i < 4; i++) {
MappedBaseAddress = NULL;
ViewSize = PAGE_SIZE;
SectionOffset.QuadPart = CurrentPhysicalAddress;
// 将物理内存中的某一页映射到当前内核进程的虚拟地址空间
Status = ZwMapViewOfSection(
SectionHandle, // 物理内存对象句柄
NtCurrentProcess(), // 映射到当前进程
&MappedBaseAddress, // 映射后的虚拟地址存放在这里
0,
PAGE_SIZE, // 映射大小为一页
&SectionOffset, // 物理内存中的偏移
&ViewSize,
ViewUnmap,
0,
PAGE_READONLY // 只读映射
);
if (!NT_SUCCESS(Status)) {
ZwClose(SectionHandle);
return 0;
}
// 将映射出来的地址看作一个 64 位数组
PULONG64 TableArray = (PULONG64)MappedBaseAddress;
// 从当前表中取出下一级表的物理基地址,并抹除属性位 (掩码 0xFFFFFFFFF000)
CurrentPhysicalAddress = TableArray[Indices[i]] & 0x0000FFFFFFFFF000;
// 重要:读取完必须立即取消映射,否则会导致内核内存溢出
ZwUnmapViewOfSection(NtCurrentProcess(), MappedBaseAddress);
if (CurrentPhysicalAddress == 0) break;
}
ZwClose(SectionHandle);
// 返回最终合成的物理地址
return CurrentPhysicalAddress ? (CurrentPhysicalAddress + PageOffset) : 0;
}这种方法更加底层。它非常考验程序员对资源生命周期的管理:ZwOpen 必须对应 ZwClose,ZwMap 必须对应 ZwUnmap。错一步,立刻蓝屏。
模块 4:驱动入口 (DriverEntry) —— 终极测试
我们将两种方案结合在 DriverEntry 中,以读取 calc.exe (计算器) 的基地址前 8 字节(通常是 PE 文件的 MZ 签名)为例进行测试。
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DriverObject->DriverUnload = DriverUnload;
DbgPrint("====================================\n");
DbgPrint("驱动物理内存读写综合测试开始...\n");
// 1. 寻找目标进程 (例如计算器)
char* TargetProcessName = "calc.exe";
int TargetProcessId = GetProcessIdByImageFileName(TargetProcessName);
if (TargetProcessId == 0) {
DbgPrint("错误:未找到 %s,请检查进程是否开启。\n", TargetProcessName);
return STATUS_SUCCESS;
}
// 2. 设定测试虚拟地址 (请根据实际情况修改此地址)
ULONG64 TestVirtualAddress = 0x7ff600000000;
DbgPrint("目标进程: %s (PID: %d), 目标虚拟地址: 0x%llX\n", TargetProcessName, TargetProcessId, TestVirtualAddress);
// --------------------------------------------------------------------------
// 测试方案一:使用 MmMapIoSpace 方式
// --------------------------------------------------------------------------
DbgPrint(">>> [方案一] 开始测试 MmMapIoSpace 转换逻辑...\n");
ULONG64 PhysicalAddressByMmMap = GetPhysicalAddressByMmMap(TargetProcessId, TestVirtualAddress);
if (PhysicalAddressByMmMap != 0) {
DbgPrint("MmMap 转换成功!物理地址: 0x%llX\n", PhysicalAddressByMmMap);
// 尝试读取验证
PHYSICAL_ADDRESS TargetPhys;
TargetPhys.QuadPart = PhysicalAddressByMmMap;
PULONG64 MappedData = (PULONG64)MmMapIoSpace(TargetPhys, 8, MmNonCached);
if (MappedData) {
DbgPrint("MmMap 验证读取数据: 0x%llX\n", *MappedData);
MmUnmapIoSpace(MappedData, 8);
}
}
else {
DbgPrint("MmMap 转换失败。\n");
}
DbgPrint("------------------------------------\n");
// --------------------------------------------------------------------------
// 测试方案二:使用 ZwMapViewOfSection 方式
// --------------------------------------------------------------------------
DbgPrint(">>> [方案二] 开始测试 ZwMapViewOfSection 转换逻辑...\n");
ULONG64 PhysicalAddressBySection = GetPhysicalAddressBySection(TargetProcessId, TestVirtualAddress);
if (PhysicalAddressBySection != 0) {
DbgPrint("Section 转换成功!物理地址: 0x%llX\n", PhysicalAddressBySection);
// 尝试使用 Section 方式读取验证
HANDLE SectionHandle = NULL;
UNICODE_STRING PhysicalMemoryDeviceName;
OBJECT_ATTRIBUTES ObjectAttributes;
RtlInitUnicodeString(&PhysicalMemoryDeviceName, L"\\Device\\PhysicalMemory");
InitializeObjectAttributes(&ObjectAttributes, &PhysicalMemoryDeviceName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
if (NT_SUCCESS(ZwOpenSection(&SectionHandle, SECTION_MAP_READ, &ObjectAttributes))) {
PVOID FinalMappedBaseAddress = NULL;
SIZE_T ViewSize = PAGE_SIZE;
LARGE_INTEGER SectionOffset;
SectionOffset.QuadPart = PhysicalAddressBySection & ~0xFFF; // 页面对齐
if (NT_SUCCESS(ZwMapViewOfSection(SectionHandle, NtCurrentProcess(), &FinalMappedBaseAddress, 0, PAGE_SIZE, &SectionOffset, &ViewSize, ViewUnmap, 0, PAGE_READONLY))) {
ULONG64 ExactOffsetInPage = PhysicalAddressBySection & 0xFFF;
PULONG64 FinalDataPointer = (PULONG64)((PUCHAR)FinalMappedBaseAddress + ExactOffsetInPage);
DbgPrint("Section 验证读取数据: 0x%llX\n", *FinalDataPointer);
ZwUnmapViewOfSection(NtCurrentProcess(), FinalMappedBaseAddress);
}
ZwClose(SectionHandle);
}
}
else {
DbgPrint("Section 转换失败。\n");
}
DbgPrint("====================================\n");
return STATUS_SUCCESS;
}完整版最终代码
#include<ntifs.h>
HANDLE g_handle;
ULONG64 GetPhysicalAddressByMmMap(int ProcessId, ULONG64 VirtualAddress) {
PEPROCESS ProcessObject = NULL;
// 1. 根据进程ID获取进程对象
NTSTATUS Status = PsLookupProcessByProcessId((HANDLE)ProcessId, &ProcessObject);
if (!NT_SUCCESS(Status)) {
return 0;
}
// 2. 获取该进程的页目录基地址 (Directory Table Base),也就是 CR3 寄存器的值
// 偏移量 0x28 对应的是 64 位系统下 EPROCESS 结构中的 DirectoryTableBase
ULONG64 DirectoryTableBase = *(PULONG64)((PUCHAR)ProcessObject + 0x28);
// 使用完 ProcessObject 后,减少引用计数
ObDereferenceObject(ProcessObject);
// 3. 拆解虚拟地址。将 64 位虚拟地址拆解为 4 级索引和 1 个页内偏移
// 每级索引占据虚拟地址中的 9 位 (掩码为 0x1FF)
unsigned short Pml4Index = (VirtualAddress >> 39) & 0x1FF; // 第1级:Page Map Level 4
unsigned short DirectoryPointerIndex = (VirtualAddress >> 30) & 0x1FF; // 第2级:Page Directory Pointer
unsigned short DirectoryIndex = (VirtualAddress >> 21) & 0x1FF; // 第3级:Page Directory
unsigned short TableIndex = (VirtualAddress >> 12) & 0x1FF; // 第4级:Page Table
ULONG64 PageOffset = VirtualAddress & 0xFFF; // 最后 12 位:页内偏移量
PHYSICAL_ADDRESS TargetPhysicalAddress;
// --- 第一步:映射并读取 PML4 表 (第1级) ---
TargetPhysicalAddress.QuadPart = DirectoryTableBase;
PULONG64 Pml4Table = (PULONG64)MmMapIoSpace(TargetPhysicalAddress, PAGE_SIZE, MmNonCached);
// 提取指向下一级表(Directory Pointer Table)的物理基地址,并抹除属性位
ULONG64 DirectoryPointerTablePhysicalAddress = Pml4Table[Pml4Index] & 0x0000FFFFFFFFF000;
MmUnmapIoSpace(Pml4Table, PAGE_SIZE); // 映射完必须立即释放
if (DirectoryPointerTablePhysicalAddress == 0) return 0;
// --- 第二步:映射并读取 Page Directory Pointer 表 (第2级) ---
TargetPhysicalAddress.QuadPart = DirectoryPointerTablePhysicalAddress;
PULONG64 DirectoryPointerTable = (PULONG64)MmMapIoSpace(TargetPhysicalAddress, PAGE_SIZE, MmNonCached);
// 提取指向下一级表(Page Directory Table)的物理基地址
ULONG64 DirectoryTablePhysicalAddress = DirectoryPointerTable[DirectoryPointerIndex] & 0x0000FFFFFFFFF000;
MmUnmapIoSpace(DirectoryPointerTable, PAGE_SIZE);
if (DirectoryTablePhysicalAddress == 0) return 0;
// --- 第三步:映射并读取 Page Directory 表 (第3级) ---
TargetPhysicalAddress.QuadPart = DirectoryTablePhysicalAddress;
PULONG64 DirectoryTable = (PULONG64)MmMapIoSpace(TargetPhysicalAddress, PAGE_SIZE, MmNonCached);
// 提取指向下一级表(Page Table)的物理基地址
ULONG64 PageTablePhysicalAddress = DirectoryTable[DirectoryIndex] & 0x0000FFFFFFFFF000;
MmUnmapIoSpace(DirectoryTable, PAGE_SIZE);
if (PageTablePhysicalAddress == 0) return 0;
// --- 第四步:映射并读取 Page Table 表 (第4级) ---
TargetPhysicalAddress.QuadPart = PageTablePhysicalAddress;
PULONG64 PageTable = (PULONG64)MmMapIoSpace(TargetPhysicalAddress, PAGE_SIZE, MmNonCached);
// 提取最终的物理页基地址
ULONG64 FinalPagePhysicalAddress = PageTable[TableIndex] & 0x0000FFFFFFFFF000;
MmUnmapIoSpace(PageTable, PAGE_SIZE);
if (FinalPagePhysicalAddress == 0) return 0;
// 最终物理地址 = 物理页基地址 + 虚拟地址中的页内偏移
return FinalPagePhysicalAddress + PageOffset;
}
ULONG64 GetPhysicalAddressBySection(int ProcessId, ULONG64 VirtualAddress) {
PEPROCESS ProcessObject = NULL;
PsLookupProcessByProcessId((HANDLE)ProcessId, &ProcessObject);
ULONG64 DirectoryTableBase = *(PULONG64)((PUCHAR)ProcessObject + 0x28);
ObDereferenceObject(ProcessObject);
// 1. 初始化物理内存对象路径
HANDLE SectionHandle = NULL;
UNICODE_STRING PhysicalMemoryDeviceName;
OBJECT_ATTRIBUTES ObjectAttributes;
RtlInitUnicodeString(&PhysicalMemoryDeviceName, L"\\Device\\PhysicalMemory");
// 注意:OBJ_KERNEL_HANDLE 保证了句柄在内核中是安全的
InitializeObjectAttributes(&ObjectAttributes, &PhysicalMemoryDeviceName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
// 2. 打开物理内存段对象的只读权限
NTSTATUS Status = ZwOpenSection(&SectionHandle, SECTION_MAP_READ, &ObjectAttributes);
if (!NT_SUCCESS(Status)) return 0;
// 3. 准备索引数组(展开所有缩写)
ULONG64 Indices[4];
Indices[0] = (VirtualAddress >> 39) & 0x1FF; // Page Map Level 4 Index
Indices[1] = (VirtualAddress >> 30) & 0x1FF; // Page Directory Pointer Index
Indices[2] = (VirtualAddress >> 21) & 0x1FF; // Page Directory Index
Indices[3] = (VirtualAddress >> 12) & 0x1FF; // Page Table Index
ULONG64 PageOffset = VirtualAddress & 0xFFF;
ULONG64 CurrentPhysicalAddress = DirectoryTableBase;
PVOID MappedBaseAddress = NULL;
SIZE_T ViewSize = PAGE_SIZE;
LARGE_INTEGER SectionOffset;
// 4. 开始逐级映射并解析
for (int i = 0; i < 4; i++) {
MappedBaseAddress = NULL;
ViewSize = PAGE_SIZE;
SectionOffset.QuadPart = CurrentPhysicalAddress;
// 将物理内存中的某一页映射到当前内核进程的虚拟地址空间
Status = ZwMapViewOfSection(
SectionHandle, // 物理内存对象句柄
NtCurrentProcess(), // 映射到当前进程
&MappedBaseAddress, // 映射后的虚拟地址存放在这里
0,
PAGE_SIZE, // 映射大小为一页
&SectionOffset, // 物理内存中的偏移
&ViewSize,
ViewUnmap,
0,
PAGE_READONLY // 只读映射
);
if (!NT_SUCCESS(Status)) {
ZwClose(SectionHandle);
return 0;
}
// 将映射出来的地址看作一个 64 位数组
PULONG64 TableArray = (PULONG64)MappedBaseAddress;
// 从当前表中取出下一级表的物理基地址,并抹除属性位 (掩码 0xFFFFFFFFF000)
CurrentPhysicalAddress = TableArray[Indices[i]] & 0x0000FFFFFFFFF000;
// 重要:读取完必须立即取消映射,否则会导致内核内存溢出
ZwUnmapViewOfSection(NtCurrentProcess(), MappedBaseAddress);
if (CurrentPhysicalAddress == 0) break;
}
ZwClose(SectionHandle);
// 返回最终合成的物理地址
return CurrentPhysicalAddress ? (CurrentPhysicalAddress + PageOffset) : 0;
}
VOID DriverUnload(PDRIVER_OBJECT pDriver) {
}
// 声明我们要调用的函数
ULONG64 GetPhysicalAddressByMmMap(int ProcessId, ULONG64 VirtualAddress);
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DriverObject->DriverUnload = DriverUnload;
DbgPrint("====================================\n");
DbgPrint("驱动物理内存读写综合测试开始...\n");
// 1. 寻找目标进程 (例如计算器)
char* TargetProcessName = "calc.exe";
int TargetProcessId = GetProcessIdByImageFileName(TargetProcessName);
if (TargetProcessId == 0) {
DbgPrint("错误:未找到 %s,请检查进程是否开启。\n", TargetProcessName);
return STATUS_SUCCESS;
}
// 2. 设定测试虚拟地址 (请根据实际情况修改此地址)
ULONG64 TestVirtualAddress = 0x7ff600000000;
DbgPrint("目标进程: %s (PID: %d), 目标虚拟地址: 0x%llX\n", TargetProcessName, TargetProcessId, TestVirtualAddress);
// --------------------------------------------------------------------------
// 测试方案一:使用 MmMapIoSpace 方式
// --------------------------------------------------------------------------
DbgPrint(">>> [方案一] 开始测试 MmMapIoSpace 转换逻辑...\n");
ULONG64 PhysicalAddressByMmMap = GetPhysicalAddressByMmMap(TargetProcessId, TestVirtualAddress);
if (PhysicalAddressByMmMap != 0) {
DbgPrint("MmMap 转换成功!物理地址: 0x%llX\n", PhysicalAddressByMmMap);
// 尝试读取验证
PHYSICAL_ADDRESS TargetPhys;
TargetPhys.QuadPart = PhysicalAddressByMmMap;
PULONG64 MappedData = (PULONG64)MmMapIoSpace(TargetPhys, 8, MmNonCached);
if (MappedData) {
DbgPrint("MmMap 验证读取数据: 0x%llX\n", *MappedData);
MmUnmapIoSpace(MappedData, 8);
}
}
else {
DbgPrint("MmMap 转换失败。\n");
}
DbgPrint("------------------------------------\n");
// --------------------------------------------------------------------------
// 测试方案二:使用 ZwMapViewOfSection 方式
// --------------------------------------------------------------------------
DbgPrint(">>> [方案二] 开始测试 ZwMapViewOfSection 转换逻辑...\n");
ULONG64 PhysicalAddressBySection = GetPhysicalAddressBySection(TargetProcessId, TestVirtualAddress);
if (PhysicalAddressBySection != 0) {
DbgPrint("Section 转换成功!物理地址: 0x%llX\n", PhysicalAddressBySection);
// 尝试使用 Section 方式读取验证
HANDLE SectionHandle = NULL;
UNICODE_STRING PhysicalMemoryDeviceName;
OBJECT_ATTRIBUTES ObjectAttributes;
RtlInitUnicodeString(&PhysicalMemoryDeviceName, L"\\Device\\PhysicalMemory");
InitializeObjectAttributes(&ObjectAttributes, &PhysicalMemoryDeviceName, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
if (NT_SUCCESS(ZwOpenSection(&SectionHandle, SECTION_MAP_READ, &ObjectAttributes))) {
PVOID FinalMappedBaseAddress = NULL;
SIZE_T ViewSize = PAGE_SIZE;
LARGE_INTEGER SectionOffset;
SectionOffset.QuadPart = PhysicalAddressBySection & ~0xFFF; // 页面对齐
if (NT_SUCCESS(ZwMapViewOfSection(SectionHandle, NtCurrentProcess(), &FinalMappedBaseAddress, 0, PAGE_SIZE, &SectionOffset, &ViewSize, ViewUnmap, 0, PAGE_READONLY))) {
ULONG64 ExactOffsetInPage = PhysicalAddressBySection & 0xFFF;
PULONG64 FinalDataPointer = (PULONG64)((PUCHAR)FinalMappedBaseAddress + ExactOffsetInPage);
DbgPrint("Section 验证读取数据: 0x%llX\n", *FinalDataPointer);
ZwUnmapViewOfSection(NtCurrentProcess(), FinalMappedBaseAddress);
}
ZwClose(SectionHandle);
}
}
else {
DbgPrint("Section 转换失败。\n");
}
DbgPrint("====================================\n");
return STATUS_SUCCESS;
}
// 辅助函数:通过进程名获取 PID (引用自之前的简单版本)
int GetProcessIdByImageFileName(char* TargetImageName) {
PEPROCESS ProcessObject = PsGetCurrentProcess();
PLIST_ENTRY ListStart = (PLIST_ENTRY)((PUCHAR)ProcessObject + 0x188); // 基于你之前的偏移 0x188
PLIST_ENTRY ListCurrent = ListStart;
do {
PEPROCESS Entry = (PEPROCESS)((PUCHAR)ListCurrent - 0x188);
// 获取进程名并比较
if (_stricmp((char*)((PUCHAR)Entry + 0x2e0), TargetImageName) == 0) { // 基于你之前的偏移 0x2e0
return (int)*(PULONG64)((PUCHAR)Entry + 0x180); // 基于你之前的偏移 0x180
}
ListCurrent = ListCurrent->Flink;
} while (ListCurrent != ListStart);
return 0;
}三、 总结与排坑指南 (Author's Notes)
哪种方案更好?在实际的驱动开发中,强烈建议使用方案一(MmMapIoSpace)。它的 API 语义明确,代码量少,且不容易因为句柄泄露或页面对齐问题导致蓝屏。方案二(Section)更多用于理解 Windows 底层抽象,或是应对某些极端 Hook 了
MmMapIoSpace的环境。偏移量陷阱(硬编码警告)代码中的
0x28(CR3),0x188(ActiveProcessLinks),0x2e0(ImageFileName) 都是与具体 Windows 版本绑定的。不要直接在生产环境使用这些硬编码! 正规的做法是结合特征码搜索(PDB符号解析)或根据不同系统版本进行动态适配。随着 Windows 10/11 的更新,如果系统开启了 VBS (基于虚拟化的安全性),直接读取物理内存可能会因为影子页表(EPT)的存在而失效或触发异常。
参考视频:【【Windows内核】不附加进程,读写物理地址】 a85K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2T1K9h3I4A6j5X3W2D9K9g2)9J5k6h3y4G2L8g2)9J5c8Y4k6A6k6r3g2G2i4K6u0r3b7W2j5I4M7h3W2B7f1%4A6X3c8g2y4d9i4K6u0r3i4K6y4r3M7$3S2S2M7X3g2Q4y4h3k6K6L8%4g2J5j5$3g2Q4x3@1c8U0L8%4m8&6i4K6g2X3N6$3g2T1