首页
社区
课程
招聘
[原创]Windows不附加进程从而读写物理地址
发表于: 2小时前 79

[原创]Windows不附加进程从而读写物理地址

2小时前
79

最近回顾学习,复习到了这个知识点。

在 Windows 内核安全与反作弊对抗(驱动开发)领域,内存读写是最基础也是最核心的技术。传统的读写方式通常依赖于 KeStackAttachProcess 将当前线程附加到目标进程的上下文中,然后直接使用 memcpyRtlCopyMemory 进行数据拷贝。

然而,在高级的安全对抗中,“附加进程”这一操作会留下明显的痕迹(如修改跨进程上下文、触发某些回调等),极易被安全软件或反作弊系统(EAC/BE等)拦截或记录。

那么,有没有一种方法可以完全不改变进程上下文,静悄悄地读取目标进程的内存呢?答案就是:手动解析多级页表,直接读取物理内存(Physical Memory)

本文将带你从零开始,详细解析并实现两套完整的“无附加物理内存读写”方案。

一、核心思路:从虚拟地址转换为物理地址

在 64 位 Windows 系统中,每个进程都有自己独立的虚拟地址空间。我们平时在 Cheat Engine 或代码中看到的地址(如 0x7ff600000000),都是虚拟地址(Virtual Address, VA)

虚拟地址本身并不存储数据,它必须通过 CPU 的页表机制翻译成内存条上的物理地址(Physical Address, PA)。这个翻译过程由 CPU 的 CR3 寄存器(保存了当前进程页表的物理根地址)主导,经过四级页表层层深入:

  1. PML4 (Page Map Level 4) - 第 1 级

  2. PDPT (Page Directory Pointer Table) - 第 2 级

  3. PDE (Page Directory Entry) - 第 3 级

  4. 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 必须对应 ZwCloseZwMap 必须对应 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)

  1. 哪种方案更好?在实际的驱动开发中,强烈建议使用方案一(MmMapIoSpace)。它的 API 语义明确,代码量少,且不容易因为句柄泄露或页面对齐问题导致蓝屏。方案二(Section)更多用于理解 Windows 底层抽象,或是应对某些极端 Hook 了 MmMapIoSpace 的环境。

  2. 偏移量陷阱(硬编码警告)代码中的 0x28 (CR3), 0x188 (ActiveProcessLinks), 0x2e0 (ImageFileName) 都是与具体 Windows 版本绑定的。不要直接在生产环境使用这些硬编码! 正规的做法是结合特征码搜索(PDB符号解析)或根据不同系统版本进行动态适配。

  3. 随着 Windows 10/11 的更新,如果系统开启了 VBS (基于虚拟化的安全性),直接读取物理内存可能会因为影子页表(EPT)的存在而失效或触发异常。



参考视频:【【Windows内核】不附加进程,读写物理地址】 a85K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2T1K9h3I4A6j5X3W2D9K9g2)9J5k6h3y4G2L8g2)9J5c8Y4k6A6k6r3g2G2i4K6u0r3b7W2j5I4M7h3W2B7f1%4A6X3c8g2y4d9i4K6u0r3i4K6y4r3M7$3S2S2M7X3g2Q4y4h3k6K6L8%4g2J5j5$3g2Q4x3@1c8U0L8%4m8&6i4K6g2X3N6$3g2T1



传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回