通过修改物理内存实现跨进程内存读写

mi1itray.axe published on
17 min, 3347 words

Categories: Driver

跨进程内存读写有多种方式,这里主要了解一下修改物理内存来实现跨进程内存修改方法。这种方法主要是无痕,对比修改cr3来说要无痕的多。

后面再试试修改cr3和修改MDL映射来做到跨进程内存读写方式。

系统:win10 21h1 x64

编译环境: vs2022 详情见附录

基础

虚拟地址转物理地址

虚拟地址也称线性地址,一个线性地址+进程的DirBase地址可以转换成物理地址。先来看线性地址的含义

在x64体系中只实现了48位的virtual address,高16位被用作符号扩展,这高16位要么全是0,要么全是1。 不同于x86体系结构,每级页表寻址长度变成9位,由于在x64体系结构中,普通页大小仍为4KB,然而数据却表示64位长,因此一个4KB页在x64体系结构下只能包含512项内容,所以为了保证页对齐和以页为单位的页表内容换入换出,在x64下每级页表寻址部分长度定位9位。

img

从Page Map Level 4(PML4)开始到最后的物理地址,每一个都可以理解成一层页表的索引,索引值就是线性地址上不同的部分,分别缩写是PML4, PDPE, PDE,PTE。

注意,并不是取出来的值就直接指向一下一个页表,个人PC上一般是取值的12-35bit的值,其他置0。具体的后面见代码,或参考看雪的文章

img

使用windbg可以先查看进程对应的DirBase地址,然后再使用!vtop Dirbase地址 虚拟地址查看虚拟地址对应的物理地址,如下。

3: kd> !process 258c 0
Searching for Process with Cid == 258c
PROCESS ffffc40d2ab48340
    SessionId: 1  Cid: 258c    Peb: a6e35cd000  ParentCid: 1250
    DirBase: 235ae6000  ObjectTable: ffff998138d4ee00  HandleCount:  38.
    Image: test.exe

3: kd> !vtop 235ae6000 0000A6E334FB00
Amd64VtoP: Virt 000000a6e334fb00, pagedir 0000000235ae6000
Amd64VtoP: PML4E 0000000235ae6008
Amd64VtoP: PDPE 00000001087fb4d8
Amd64VtoP: PDE 000000010f7fc8c8
Amd64VtoP: PTE 00000000ad207a78
Amd64VtoP: Mapped phys 000000011b10cb00
Virtual address a6e334fb00 translates to physical address 11b10cb00.

上面得到DirBase的值是235ae6000,然后需要查看物理地址的虚拟地址是0x0000A6E334FB00,就使用命令

!vtop 235ae6000 0000A6E334FB00

得到最后对应的物理地址是0x11b10cb00。

简单例子代码如下:

#include <stdio.h>
#include <stdlib.h>

int main() {
    char flag[] = {"flag{b7285d748dd042a4929d3dbec778e637}"};

    printf("value addr: %p", flag);
    getchar();

    return 0;
}

运行后可以打印出来字符串的虚拟地址0000A6E334FB00,然后通过上述步骤得到物理地址。

我们尝试看看物理内存中的字符串,现在已经确定物理内存的地址是0xD0000147,使用!db 0xD0000147来查看物理内存,记住要!,没有感叹号的是查看虚拟内存的

3: kd> !db 0x11b10cb00
#11b10cb00 66 6c 61 67 7b 62 37 32-38 35 64 37 34 38 64 64 flag{b7285d748dd
#11b10cb10 30 34 32 61 34 39 32 39-64 33 64 62 65 63 37 37 042a4929d3dbec77
#11b10cb20 38 65 36 33 37 7d 00 00-f8 82 20 82 f7 7f 00 00 8e637}.... .....
#11b10cb30 00 00 00 00 00 00 00 00-20 13 1f 82 f7 7f 00 00 ........ .......
#11b10cb40 00 00 00 00 00 00 00 00-99 13 1f 82 f7 7f 00 00 ................
#11b10cb50 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
#11b10cb60 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
#11b10cb70 00 00 00 00 00 00 00 00-44 73 d3 08 fe 7f 00 00 ........Ds......

可以看到物理内存上的字符串内容。

DirBase地址获取

DirBase地址除了通过上述windbg直接得到这个值以外,还可以通过EPROCESS来得到,这个是代码比较需要的

3: kd> dt _eprocess ffffc40d2ab48340
nt!_EPROCESS
   +0x000 Pcb              : _KPROCESS
   +0x438 ProcessLock      : _EX_PUSH_LOCK
   +0x440 UniqueProcessId  : 0x00000000`0000258c Void
   +0x448 ActiveProcessLinks : _LIST_ENTRY [ 0xffffc40d`2cb43788 - 0xffffc40d`2cd444c8 ]
   +0x458 RundownProtect   : _EX_RUNDOWN_REF
   .....
3: kd> dx -id 0,0,ffffc40d23c95040 -r1 (*((ntkrnlmp!_KPROCESS *)0xffffc40d2ab48340))
(*((ntkrnlmp!_KPROCESS *)0xffffc40d2ab48340))                 [Type: _KPROCESS]
    [+0x000] Header           [Type: _DISPATCHER_HEADER]
    [+0x018] ProfileListHead  [Type: _LIST_ENTRY]
    [+0x028] DirectoryTableBase : 0x235ae6000 [Type: unsigned __int64]

DirectoryTableBase的值就是DirBase地址了,实际上就是EPROCESS + 0x28的偏移

还可以通过获取CR3寄存器的值,CR3寄存器中的值就是页目录表的物理地址,也就是DirBase

思路

目的:进程B可以通过修改物理内存的内容来修改进程A内存中的数据

实验设置:进程A泄露一个变量地址,然后等待进程B修改,修改后再回复执行,打印变量值看是否修改成功

内核部分思路:

  • 将R3的虚拟地址转换为物理地址
  • 使用MmCopyMemory复制物理地址内容
  • 修改内容
  • 使用mmMapIoSpaceEx将修改后的内容映射回物理地址

代码实现

被修改进程代码

这里写一个例子来充当被攻击(修改内存)的进程。主要就是打印变量内容和地址,然后暂停程序等待一段时间(等待被驱动修改),然后再打印变量内容,看看是否被驱动修改内存成功。

#include <stdio.h>
#include <stdlib.h>

int main() {
    char flag[] = {"flag{b7285d748dd042a4929d3dbec778e637}"};

    printf("value addr: %p\r\n", flag);
    printf("flag data: %s\r\n", flag);
    getchar();

    printf("flag data Now: %s\r\n", flag);
    return 0;
}

驱动代码

这里就是主要逻辑,通过驱动代码取修改目标进程的内存内容,做到跨进程内存读取,修改。

定义一个读取物理内存函数

/// @brief 读取物理地址的内存内容
/// @param address 物理地址
/// @param buffer 复制内存地址到buffer
/// @param size 复制大小
/// @param BytesTransferred 读取的字节数
/// @return
NTSTATUS ReadPhysicalAddress(IN PVOID64 address, OUT PVOID64 buffer,
                             IN SIZE_T size, OUT SIZE_T* BytesTransferred)
{
    MM_COPY_ADDRESS Read          = {0};
    Read.PhysicalAddress.QuadPart = (LONG64)address;
    return MmCopyMemory(
        buffer, Read, size, MM_COPY_MEMORY_PHYSICAL, BytesTransferred);
}

再定义一个写入物理内存的函数

/// @brief 写入指定内容到物理内存中
/// @param address 被写入的物理地址
/// @param buffer 需要写入的缓冲区指针
/// @param size 需要写入的大小
/// @param BytesTransferred 写入成功后的大小
/// @return
NTSTATUS WritePhysicalAddress(IN PVOID64 address, IN PVOID64 buffer,
                              IN SIZE_T size, OUT SIZE_T* BytesTransferred)
{
    PVOID            map;
    PHYSICAL_ADDRESS Write = {0};

    if (!address) {
        kprintf("Address value error. \r\n");
        return STATUS_UNSUCCESSFUL;
    }

    Write.QuadPart = (LONG64)address;
    map            = MmMapIoSpaceEx(Write, size, PAGE_READWRITE);

    if (!map) {
        kprintf("Write Memory faild.\r\n");
        return STATUS_UNSUCCESSFUL;
    }
    RtlCopyMemory(map, buffer, size);
    *BytesTransferred = size;
    MmUnmapIoSpace(map, size);
    return STATUS_SUCCESS;
}

我们需要将虚拟地址转换成物理地址,那么首先需要线性地址+DirBase地址,DirBase地址获取是通过PEPROCESS+0x28偏移读取的

/// @brief 通过EPROCESS获取DirBase值
/// @param pid 进程PID
/// @param pDirbase 一个UINT64指针,获取成功后返回值
/// @return
NTSTATUS GetDirBaseByEprocess(IN UINT64 pid, OUT PUINT64 pDirbase)
{
    PEPROCESS pEprocess;
    NTSTATUS  status;

    status = PsLookupProcessByProcessId((HANDLE)pid, &pEprocess);
    if (!NT_SUCCESS(status)) {
        kprintf("[!] Get Pid=%d _EPROCESS failed!", pid);
        return STATUS_UNSUCCESSFUL;
    }

    *pDirbase =
        *(PUINT64)((PUCHAR)pEprocess + WIN10_21H1_EPROCESS2DIRBASE_OFFSET);
    kprintf("[+] uDirBase ==> %llx\r\n", *pDirbase);

    return STATUS_SUCCESS;
}

得到DirBase后,就可以虚拟地址转换物理地址。

传入虚拟地址后,取后48bit,然后将这48bit分成4个9bit和最后12bit,分别是PML4,PDPE,PDE,PTE和页内偏移offset。需要注意的是DirBase就已经是物理内存了,所以读取DirBase内容并且一层一层读取都要用自定义函数ReadPhysicalAddress

每一层都是基地址+8*偏移,读取的内容,取12-35bit就是下一层的基地址

/// @brief 传入DirBase值和虚拟地址后,回转化成一个物理地址返回
/// @param DirBase DirBase地址,传入一个UINT64值
/// @param addr 传入一个指向虚拟地址的指针,转化成物理地址后会修改这个指针的值
/// @return
NTSTATUS TranslateAddress(IN UINT64 DirBase, _Inout_ PUINT64 addr)
{
    UINT16   PML4, PDPE, PDE, PTE, offset;
    UINT64   mask = 0x7fffff000;
    UINT64   uTmp;
    SIZE_T   BytesTransferred;
    NTSTATUS status;

    offset = *addr & 0xfff;
    PTE    = (*addr >> 12) & 0x1ff;
    PDE    = (*addr >> (12 + 9)) & 0x1ff;
    PDPE   = (*addr >> (9 * 2 + 12)) & 0x1ff;
    PML4   = (*addr >> (9 * 3 + 12)) & 0x1ff;

    status = ReadPhysicalAddress(
        (PVOID64)(DirBase + PML4 * 8), &uTmp, sizeof(uTmp), &BytesTransferred);
    uTmp &= mask;
    kprintf("[+] PML4(%x) ==> %llx\r\n", PML4, uTmp);

    status = ReadPhysicalAddress(
        (PVOID64)(uTmp + PDPE * 8), &uTmp, sizeof(uTmp), &BytesTransferred);
    uTmp &= mask;
    kprintf("[+] PDPE(%x) ==> %llx\r\n", PDPE, uTmp);

    status = ReadPhysicalAddress(
        (PVOID64)(uTmp + PDE * 8), &uTmp, sizeof(uTmp), &BytesTransferred);
    uTmp &= mask;
    kprintf("[+] PDE(%x) ==> %llx\r\n", PDE, uTmp);

    status = ReadPhysicalAddress(
        (PVOID64)(uTmp + PTE * 8), &uTmp, sizeof(uTmp), &BytesTransferred);
    uTmp &= mask;
    kprintf("[+] PTE(%x) ==> %llx\r\n", PTE, uTmp);

    *addr = uTmp + offset;
    kprintf("[+] physical address: %llx\r\n", *addr);
    return STATUS_SUCCESS;
}

最后再主函数中定义一下逻辑。这里直接手动指定进程号和目标进程打印出来的变量地址,然后将虚拟地址转化成物理地址,读取物理地址上的内容并打印出来看看是否正确。再修改物理地址上的内容。

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING path)
{
    NTSTATUS status;
    UINT64   pid, uAddr, uDirBase;
    SIZE_T   BytesTransferred;
    UCHAR    charArry[40] = {0};
    UCHAR    example[40] = {"Yes I change memory by physical"};

    pid   = 10276;
    uAddr = 0x3629FAFB80;

    pDriver->DriverUnload = DriverUnload;
    // 手动指定进程号
    status = GetDirBaseByEprocess(pid, &uDirBase);
    if (!NT_SUCCESS(status)) {
        kprintf("[!] Get DirBase address failed!\r\n");
        return STATUS_UNSUCCESSFUL;
    }

    // 将虚拟地址转化成物理地址
    status = TranslateAddress(uDirBase, &uAddr);
    if (!NT_SUCCESS(status)) {
        kprintf("[!] Translate address failed!\r\n");
        return STATUS_UNSUCCESSFUL;
    }

    // 读取物理地址内容, 然后修改内容
    ReadPhysicalAddress((PVOID64)uAddr, charArry, 40, &BytesTransferred);
    kprintf("[+] data is %s\r\n", charArry);

    // 将example字符串写入物理内存
    WritePhysicalAddress((PVOID64)uAddr, example, 40, &BytesTransferred);
    kprintf("[+] Write end\r\n");

    return STATUS_SUCCESS;
}

结果

目标进程

image-20240505235113447

驱动

image-20240505235141011

可以看到目标进程的指定内存被修改,同时驱动也跨进程读取,修改内存成功

参考

使用CR3切换实现读取指定进程内存数据 | pnpon.com

c/c++/易语言驱动内存无痕读写源码 | csdn.net

将虚拟地址转换为物理地址 | learn.microsoft.com

X64下的虚拟地址到物理地址的转换 | bbs.kanxue.com

几种挖掘任意读写驱动的方法 | myzxcg.com/

附录

驱动全部代码

#include <ntifs.h>
#include <ntddk.h>
#include <intrin.h>
#include <ntdef.h>

#define WIN10_21H1_EPROCESS2DIRBASE_OFFSET 0x28

#define kprintf(...) \
    KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, __VA_ARGS__))


NTSTATUS GetDirBaseByEprocess(IN UINT64, OUT PUINT64);
NTSTATUS TranslateAddress(IN UINT64, _Inout_ PUINT64);
NTSTATUS ReadPhysicalAddress(IN PVOID64, OUT PVOID64, IN SIZE_T, OUT SIZE_T*);
NTSTATUS WritePhysicalAddress(IN PVOID64, IN PVOID64, IN SIZE_T, OUT SIZE_T*);
NTKERNELAPI PPEB NTAPI  PsGetProcessPeb(IN PEPROCESS Process);
NTKERNELAPI PVOID NTAPI PsGetProcessWow64Process(IN PEPROCESS Process);

VOID DriverUnload(PDRIVER_OBJECT pDriver)
{
    kprintf("驱动已卸载.\r\n");
}

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING path)
{
    NTSTATUS status;
    UINT64   pid, uAddr, uDirBase;
    SIZE_T   BytesTransferred;
    UCHAR    charArry[40] = {0};
    UCHAR    example[40] = {"Yes I change memory by physical"};

    pid   = 10276;
    uAddr = 0x3629FAFB80;

    pDriver->DriverUnload = DriverUnload;
    // 手动指定进程号
    status = GetDirBaseByEprocess(pid, &uDirBase);
    if (!NT_SUCCESS(status)) {
        kprintf("[!] Get DirBase address failed!\r\n");
        return STATUS_UNSUCCESSFUL;
    }

    // 将虚拟地址转化成物理地址
    status = TranslateAddress(uDirBase, &uAddr);
    if (!NT_SUCCESS(status)) {
        kprintf("[!] Translate address failed!\r\n");
        return STATUS_UNSUCCESSFUL;
    }

    // 读取物理地址内容, 然后修改内容
    ReadPhysicalAddress((PVOID64)uAddr, charArry, 40, &BytesTransferred);
    kprintf("[+] data is %s\r\n", charArry);

    // 将example字符串写入物理内存
    WritePhysicalAddress((PVOID64)uAddr, example, 40, &BytesTransferred);
    kprintf("[+] Write end\r\n");

    return STATUS_SUCCESS;
}

/// @brief 读取物理地址的内存内容
/// @param address 物理地址
/// @param buffer 复制内存地址到buffer
/// @param size 复制大小
/// @param BytesTransferred 读取的字节数
/// @return
NTSTATUS ReadPhysicalAddress(IN PVOID64 address, OUT PVOID64 buffer,
                             IN SIZE_T size, OUT SIZE_T* BytesTransferred)
{
    MM_COPY_ADDRESS Read          = {0};
    Read.PhysicalAddress.QuadPart = (LONG64)address;
    return MmCopyMemory(
        buffer, Read, size, MM_COPY_MEMORY_PHYSICAL, BytesTransferred);
}

/// @brief 写入指定内容到物理内存中
/// @param address 被写入的物理地址
/// @param buffer 需要写入的缓冲区指针
/// @param size 需要写入的大小
/// @param BytesTransferred 写入成功后的大小
/// @return
NTSTATUS WritePhysicalAddress(IN PVOID64 address, IN PVOID64 buffer,
                              IN SIZE_T size, OUT SIZE_T* BytesTransferred)
{
    PVOID            map;
    PHYSICAL_ADDRESS Write = {0};

    if (!address) {
        kprintf("Address value error. \r\n");
        return STATUS_UNSUCCESSFUL;
    }

    Write.QuadPart = (LONG64)address;
    map            = MmMapIoSpaceEx(Write, size, PAGE_READWRITE);

    if (!map) {
        kprintf("Write Memory faild.\r\n");
        return STATUS_UNSUCCESSFUL;
    }
    RtlCopyMemory(map, buffer, size);
    *BytesTransferred = size;
    MmUnmapIoSpace(map, size);
    return STATUS_SUCCESS;
}

/// @brief 通过EPROCESS获取DirBase值
/// @param pid 进程PID
/// @param pDirbase 一个UINT64指针,获取成功后返回值
/// @return
NTSTATUS GetDirBaseByEprocess(IN UINT64 pid, OUT PUINT64 pDirbase)
{
    PEPROCESS pEprocess;
    NTSTATUS  status;

    status = PsLookupProcessByProcessId((HANDLE)pid, &pEprocess);
    if (!NT_SUCCESS(status)) {
        kprintf("[!] Get Pid=%d _EPROCESS failed!", pid);
        return STATUS_UNSUCCESSFUL;
    }

    *pDirbase =
        *(PUINT64)((PUCHAR)pEprocess + WIN10_21H1_EPROCESS2DIRBASE_OFFSET);
    kprintf("[+] uDirBase ==> %llx\r\n", *pDirbase);

    return STATUS_SUCCESS;
}

/// @brief 传入DirBase值和虚拟地址后,回转化成一个物理地址返回
/// @param DirBase DirBase地址,传入一个UINT64值
/// @param addr 传入一个指向虚拟地址的指针,转化成物理地址后会修改这个指针的值
/// @return
NTSTATUS TranslateAddress(IN UINT64 DirBase, _Inout_ PUINT64 addr)
{
    UINT16   PML4, PDPE, PDE, PTE, offset;
    UINT64   mask = 0x7fffff000;
    UINT64   uTmp;
    SIZE_T   BytesTransferred;
    NTSTATUS status;

    offset = *addr & 0xfff;
    PTE    = (*addr >> 12) & 0x1ff;
    PDE    = (*addr >> (12 + 9)) & 0x1ff;
    PDPE   = (*addr >> (9 * 2 + 12)) & 0x1ff;
    PML4   = (*addr >> (9 * 3 + 12)) & 0x1ff;

    status = ReadPhysicalAddress(
        (PVOID64)(DirBase + PML4 * 8), &uTmp, sizeof(uTmp), &BytesTransferred);
    uTmp &= mask;
    kprintf("[+] PML4(%x) ==> %llx\r\n", PML4, uTmp);

    status = ReadPhysicalAddress(
        (PVOID64)(uTmp + PDPE * 8), &uTmp, sizeof(uTmp), &BytesTransferred);
    uTmp &= mask;
    kprintf("[+] PDPE(%x) ==> %llx\r\n", PDPE, uTmp);

    status = ReadPhysicalAddress(
        (PVOID64)(uTmp + PDE * 8), &uTmp, sizeof(uTmp), &BytesTransferred);
    uTmp &= mask;
    kprintf("[+] PDE(%x) ==> %llx\r\n", PDE, uTmp);

    status = ReadPhysicalAddress(
        (PVOID64)(uTmp + PTE * 8), &uTmp, sizeof(uTmp), &BytesTransferred);
    uTmp &= mask;
    kprintf("[+] PTE(%x) ==> %llx\r\n", PTE, uTmp);

    *addr = uTmp + offset;
    kprintf("[+] physical address: %llx\r\n", *addr);
    return STATUS_SUCCESS;
}

编译环境

Microsoft Visual Studio Community 2022
Version 17.9.6
VisualStudio.17.Release/17.9.6+34728.123
Microsoft .NET Framework
Version 4.8.09032

Installed Version: Community

Visual C++ 2022   00482-90000-00000-AA134
Microsoft Visual C++ 2022

ASP.NET and Web Tools   17.9.199.22661
ASP.NET and Web Tools

Azure App Service Tools v3.0.0   17.9.199.22661
Azure App Service Tools v3.0.0

C# Tools   4.9.0-3.24121.1+a98c90d56455379836dd5c845b35fa932b00cfa3
C# components used in the IDE. Depending on your project type and settings, a different version of the compiler may be used.

Debugging Tools for Windows   10.0.26090.1
Integrates the Windows Debugger functionality (http://go.microsoft.com/fwlink/?linkid=223405) in Visual Studio.

Microsoft JVM Debugger   1.0
Provides support for connecting the Visual Studio debugger to JDWP compatible Java Virtual Machines

NuGet Package Manager   6.9.2
NuGet Package Manager in Visual Studio. For more information about NuGet, visit https://docs.nuget.org/

Test Adapter for Boost.Test   1.0
Enables Visual Studio's testing tools with unit tests written for Boost.Test.  The use terms and Third Party Notices are available in the extension installation directory.

Test Adapter for Google Test   1.0
Enables Visual Studio's testing tools with unit tests written for Google Test.  The use terms and Third Party Notices are available in the extension installation directory.

TypeScript Tools   17.0.30103.2001
TypeScript Tools for Microsoft Visual Studio

Visual Basic Tools   4.9.0-3.24121.1+a98c90d56455379836dd5c845b35fa932b00cfa3
Visual Basic components used in the IDE. Depending on your project type and settings, a different version of the compiler may be used.

Visual Studio IntelliCode   2.2
AI-assisted development for Visual Studio.

Windows Driver Kit   10.0.26090.1
Headers, libraries, and tools needed to develop, debug, and test Windows drivers (msdn.microsoft.com/en-us/windows/hardware/gg487428.aspx)