• Windows x64隐藏可执行内存


    实现效果

    在这里插入图片描述

    驱动程序在Test进程中申请一块内存地址并打印,然后控制台程序在接收到输入的地址后开始跳转执行。申请的内存必须具有可执行属性,否则三环程序会抛异常。
    在这里插入图片描述

    打开CE查看0x1F0000的内存,属性仅显示可读可写,再查看内存区域,1F0000的地址处也只显示读写属性,说明我们成功的隐藏了一块可执行内存。

    实现原理

    实现的原理其实很简单,就是在申请一块不可执行的内存后,通过修改PDE和PTE的属性位,将这块连续的内存区域设置为可执行,从而达到隐藏可执行内存的目的。

    所需要的前置知识有两块,一个是VAD内存管理,一个是x64的分页机制。

    实际上病毒木马也会用到这个操作,在申请内存的时候先申请一块不可执行的内存,然后再通过VirtualProtetc的方式来修改内存属性,这样可以躲避掉一部分的杀软查杀。尽管隐藏的方式比较low,但好过直接申请可执行的内存。

    VAD内存

    什么是VAD内存
    0: kd> dt _EPROCESS ffffaa0daaa3f080
    ntdll!_EPROCESS
    +0x628 VadRoot          : _RTL_AVL_TREE
    
    • 1
    • 2
    • 3

    在EPROCESS进程结构体+0x628的位置有一个成员叫VadRoot,这是一个二叉树结构,里面保存了这个进程所有内存的信息。

    查看VAD内存

    再来查看一下当前这块进程的vad内存,用!vad命令

    在这里插入图片描述

    其中:

    • Level表示层级
    • start表示起始地址,这个是页号,真正的虚拟地址要乘以0x1000
    • end是结束地址 也是页号
    • Commit表示提交属性
    • Mapped表示映射内存
    • Private表示私有内存
    • 倒数第二列是内存属性

    在这里插入图片描述

    • 如果commit属性为Mapoed Exe,说明这是一个私有PE文件的映射,在最后一列会出现PE文件的路径

    可以看到VAD树把当前进程所有的内存块都清晰的展现出来了,如果你在进程里开辟了一块内存用来执行shellcode的话,那么只要一遍历,直接就能查出来。

    VAD属性

    VAD是管理虚拟内存的,每一个进程有自己单独的一个VAD树 , 使用VirtualAllocate函数申请一个内存,则会在VAD树上增加一个结点,是_MMVAD结构体,私有内存一般是MMVAD_SHORT,映射内存一般是MMVAD_LONG’

    _MMVAD结构

    0: kd> dt _MMVAD
    nt!_MMVAD
       +0x000 Core             : _MMVAD_SHORT
       +0x040 u2               : 
       +0x048 Subsection       : Ptr64 _SUBSECTION
       +0x050 FirstPrototypePte : Ptr64 _MMPTE
       +0x058 LastContiguousPte : Ptr64 _MMPTE
       +0x060 ViewLinks        : _LIST_ENTRY
       +0x070 VadsProcess      : Ptr64 _EPROCESS
       +0x078 u4               : 
       +0x080 FileObject       : Ptr64 _FILE_OBJECT
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    _MMVAD_SHORT结构

    kd> dt _MMVAD_SHORT 876f4f50  -r1
    nt!_MMVAD_SHORT
       +0x000 u1               : 
          +0x000 Balance          : 0y00
          +0x000 Parent           : 0x8757afc0 _MMVAD //有无父节点
       +0x004 LeftChild        : (null) 
       +0x008 RightChild       : (null) //有无左子树与右子树
       +0x00c StartingVpn      : 0xd0
       +0x010 EndingVpn        : 0xd0//起始页与结束页
       +0x014 u                : //锁页
          +0x000 LongFlags        : 0x98000001
          +0x000 VadFlags         : _MMVAD_FLAGS
       +0x018 PushLock         : _EX_PUSH_LOCK
          +0x000 Locked           : 0y0
          +0x000 Waiting          : 0y0
          +0x000 Waking           : 0y0
          +0x000 MultipleShared   : 0y0
          +0x000 Shared           : 0y0000000000000000000000000000 (0)
          +0x000 Value            : 0
          +0x000 Ptr              : (null) 
       +0x01c u5               : 
          +0x000 LongFlags3       : 0
          +0x000 VadFlags3        : _MMVAD_FLAGS3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    kd> dt _MMVAD_FLAGS 876f4f50+14
    nt!_MMVAD_FLAGS
       +0x000 CommitCharge     : 0y0000000000000000001 (0x1)
       +0x000 NoChange         : 0y0//如果为1则不可改属性
       +0x000 VadType          : 0y000//类型
       +0x000 MemCommit        : 0y0
       +0x000 Protection       : 0y11000 (0x18)//保护
       +0x000 Spare            : 0y00
       +0x000 PrivateMemory    : 0y1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    VAD内存可利用的点

    之前我们说了句柄的对抗,实际上游戏和杀软在内存上也会做很多手脚,来达到保护自身的目的。这里有几个可以操作的点。

    保护内存属性不被修改

    一个是修改_MMVAD_FLAGS结构里面的NoChange字段,这个字段被置为1之后,表示这块内存属性不能被修改,即使你调用VirtualProtect这样的API也是没用的。可以保护自己的内存属性不被修改。

    TP一字节保护

    bool ProtectMemory(void* memory)
    {
        auto vad = GetMemoryVad(memory);
        if(vad) {
            vad->Core.u.LongFlags &= 0x10000;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    TP的一字节保护也是修改了其中的一个标志位,这个标志位被修改后,你就不能再修改内存属性了

    隐藏私有内存

    还有一个可以玩的地方是可以利用VAD来隐藏私有内存

    +0x00c StartingVpn      : 0xd0
    +0x010 EndingVpn        : 0xd0//起始页与结束页
    
    • 1
    • 2

    这种方法需要找到隐藏的VAD节点,让StartingVpn等于EndingVpn,就可以达到隐藏效果了。跟断链的套路一样,只不过这里断的是二叉树。

    这种方式仅限于隐藏一小块不频繁使用的私有内存,而且有蓝屏的几率,不稳定。

    x64分页机制

    说完了VAD的内存管理,再来说x64的分页机制。这里只说如何在x64下通过任意一个虚拟地址获取PDE和PTE的方法。其他的分页知识各位需要自行补充。

    #### x86 PDE PTE基址

    PTEBase=0xC0000000 
    PDEBase=0xC0300000
    
    • 1
    • 2

    这个是XP下的PTE和PTE的基地址

    访问页目录表的公式:0xC0300000+PDI*4
    访问页表的公式:0xC0000000+PDI*4096+PTI*4
    
    • 1
    • 2

    再通过这个公式,我们就可以修改任意一个地址的PDE和PTE属性。

    W7 x64下任意地址PDT PTE算法

    在这里插入图片描述

    在IDA中找到MiIsAddressValid这个函数,这个函数的原理就是通过查看PDE和PTE的P位是否为零的方式,来判断当前的内存地址是否有效。

    从下往上,分别是PTE,PDE,PPE和PXE的算法,在w7x64PDE和PTE的基地址都是固定的,直接拿过来用就可以了。这个没什么可说的。

    W10 x64定位随机化页表基址

    到了W10 x64下,情况就不一样了, windows 10 14316开始实现了页表随机化 ,PTE的基地址变成了随机的。但是我们只要把PTE的基地址拿到了,剩下的三个基地址就可以推算出来。

    获取PTE基地址

    在这里插入图片描述

    W10我们可以用这个函数``MmGetVirtualForPhysical`,其中rdx里面的值是页表基地址,也就是PTE的基地址,拿到了这个基地址,其他的就可以推算出来了。

    在这里插入图片描述

    MmGetVirtualForPhysical进行反汇编比对PTE,发现地址是一样的。有了PTE的基地址,那么我们就可以拿到任意一个地址的PTE

    拿任意一个地址的PTE的公式(这里要取后48位):

    (f807`16a90fb0/0x1000)*8
    
    • 1

    简化一下就成了这个样子

    (f807`16a90fb0>>12)<<3+PTEBase
    
    • 1

    在这里插入图片描述

    实际上跟IDA里面的这个pde的算法是一样的,两种都可以,只不过我用的比较方便理解

    根据PTE求PDE

    把PTE当成一个虚拟地址来求物理地址,求得的物理地址就是PDT,原理就是指向PTE所在物理页面的PTE是PDE。

    PDEBase=(B08000000000>>12)<<3+FFFFB08000000000
    
    • 1

    在这里插入图片描述

    0: kd> !pte 0
                                               VA 0000000000000000
    PXE at FFFFB0D86C361000    PPE at FFFFB0D86C200000    PDE at FFFFB0D840000000    PTE at FFFFB08000000000
    contains 0A000000072D3867  contains 0000000000000000
    pfn 72d3      ---DA--UWEV  contains 0000000000000000
    not valid
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    算出来的PDE结果是一样的

    根据PDE算PPE

    同样把PDE看成是一个普通的虚拟地址

    PPEBase=(B0D840000000>>12)<<3+PTEBase
    
    • 1

    根据PPE算PXE

    PXEBase=(B0D86C200000>>12)+PTEBase
    
    • 1

    实际上,定位随机化页表基址有很种方法,各路神仙好像都有自己的套路,鹅厂的方法是通过CR3里面的字段计算拿到页表基地址,有兴趣可以自行研究一下。用哪个都不重要,理解透一个以后封装拿来用就行了。

    那么有了任意地址的PDE和PTE算法之后,我们就实现了修改任意一个地址读写属性的操作了。

    实现隐藏可执行内存

    最后万事俱备,再来实现隐藏可执行内存的操作。示例代码如下:

    PVOID AllocateMemory(HANDLE pid, SIZE_T size)
    {
    	//获取进程结构体
    	PEPROCESS Process=NULL;
    	PVOID BaseAddress = 0;
    	NTSTATUS status= PsLookupProcessByProcessId(pid, &Process);
    	if (!NT_SUCCESS(status))
    	{
    		return NULL;
    	}
    
    	//判断进程是否退出
    	if (PsGetProcessExitStatus(Process)!=STATUS_PENDING)
    	{
    		ObDereferenceObject(Process);
    		return NULL;
    	}
    
    	//附加进程
    	KAPC_STATE kApcState = {0};
    	KeStackAttachProcess(Process, &kApcState);
    
    	//附加 
    	ZwAllocateVirtualMemory(NtCurrentProcess(), &BaseAddress, 0, &size, MEM_COMMIT, PAGE_READWRITE);
    	if (NT_SUCCESS(status))
    	{
    		RtlZeroMemory(BaseAddress, size);
    
    		memcpy(BaseAddress, shellcode, sizeof(shellcode));
    
    		//修改PDE和PTE 设置可写和可执行属性
    		SetExecutePage(BaseAddress, size);
    	}
    
    	KeUnstackDetachProcess(&kApcState);
    
    	return BaseAddress;
    
    }
    
    • 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

    实际上就是申请一块内存,在申请完了之后用修改PDE和PTE属性的方式来修改内存属性。

    隐藏内存对抗

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E9osXzn6-1670137929254)(Windows x64隐藏可执行内存.assets/1669908278279.png)]

    在CE的设置里面把这几个复选框给勾上,然后重启CE,重新附加进程内存。

    在这里插入图片描述

    会发现此时我们的内存属性显示成了真正的可读可写可执行的内存方式,这是因为CE在开启这几个选项之后,会利用驱动查PDE PTE属性的方式来查询当前的内存。

    在这里插入图片描述

    为什么知道是驱动?因为现在CE不仅可以读取三环的地址,也可以读取到零环的内存地址。

    不过应该没有哪家公司会用这种方式来进行检测,毕竟这对抗都到物理内存层面了,而且CE上面的选项也提示了,开启之后扫描会变得很慢。

    在这里插入图片描述

    再来看这个VAD树的遍历,依然还是欺骗过去了。

    完整代码:

    https://download.csdn.net/download/qq_38474570/87230134?spm=1001.2014.3001.5501

  • 相关阅读:
    集合框架----源码解读Vector篇
    【Windows】安装win10虚拟机
    GPE(Grafana+Prometheus+Exporter)项目实战之Golang篇(上)
    java添加图片到Excel单元格中(POI)
    3.1 - 程序设计语言 3.2 - 高级语言的特点及引用 3.3 - 静态/动态类型语言
    关于:在企业局域网中启用 Delivery Optimization
    Leetcode66. 加一
    48MySQL数据库基础
    【人工智能深度报告--机器人系列技术篇】
    企业IT信息化三阶段:追随、协同,到引领
  • 原文地址:https://blog.csdn.net/qq_38474570/article/details/128172936