习一下利用修改物理内存来跨进程内存读写
 
 系统:win10 21h1 x64
 编译环境: vs2022 详情见附录
 
 
 
 
虚拟地址也称线性地址,一个线性地址+进程的DirBase地址可以转换成物理地址。先来看线性地址的含义
 
 在x64体系中只实现了48位的virtual address,高16位被用作符号扩展,这高16位要么全是0,要么全是1。
 不同于x86体系结构,每级页表寻址长度变成9位,由于在x64体系结构中,普通页大小仍为4KB,然而数据却表示64位长,因此一个4KB页在x64体系结构下只能包含512项内容,所以为了保证页对齐和以页为单位的页表内容换入换出,在x64下每级页表寻址部分长度定位9位。
 
 
 
 
从Page Map Level 4(PML4)开始到最后的物理地址,每一个都可以理解成一层页表的索引,索引值就是线性地址上不同的部分,分别缩写是PML4, PDPE, PDE,PTE。 
 
 
 
 
使用windbg可以先查看进程对应的DirBase地址,然后再使用!vtop Dirbase地址 虚拟地址查看虚拟地址对应的物理地址,如下。
 
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 3: kd> !process 258c 0
 Searching forProcess 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,就使用命令
 
| 1 | !vtop 235ae6000 0000A6E334FB00
 | 
 
得到最后对应的物理地址是0x11b10cb00。
 
简单例子代码如下:
 
| 1 2 3 4 5 6 7 8 9 10 11 | #include <stdio.h>
 #include <stdlib.h>
 intmain() {
     charflag[] = {"flag{b7285d748dd042a4929d3dbec778e637}"};
     printf("value addr: %p", flag);
     getchar();
     return0;
 }
 | 
 
运行后可以打印出来字符串的虚拟地址0000A6E334FB00,然后通过上述步骤得到物理地址。
 
我们尝试看看物理内存中的字符串,现在已经确定物理内存的地址是0xD0000147,使用!db 0xD0000147来查看物理内存,记住要!,没有感叹号的是查看虚拟内存的
 
| 1 2 3 4 5 6 7 8 9 | 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地址除了通过上述windbg直接得到这个值以外,还可以通过EPROCESS来得到,这个是代码比较需要的
 
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | 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将修改后的内容映射回物理地址
这里写一个例子来充当被攻击(修改内存)的进程。主要就是打印变量内容和地址,然后暂停程序等待一段时间(等待被驱动修改),然后再打印变量内容,看看是否被驱动修改内存成功。
 
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | #include <stdio.h>
 #include <stdlib.h>
 intmain() {
     charflag[] = {"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);
     return0;
 }
 | 
 
 
这里就是主要逻辑,通过驱动代码取修改目标进程的内存内容,做到跨进程内存读取,修改。
 
定义一个读取物理内存函数
 
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /// @brief 读取物理地址的内存内容
 /// @param address 物理地址
 /// @param buffer 复制内存地址到buffer
 /// @param size 复制大小
 /// @param BytesTransferred 读取的字节数
 /// @return
 NTSTATUS ReadPhysicalAddress(IN PVOID64 address, OUT PVOID64 buffer,
                              IN SIZE_Tsize, OUT SIZE_T* BytesTransferred)
 {
     MM_COPY_ADDRESS Read          = {0};
     Read.PhysicalAddress.QuadPart = (LONG64)address;
     returnMmCopyMemory(
         buffer, Read, size, MM_COPY_MEMORY_PHYSICAL, BytesTransferred);
 }
 | 
 
再定义一个写入物理内存的函数
 
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | /// @brief 写入指定内容到物理内存中
 /// @param address 被写入的物理地址
 /// @param buffer 需要写入的缓冲区指针
 /// @param size 需要写入的大小
 /// @param BytesTransferred 写入成功后的大小
 /// @return
 NTSTATUS WritePhysicalAddress(IN PVOID64 address, IN PVOID64 buffer,
                               IN SIZE_Tsize, OUT SIZE_T* BytesTransferred)
 {
     PVOIDmap;
     PHYSICAL_ADDRESS Write = {0};
     if(!address) {
         kprintf("Address value error. \r\n");
         returnSTATUS_UNSUCCESSFUL;
     }
     Write.QuadPart = (LONG64)address;
     map            = MmMapIoSpaceEx(Write, size, PAGE_READWRITE);
     if(!map) {
         kprintf("Write Memory faild.\r\n");
         returnSTATUS_UNSUCCESSFUL;
     }
     RtlCopyMemory(map, buffer, size);
     *BytesTransferred = size;
     MmUnmapIoSpace(map, size);
     returnSTATUS_SUCCESS;
 }
 | 
 
我们需要将虚拟地址转换成物理地址,那么首先需要线性地址+DirBase地址,DirBase地址获取是通过PEPROCESS+0x28偏移读取的
 
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | /// @brief 通过EPROCESS获取DirBase值
 /// @param pid 进程PID
 /// @param pDirbase 一个UINT64指针,获取成功后返回值
 /// @return
 NTSTATUS GetDirBaseByEprocess(IN UINT64pid, OUT PUINT64pDirbase)
 {
     PEPROCESS pEprocess;
     NTSTATUS  status;
     status = PsLookupProcessByProcessId((HANDLE)pid, &pEprocess);
     if(!NT_SUCCESS(status)) {
         kprintf("[!] Get Pid=%d _EPROCESS failed!", pid);
         returnSTATUS_UNSUCCESSFUL;
     }
     *pDirbase =
         *(PUINT64)((PUCHAR)pEprocess + WIN10_21H1_EPROCESS2DIRBASE_OFFSET);
     kprintf("[+] uDirBase ==> %llx\r\n", *pDirbase);
     returnSTATUS_SUCCESS;
 }
 | 
 
得到DirBase后,就可以虚拟地址转换物理地址。
 
传入虚拟地址后,取后48bit,然后将这48bit分成4个9bit和最后12bit,分别是PML4,PDPE,PDE,PTE和页内偏移offset。需要注意的是DirBase就已经是物理内存了,所以读取DirBase内容并且一层一层读取都要用自定义函数ReadPhysicalAddress。
 
每一层都是基地址+8*偏移,读取的内容,取12-35bit就是下一层的基地址
 
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | /// @brief 传入DirBase值和虚拟地址后,回转化成一个物理地址返回
 /// @param DirBase DirBase地址,传入一个UINT64值
 /// @param addr 传入一个指向虚拟地址的指针,转化成物理地址后会修改这个指针的值
 /// @return
 NTSTATUS TranslateAddress(IN UINT64DirBase, _Inout_ PUINT64addr)
 {
     UINT16   PML4, PDPE, PDE, PTE, offset;
     UINT64mask = 0x7fffff000;
     UINT64uTmp;
     SIZE_TBytesTransferred;
     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);
     returnSTATUS_SUCCESS;
 }
 | 
 
最后再主函数中定义一下逻辑。这里直接手动指定进程号和目标进程打印出来的变量地址,然后将虚拟地址转化成物理地址,读取物理地址上的内容并打印出来看看是否正确。再修改物理地址上的内容。
 
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING path)
 {
     NTSTATUS status;
     UINT64pid, uAddr, uDirBase;
     SIZE_TBytesTransferred;
     UCHARcharArry[40] = {0};
     UCHARexample[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");
         returnSTATUS_UNSUCCESSFUL;
     }
     // 将虚拟地址转化成物理地址
     status = TranslateAddress(uDirBase, &uAddr);
     if(!NT_SUCCESS(status)) {
         kprintf("[!] Translate address failed!\r\n");
         returnSTATUS_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");
     returnSTATUS_SUCCESS;
 }
 | 
 
 
目标进程
 
 
 
 
可以看到目标进程的指定内存被修改,同时驱动也跨进程读取,修改内存成功