ReadProcessMemory函数的分析「建议收藏」

ReadProcessMemory函数的分析「建议收藏」ReadProcessMemory函数用于读取其他进程的数据。我们知道自远古时代结束后,user模式下的进程都有自己的地址空间,进程与进程间互不干扰,这叫私有财产神圣不可侵犯。但windows里还真就提供了那么一个机制,让你可以合法的获取别人的私有财产,这就是ReadProcessMemory和WriteProcessMemory。为什么一个进程居然可以访问另一个进程的地址空间呢?因为独立的只是低

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE稳定放心使用

ReadProcessMemory函数用于读取其他进程的数据。我们知道自远古时代结束后,user模式下的进程都有自己的地址空间,进程与进程间互不干扰,这叫私有财产神圣不可侵犯。但windows里还真就提供了那么一个机制,让你可以合法的获取别人的私有财产,这就是ReadProcessMemory和WriteProcessMemory。为什么一个进程居然可以访问另一个进程的地址空间呢?因为独立的只是低2G的用户态空间,高2G的内核态空间是所有进程共享的。一段执行中的线程进入内核态后,它可以拿到别人的cr3寄存器,用该cr3替换自己的cr3便完成了地址空间的转换。理论说明完毕,下面来看实现细节:gussing.cnblogs.com

BOOL
STDCALL
ReadProcessMemory (
	HANDLE	hProcess,
	LPCVOID	lpBaseAddress,
	LPVOID	lpBuffer,
	DWORD	nSize,
	LPDWORD	lpNumberOfBytesRead
	)
{

	NTSTATUS Status;

	Status = NtReadVirtualMemory( hProcess, (PVOID)lpBaseAddress,lpBuffer, nSize,
		(PULONG)lpNumberOfBytesRead
		);

	if (!NT_SUCCESS(Status))
     	{
		SetLastErrorByStatus (Status);
		return FALSE;
     	}
	return TRUE;
}
 
这是用户态ReadProcessMemory的实现,它只做了一件事那就是调用NtReadVirtualMemory。NtReadVirtualMemory函数位于ntdll中,属于所谓的桩函数,
作用就是把用户态的函数调用翻译成相应的系统调用,进入内核态。内核中一般有一个相同名字的处理函数,接收到该类型的系统调用后做实际的工作。系统调用
的细节按下不表,让我们来看NtReadVirtualMemory到底在做什么事情:gussing.cnblogs.com
NTSTATUS STDCALL
NtReadVirtualMemory(IN HANDLE ProcessHandle,
                    IN PVOID BaseAddress,
                    OUT PVOID Buffer,
                    IN ULONG NumberOfBytesToRead,
                    OUT PULONG NumberOfBytesRead)
{
   NTSTATUS Status;
   PMDL Mdl;
   PVOID SystemAddress;
   PEPROCESS Process;

   DPRINT("NtReadVirtualMemory(ProcessHandle %x, BaseAddress %x, "
          "Buffer %x, NumberOfBytesToRead %d)\n",ProcessHandle,BaseAddress,
          Buffer,NumberOfBytesToRead);

   Status = ObReferenceObjectByHandle(ProcessHandle,
                                      PROCESS_VM_WRITE,
                                      NULL,
                                      UserMode,
                                      (PVOID*)(&Process),
                                      NULL);
 
   if (Status != STATUS_SUCCESS)
   {
      return(Status);
   }
ObReferenceObjectByHandle函数从代表目标进程的handle里获取EPROCESS类型的指针,存放在变量Process中。EPROCESS结构保存了能代表一个进程的
几乎所有关键数据,包括我们这里急需的cr3。gussing.cnblogs.com
struct _EPROCESS
{
  /* Microkernel specific process state. */
  KPROCESS              Pcb;                          /* 000 */
。。。/*其他*/
typedef struct _KPROCESS 
{
  /* So it's possible to wait for the process to terminate */
  DISPATCHER_HEADER 	DispatcherHeader;             /* 000 */
  /* 
   * Presumably a list of profile objects associated with this process,
   * currently unused.
   */
  LIST_ENTRY            ProfileListHead;              /* 010 */
  /*
   * We use the first member of this array to hold the physical address of
   * the page directory for this process.
   */
  PHYSICAL_ADDRESS      DirectoryTableBase;           /* 018 这是cr3*/
。。。/*其他*/
接下来是从目标地址里创建一个MDL并将其锁定在主存里:gussing.cnblogs.com
   Mdl = MmCreateMdl(NULL,
                     Buffer,
                     NumberOfBytesToRead);
   MmProbeAndLockPages(Mdl,
                       UserMode,
                       IoWriteAccess);
为什么要创建这个MDL?等会儿再说。
然后是最关键的一步,当前线程要当逃兵,叛逃至目标进程里了。。。gussing.cnblogs.com
   KeAttachProcess(Process);
执行完KeAttachProcess后,当前线程就成了Process进程所属的线程了,悲剧啊。怎么着咱们就被策反了呢?细节我们等下再看,让我们完成主逻辑先。gussing.cnblogs.com
   SystemAddress = MmGetSystemAddressForMdl(Mdl);
   memcpy(SystemAddress, BaseAddress, NumberOfBytesToRead);
   KeDetachProcess();
   if (Mdl->MappedSystemVa != NULL)
   {
      MmUnmapLockedPages(Mdl->MappedSystemVa, Mdl);
   }
   MmUnlockPages(Mdl);
   ExFreePool(Mdl);

   ObDereferenceObject(Process);

   *NumberOfBytesRead = NumberOfBytesToRead;
   return(STATUS_SUCCESS);
}
attach到目标进程里之后,我们又从之前生成好的MDL里获取一个虚拟地址映射,然后执行memcpy操作。这下为什么要创建MDL的秘密就清楚了,假如我们直接这样
写memcpy:gussing.cnblogs.com
   memcpy(Buffer, BaseAddress, NumberOfBytesToRead);
看着好像没什么问题,其实问题很大。Buffer所代表的地址应该是前一个进程空间里的,但现在确实新进程空间里的,根本不是一回事。我们费劲拷贝
过去的数据,其实位于错误的内存里,等KeDetachProcess执行完切回原来的进程空间后,这些数据就全丢了,找都没地方找去。所以我们应该先从Buffer里
生成一个MDL,切换进程完成后再从该MDL里反生成一个Virtual Address,然后memcpy就可以正确的将数据拷贝到该去的地方了。
完成内存拷贝后,KeDetachProcess函数又将我们的线程从Process进程转回原来的进程,这下好,数据也偷到了,组织也回归了,原来这家伙是个间谍啊。。。
现在我们可以来看看KeAttachProcess函数到底做了什么事情了。核心行为很明确,那就是替换cr3,但是细节到底如何呢:gussing.cnblogs.com
VOID STDCALL
KeAttachProcess (PEPROCESS Process)
{
   KIRQL oldlvl;
   PETHREAD CurrentThread;
   PULONG AttachedProcessPageDir;
   ULONG PageDir;
   
   DPRINT("KeAttachProcess(Process %x)\n",Process);
   
   CurrentThread = PsGetCurrentThread();

   if (CurrentThread->OldProcess != NULL)
     {
	DbgPrint("Invalid attach (thread is already attached)\n");
	KEBUGCHECK(0);
     }
   
   KeRaiseIrql(DISPATCH_LEVEL, &oldlvl);

   KiSwapApcEnvironment(&CurrentThread->Tcb, &Process->Pcb);
这里我们把当前的IRQL提升到了DPC level,为的就是防止线程切换。然后调用KiSwapApcEnvironment把当前的apc队列也贴到目标进程里,按下不表。gussing.cnblogs.com
      /* The stack of the current process may be located in a page which is
      not present in the page directory of the process we're attaching to.
      That would lead to a page fault when this function returns. However,
      since the processor can't call the page fault handler 'cause it can't
      push EIP on the stack, this will show up as a stack fault which will
      crash the entire system.
      To prevent this, make sure the page directory of the process we're
      attaching to is up-to-date. */

   AttachedProcessPageDir = ExAllocatePageWithPhysPage(Process->Pcb.DirectoryTableBase);
   MmUpdateStackPageDir(AttachedProcessPageDir, &CurrentThread->Tcb);
   ExUnmapPage(AttachedProcessPageDir);
接下来如注释所说,Process->Pcb.DirectoryTableBase所代表的数据很有可能正在硬盘里的,物理如何也要保证它在内存里,因为函数返回时要做栈操作,
如果Process->Pcb.DirectoryTableBase在硬盘上,栈操作就会引起page fault,而处理page fault前又必须要push eip,悲剧就要发生了。同样的,
stack base 和 stack top这两哥们也一定得在内存里,MmUpdateStackPageDir做的就是这个事情。gussing.cnblogs.com
   CurrentThread->OldProcess = PsGetCurrentProcess();
   CurrentThread->ThreadsProcess = Process;
   PageDir = Process->Pcb.DirectoryTableBase.u.LowPart;
   DPRINT("Switching process context to %x\n",PageDir);
   Ke386SetPageTableDirectory(PageDir);
   KeLowerIrql(oldlvl);
}
最后做的事情就简单了,把当前线程的ThreadsProcess换成新的,再把当前的cr3换成Process->Pcb.DirectoryTableBase.u.LowPart。一番梳妆打扮后,
敌人就分不清咱的身份了。
至此为止,ReadProcessMemory函数分析完毕。个人觉得有几个细节是需要注意的:第一呢,lpBaseAddress和lpBuffer所在的进程空间是不同的。第二呢,
KeRaiseIrql和KeLowerIrql这两个函数一定要限制在进程空间切换的函数内,绝对不能把memcpy放在它们中间,因为KeRaiseIrql之后page fault就没法处理
了,而memcpy不产生page fault那是不可能的,想都不要想。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/185918.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • springboot集成Swagger2「建议收藏」

    springboot集成Swagger2「建议收藏」Swagger2简介 我们提供Restful接口的时候,API文档是尤为的重要,它承载着对接口的定义,描述等。它还是和API消费方沟通的重要工具。在实际情况中由于接口和文档存放的位置不同,我们很难及时的去维护文档。1.随项目自动生成强大RESTful API文档,减少工作量2.API文档与代码整合在一起,便于同步更新API说明3.页面测试功能来调试每个RESTful API…

    2022年6月13日
    26
  • 解决了无法显示验证码的问题怎么办_验证码不能显示的问题

    解决了无法显示验证码的问题怎么办_验证码不能显示的问题 晚上检测一个站的时候,猜解出了密码,扫出了后台,可验证码就是无法显示,难道管理员故意弄的?不太可能吧?于是上网一搜,没想到还真找到了解决的方法。我的是VistaUltimate,部分XPSP2也会有这个问题。好了,不废话了,解决办法如下:运行regedit,找到“HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/InternetExplorer/Securit

    2022年9月6日
    3
  • 前端框架bootstrap和layui有什么区别

    前端框架bootstrap和layui有什么区别做前端的小伙伴肯定都用过或听过Bootstrap和LayUi,小编我虽然不是专业的前端程序员,但是对于前端还是颇有研究,闲暇事情会经常研究各种前端框架的源码,一来可以借鉴优秀框架的思想,二来可以顺便学习可以提高自己,好了,不废话了。web前端全栈资料粉丝福利(面试题、视频、资料笔记、进阶路线)先看百度Bootstrap的定义Bootstrap是美国Twitter公司的设计师MarkOtto和JacobThornton合作基于HTML、CSS、JavaScript开发的简洁、直观、强悍的前端

    2022年6月25日
    43
  • vue实现文件上传和下载_vue上传文件前端完整实例

    vue实现文件上传和下载_vue上传文件前端完整实例文件上传这里使用elementui组件库的文件上传组件1.手动上传(文件选取后需点击确认上传)action:上传地址auto-upload:是否在选取文件后立即进行上传,默认true手动上传要将其设置为falsebefore-upload:上传文件之前的钩子,参数为上传的文件,上传格式的规定要求可在此钩子函数中写(示例中规定上传格式xlsx或xls)on-success:文件上传成功时的钩子,function(response,file,fileList)ref:注册DOM对象(点

    2022年8月16日
    10
  • Mysql 行转列 + json

    Mysql 行转列 + json[code="java"]SET@EE=”;SET@str_tmp=”;SET@Revenue_JSON=”;SET@Revenue_JSON_tmp=”;SELECT@EE:=CONCAT( @EE, ‘SUM(IF(f…

    2022年6月1日
    56
  • SBC协议_蓝牙耳机sbc怎么改

    SBC协议_蓝牙耳机sbc怎么改sbcenc.c*main(intargc,charargv[])首先设定option的默认值,然后根据用户命令设定option相关参数。对指定文件进行编码(执行encode函数)usage(void)打印相关option:OptionOption打印帮助信息hhelp打印帮助信息vverbose详细模式mmsbcmSBC编解码器ssubbands子带数量(4/8)bbitpoolBitpoolvalue

    2022年9月11日
    0

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号