文章目录
- 实现效果
- 实现原理
- VAD内存
- 什么是VAD内存
- 查看VAD内存
- VAD属性
- VAD内存可利用的点
- x64分页机制
- W7 x64下任意地址PDT PTE算法
- W10 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
在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 : <unnamed-tag>+0x048 Subsection : Ptr64 _SUBSECTION+0x050 FirstPrototypePte : Ptr64 _MMPTE+0x058 LastContiguousPte : Ptr64 _MMPTE+0x060 ViewLinks : _LIST_ENTRY+0x070 VadsProcess : Ptr64 _EPROCESS+0x078 u4 : <unnamed-tag>+0x080 FileObject : Ptr64 _FILE_OBJECT
_MMVAD_SHORT
结构
kd> dt _MMVAD_SHORT 876f4f50 -r1
nt!_MMVAD_SHORT+0x000 u1 : <unnamed-tag>+0x000 Balance : 0y00+0x000 Parent : 0x8757afc0 _MMVAD //有无父节点+0x004 LeftChild : (null) +0x008 RightChild : (null) //有无左子树与右子树+0x00c StartingVpn : 0xd0+0x010 EndingVpn : 0xd0//起始页与结束页+0x014 u : <unnamed-tag>//锁页+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 : <unnamed-tag>+0x000 LongFlags3 : 0+0x000 VadFlags3 : _MMVAD_FLAGS3
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
VAD内存可利用的点
之前我们说了句柄的对抗,实际上游戏和杀软在内存上也会做很多手脚,来达到保护自身的目的。这里有几个可以操作的点。
保护内存属性不被修改
一个是修改_MMVAD_FLAGS
结构里面的NoChange
字段,这个字段被置为1之后,表示这块内存属性不能被修改,即使你调用VirtualProtect
这样的API也是没用的。可以保护自己的内存属性不被修改。
TP一字节保护
bool ProtectMemory(void* memory)
{auto vad = GetMemoryVad(memory);if(vad) {vad->Core.u.LongFlags &= 0x10000;}
}
TP的一字节保护也是修改了其中的一个标志位,这个标志位被修改后,你就不能再修改内存属性了
隐藏私有内存
还有一个可以玩的地方是可以利用VAD来隐藏私有内存
+0x00c StartingVpn : 0xd0
+0x010 EndingVpn : 0xd0//起始页与结束页
这种方法需要找到隐藏的VAD节点,让StartingVpn
等于EndingVpn
,就可以达到隐藏效果了。跟断链的套路一样,只不过这里断的是二叉树。
这种方式仅限于隐藏一小块不频繁使用的私有内存,而且有蓝屏的几率,不稳定。
x64分页机制
说完了VAD的内存管理,再来说x64的分页机制。这里只说如何在x64下通过任意一个虚拟地址获取PDE和PTE的方法。其他的分页知识各位需要自行补充。
#### x86 PDE PTE基址
PTEBase=0xC0000000
PDEBase=0xC0300000
这个是XP下的PTE和PTE的基地址
访问页目录表的公式:0xC0300000+PDI*4
访问页表的公式:0xC0000000+PDI*4096+PTI*4
再通过这个公式,我们就可以修改任意一个地址的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
简化一下就成了这个样子
(f807`16a90fb0>>12)<<3+PTEBase
实际上跟IDA里面的这个pde的算法是一样的,两种都可以,只不过我用的比较方便理解
根据PTE求PDE
把PTE当成一个虚拟地址来求物理地址,求得的物理地址就是PDT,原理就是指向PTE所在物理页面的PTE是PDE。
PDEBase=(B08000000000>>12)<<3+FFFFB08000000000
0: kd> !pte 0VA 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
算出来的PDE结果是一样的
根据PDE算PPE
同样把PDE看成是一个普通的虚拟地址
PPEBase=(B0D840000000>>12)<<3+PTEBase
根据PPE算PXE
PXEBase=(B0D86C200000>>12)+PTEBase
实际上,定位随机化页表基址有很种方法,各路神仙好像都有自己的套路,鹅厂的方法是通过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;}
实际上就是申请一块内存,在申请完了之后用修改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