跨进程内存读写原理学习

参考

[原创]通过修改物理内存实现跨进程内存读写-编程技术-看雪安全社区|专业技术交流与安全研究论坛

主要内容

1.根据进程名找PEPROCESS

PEPROCESS是EPROCESS结构体的指针(Pointer)

找偏移可以用的网站https://www.vergiliusproject.com/

起点是当前程序的PEPROCESS, 用的PsGetCurrentProcess();函数获取。

通过AciveProcessLinks遍历所有链在一起的EPROCESS, 判断ImageFileName(15字节截断)

有了PEPROCESS以后可以直接调用MmCopyVirtualMemory, 但是调用函数太逊了, 而且MmCopyVirtualMemory有可能被AC监控。

2.根据虚拟地址以及DirBase转换成物理地址

DirBase就是程序在执行时cr3中的值。也是第四级页表的起始地址(物理地址)

将虚拟地址的低48位进行9-9-9-9-12划分, 得到下面五个值

    UINT64 pml4_idx = (addr >> 39) & 0x1FF;
    UINT64 pdpt_idx = (addr >> 30) & 0x1FF;
    UINT64 pd_idx = (addr >> 21) & 0x1FF;
    UINT64 pt_idx = (addr >> 12) & 0x1FF;
    UINT64 offset = addr & 0xFFF;

还有一些细节

1.要用掩码去掉一些标记用的位

2.判断页面是否有效

3.是否是大页

具体看UINT64 VirtToPhys(UINT64 cr3, UINT64 addr);函数的实现。

这里0x000FFFFFFFFFF000掩码和文章里的12-35bit不太一样,只要标记位不超过高12位0x000FFFFFFFFFF000应该没问题。

3.怎么读写物理地址

这里用的MmCopyMemory, 调用函数还是太逊了,直接修改页表更叼一点

系统版本:WIN11 25H2

#include <ntifs.h>

VOID ReadPhysical(UINT64 phys_addr, SIZE_T size, VOID* out)
{
    if (out == NULL || size == 0) return;

    PHYSICAL_ADDRESS sourceAddress;
    sourceAddress.QuadPart = phys_addr;

    SIZE_T bytesCopied = 0;

    MM_COPY_ADDRESS copyAddress;
    copyAddress.PhysicalAddress = sourceAddress;

    NTSTATUS status = MmCopyMemory(
        out,
        copyAddress,
        size,
        MM_COPY_MEMORY_PHYSICAL,
        &bytesCopied
    );
}

BOOLEAN ReadPhysUint64(UINT64 physAddr, UINT64* outValue) {
    MM_COPY_ADDRESS copyAddr;
    copyAddr.PhysicalAddress.QuadPart = physAddr;
    SIZE_T bytesCopied = 0;

    NTSTATUS status = MmCopyMemory(outValue, copyAddr, sizeof(UINT64), MM_COPY_MEMORY_PHYSICAL, &bytesCopied);
    return (NT_SUCCESS(status) && bytesCopied == sizeof(UINT64));
}

UINT64 VirtToPhys(UINT64 cr3, UINT64 addr)
{
    UINT64 pml4_idx = (addr >> 39) & 0x1FF;
    UINT64 pdpt_idx = (addr >> 30) & 0x1FF;
    UINT64 pd_idx = (addr >> 21) & 0x1FF;
    UINT64 pt_idx = (addr >> 12) & 0x1FF;
    UINT64 offset = addr & 0xFFF;

    UINT64 entry = 0;
    UINT64 current_base = cr3 & 0x000FFFFFFFFFF000;

    if (!ReadPhysUint64(current_base + pml4_idx * 8, &entry) || !(entry & 1)) return 0;
    current_base = entry & 0x000FFFFFFFFFF000;

    if (!ReadPhysUint64(current_base + pdpt_idx * 8, &entry) || !(entry & 1)) return 0;
    if (entry & 0x80) return (entry & 0x000FFFFFFFFFF000 & ~0x3FFFFFFF) + (addr & 0x3FFFFFFF);
    current_base = entry & 0x000FFFFFFFFFF000;

    if (!ReadPhysUint64(current_base + pd_idx * 8, &entry) || !(entry & 1)) return 0;
    if (entry & 0x80) return (entry & 0x000FFFFFFFFFF000 & ~0x1FFFFF) + (addr & 0x1FFFFF);
    current_base = entry & 0x000FFFFFFFFFF000;

    if (!ReadPhysUint64(current_base + pt_idx * 8, &entry) || !(entry & 1)) return 0;

    return (entry & 0x000FFFFFFFFFF000) + offset;
}

VOID ReadVirtual(UINT64 cr3, UINT64 addr, SIZE_T size, VOID* out)
{
    if (out == NULL || size == 0 || cr3 == 0) return;

    SIZE_T totalRead = 0;
    PUCHAR pOut = (PUCHAR)out;

    while (totalRead < size)
    {
        UINT64 currentAddr = addr + totalRead;
        UINT64 offset = currentAddr & 0xFFF;

        // 计算当前页面还能读多少字节(不能超过 size,也不能跨页)
        SIZE_T bytesInThisPage = 0x1000 - offset;
        SIZE_T bytesToRead = (size - totalRead) < bytesInThisPage ? (size - totalRead) : bytesInThisPage;

        // 1. 解析当前虚拟地址对应的物理地址 (调用你之前的 4 级页表解析逻辑)
        UINT64 physAddr = VirtToPhys(cr3, currentAddr);

        if (physAddr == 0) {
            return;
        }
        else {
            // 2. 只读取当前页面内的部分
            ReadPhysical(physAddr, bytesToRead, pOut + totalRead);
        }

        totalRead += bytesToRead;
    }
}

UINT64 GetDirectoryTableBase(PEPROCESS process)
{
    if (process == NULL) return 0;
    return *(UINT64*)((CHAR*)process + 0x28); // process.Pcb.DirectoryTableBase
}


#define OFFSET_AciveProcessLinks 0x1d8
#define OFFSET_ImageFileName 0x338
#define OFFSET_SectionBaseAddress 0x2b0

PEPROCESS GetProcessByName(const char* name)
{
    if (name == NULL) return NULL;

    // 1. 获取当前进程作为起点
    PEPROCESS startProcess = PsGetCurrentProcess();

    // 2. 获取链表头 (指向的是 LIST_ENTRY 成员)
    PLIST_ENTRY startList = (PLIST_ENTRY)((PUCHAR)startProcess + OFFSET_AciveProcessLinks);
    PLIST_ENTRY currList = startList->Flink;


    // 3. 开始遍历
    while (currList != startList)
    {
        // 4. 通过 LIST_ENTRY 地址计算出 EPROCESS 头部地址
        PEPROCESS pEProcess = (PEPROCESS)((PUCHAR)currList - OFFSET_AciveProcessLinks);

        // 5. 获取进程名 (EPROCESS->ImageFileName 是 15 字节截断名)
        char* imageName = (char*)((PUCHAR)pEProcess + OFFSET_ImageFileName);

        // 6. 比较名称 (使用 strnicmp 忽略大小写更方便)
        if (_strnicmp(imageName, name, 15) == 0)
        {
            return pEProcess;
        }

        // 7. 移动到下一个链表节点
        currList = currList->Flink;
    }

    return NULL;
}

NTSTATUS DriverUnload(PDRIVER_OBJECT driver)
{
    return STATUS_SUCCESS;
}


NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING path)
{
    __debugbreak();
    driver->DriverUnload = DriverUnload;
	PEPROCESS process = GetProcessByName("explorer.exe");
	if (!process) return STATUS_SUCCESS;


    UINT64 dirbase = GetDirectoryTableBase(process);
	char buf[8];

    UINT64 imageBase = *(UINT64*)((CHAR*)process + OFFSET_SectionBaseAddress);

    ReadVirtual(dirbase, imageBase, 8, buf);

	DbgPrint("%llx\n", *(UINT64*)buf);

	return STATUS_SUCCESS;	
}