Cc

VJuly 21, 2020

跨进程地址空间访问

演示

MemoryBridge

地址空间介绍

一个进程的运行,分为用户态和内核态。用户态和内核态分别占据一部分虚拟地址空间,

  • x86:用户态:0x00000000~0x7fffffff,内核态:0x80000000-0xffffffff
  • x64:用户态: 0x0000000000000000~0x000007FFFFFFFFFF ,内核态: 0xFFFFF80000000000~0xFFFFFFFFFFFFFFFF

用户态代码默认情况下是无法访问内核态地址空间的,一旦访问,则会发生错误。然而,内核态的代码却可以直接访问同进程下的用户态地址空间。

但是一个系统,是有多个进程的,每个进程都是有如上的地址空间,那么如何区分每个进程的地址空间呢?答案是,CR3。CR3是一个寄存器,是CPU中的一个组件。它唯一且只能用来保存一个进程的页目录地址。页目录是存放页表的地方,而页表则被CPU是用来将检索虚拟地址到物理地址的映射。

跨进程地址空间访问

在一个进程内,访问一个地址空间是可以通过一条指令可以直接获取的,比如mov eax, [地址]。然而对于不同的进程来说,地址空间之间都是相互独立的,无法直接访问其它进程的地址空间内容。然而,程序总是会有这样需求,要访问其它进程的地址空间。由此便产生了,进程间通信技术,比如共享内存,邮件槽,管道等。但是总是有那么一种极端的情况,我们期望可以直接读取对方地址,像一条指令那样。Windows便提供了这样的方法(函数):

BOOL ReadProcessMemory(
  HANDLE  hProcess,
  LPCVOID lpBaseAddress,
  LPVOID  lpBuffer,
  SIZE_T  nSize,
  SIZE_T  *lpNumberOfBytesRead
);
BOOL WriteProcessMemory(
  HANDLE  hProcess,
  LPVOID  lpBaseAddress,
  LPCVOID lpBuffer,
  SIZE_T  nSize,
  SIZE_T  *lpNumberOfBytesWritten
);

这两个函数非常类似,一个是读进程内存,一个是写进程内存,参数介绍如下:

  • hProcess:要读/写的进程句柄,通过CreateProcess/OpenProcess获得。
  • lpBaseAddress:要读取的目标地址空间下的地址。
  • lpBuffer:读:保存读取到的数据内容,写:要写入的数据内容
  • nSize:读/写的长度。
  • lpNumberOfBytesWritten:最终实际读写的长度。

利用该函数可以实现用户态跨进程的读写,但是有一个关键的前提,就是你需要有目标进程的句柄hProcess,它可以由如下函数获得:

HANDLE OpenProcess(
  DWORD dwDesiredAccess,
  BOOL  bInheritHandle,
  DWORD dwProcessId
);
  • dwDesiredAccess:指明要获得的权限,对于读写目标进程来说,你需要 PROCESS_VM_WRITE 和 PROCESS_VM_READ 和PROCESS_VM_OPERATION 权限,简单的来说可以直接指定PROCESS_ALL_ACCESS
  • bInheritHandle:指明是否继承句柄。
  • dwProcessID:要打开的目标进程 PID。

当然,不是所有的进程地址空间,你都可以打开的,许多系统进程是无法直接打开的。即使你是以管理员权限运行。

内核下的跨进程访问<1>

高地址空间下的的内核态地址是每个进程所共享的,而运行在内核地址空间的代码,在某一时刻只能访问同一进程地址空间的内容。

在Windows内核中提供了这样个函数,用来实现跨进程地址空间的访问。

NTSTATUS
MmCopyVirtualMemory(
  IN  PEPROCESS FromProcess,
  IN  CONST VOID *FromAddress,
  IN  PEPROCESS ToProcess,
  OUT PVOID ToAddress,
  IN  SIZE_T BufferSize,
  IN  KPROCESSOR_MODE PreviousMode,
  OUT PSIZE_T NumberOfBytesCopied
  );
  • FromProcess:是源进程的EPROCESS指针
  • FromAddress:是源进程的地址
  • ToProcess和ToAddress:与上两个参数一致
  • BufferSize:要拷贝的内存大小
  • PreviousMode:表明执行此动作的模式,可以为UserMode或者KernelMode
  • NumberOfBytesCopied:是实际发生考虑的长度。

而对于某个特定进程EPROCESS结构的获取,可以通过函数PsLookupProcessByProcessId来获取。

内核下的跨进程访问<2>

MDL

这种方法的核心思想是将目标进程的物理地址映射到当前进程的内核地址空间,调整权限,再进行读取。而这种方式的核心思想就是MDL。

MDL是用来Windows用来描述物理地址状态的一个数据结构,其结构如下:

   +0x000 Next             : Ptr64 _MDL
   +0x008 Size             : Int2B
   +0x00a MdlFlags         : Int2B
   +0x00c AllocationProcessorNumber : Uint2B
   +0x00e Reserved         : Uint2B
   +0x010 Process          : Ptr64 _EPROCESS
   +0x018 MappedSystemVa   : Ptr64 Void
   +0x020 StartVa          : Ptr64 Void
   +0x028 ByteCount        : Uint4B
   +0x02c ByteOffset       : Uint4B

需要关注的几个成员:

  • MappedSystemVa:表示最终映射的系统地址
  • StartVA:指定的映射的虚拟地址的页基址
  • ByteOffset:指定的虚拟地址相对于所属页的偏移
  • ByteCount:指定虚拟地址的Buffer的长度

MDL描述的所有物理页的地址,最终保存在PFN Array中,它位于MDL结构的结尾,即PMDL + 1处,基本上是这样一个结构MDL|PFN0,PFN1,...PFN N

IoAllocateMdl

该函数用来申请一个相应的MDL结构,其函数定义如下:

PMDL IoAllocateMdl(
  __drv_aliasesMem PVOID VirtualAddress,
  ULONG                  Length,
  BOOLEAN                SecondaryBuffer,
  BOOLEAN                ChargeQuota,
  PIRP                   Irp
);

该函数的作用是申请一个MDL用以映射参数VirtualAddress和Length所描述的Buffer。如果指定了IRP,则会将该MDL插入到该Irp的Mdl chain中,IRP不是本文的关注点,可以略过。

核心:其实就做一件事,就是根据传入的参数VirtualAddress和Length,计算出所需要的页面数量,然后申请对应数量的PFN Array + MDL头部结构,初始化成员。

MmProbeAndLockPages 和 MmProbeAndLockProcessPages

MmProbeAndLockProcessPages用于MDL描述的虚拟地址位于其它进程的地址空间的情况,实现和MmProbeAndLockPages一样,只不过是在调用MmProbeAndLockPages前,先KeStackAttachProcess发生地址空间切换。这两个函数定义如下:

void MmProbeAndLockPages(
  PMDL            MemoryDescriptorList,
  KPROCESSOR_MODE AccessMode,
  LOCK_OPERATION  Operation
);
VOID
MmProbeAndLockProcessPages(
      __inout PMDL MemoryDescriptorList,
      __in PEPROCESS Process,
      __in KPROCESSOR_MODE AccessMode,
      __in LOCK_OPERATION Operation
  );
  • MemoryDescriptorList:目标MDL
  • AccessMode:指明你要Probe时的访问模式,KernelMode或者UserMode
  • Operation:指明要操作的类型,IoReadAccess,IoWriteAccess,IoModifyAccess等。

一定要注意,这两函数虽然没有返回值,但是其内部在处理错误时,会抛出异常,因此需要加入到__try{}__except中。

核心:该函数内部逻辑复杂,然而其本质就是在做一件事,将目标VA触发换页动作,发生页面错误处理,使得其PTE内容有效,然后获取对应的PFN值,填入到该MDL的PFN Array中。

该过程会处理到的情况:

  1. 会对用户态的地址页面进行Probe的动作,若是IoWriteAccess,则还会触发一次写。
  2. 获取目标地址的PTE地址,然后利用MmAccessFault来进行页面错误处理,更新将PTE状态为valid。
  3. 若是IoWriteAccess,则判断其PTE是否可写,若不可写,则检测是否有COW标志,若是则重新执行COW的动作。
  4. 获取PFN值,遍历当前进程的PhysicalVadRoot树来检测该PFN否在当前进程的物理页面中。
  5. 更新PFN Array。

MmMapLockedPagesSpecifyCache

该函数用来将一个MDL描述的物理页到系统地址或者是用户虚拟地址空间。函数原型如下:

PVOID MmMapLockedPagesSpecifyCache(
  PMDL MemoryDescriptorList,
  KPROCESSOR_MODE AccessMode,
  MEMORY_CACHING_TYPE CacheType,
  PVOID RequestedAddress,
  ULONG BugCheckOnFailure,
  ULONG Priority
);
  • MemoryDescriptorList:目标MDL。
  • AccessMode:指示这些页映射到哪里,是用户态地址空间还是内核态地址空间。
  • CacheType:指明该MDL要使用的Cache类型。默认为MmCached。
  • RequestedAddress:只有当AccessMode为UserMode时才有效,指明你想分配到的虚拟地址。
  • BugCheckOnFailure:不言而喻,失败否是BugCheck。

核心:该函数的对于内核地址的映射就是从SystemPTE区域维护的空闲PTE链表上摘下对应的PTE项来,将该MDL描述的PFN值填入,返回对应的SystemPTE Adresss。

核心:而要映射到用户态,则会构建一个VadDevicePhysicalMemory类型的VAD,若没有指定RequestedAddress,则从VAD树中动态获取一个有效的地址范围返回,将该MDL描述的PFN值填入到这些虚拟地址的PTE中。最后将该VAD插入到当前进程的VAD树中。其间还会创建一个结构为PMI_PHYSICAL_VIEW的Address Node,用以更新当前进程的Process->PhysicalVadRoot。

POC

本POC用于那些有心研究该技术,然而代码功力略微薄弱的同学。
抵制拿来主义,因此设置了条件,贴出来的是核心的实现代码,买了也没法直接编译,需要有一些基础的代码添加即可。
所以建议别买!!!一旦买了,退是不可能退的,想清楚!!!

价格: 999.00 元
VIP会员价格:999.00元终身会员免费
温馨提示:登录付款后可永久阅读隐藏的内容。 付费可读

内核下的跨进程访问<3>

待续。。。

本文来自投稿,不代表本站立场,如若转载,请注明出处:https://int0x3.cc/archives/10/
252

发表评论

内核解决方案

内核解决方案

提供Windows内核相关的解决方案