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

在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 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

算出来的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

易语言辅助必备驱动保护模块 代码公开 透明 绝无暗装之类 ------------------------------ .版本 2 .子程序 关闭保护辅助进程, 逻辑型, 公开, 取消禁止结束并保护程序 .参数 进程ID, 整数型, 可空, 可空,默认取消自身,可用的进程_名取ID()获取进程ID, .子程序 关闭防各类调试, 逻辑型, 公开, 取消结束并保护程序 .参数 进程ID, 整数型, 可空, 可空,默认取消自身,可用的进程_名取ID()获取进程ID, .子程序 开启保护辅助进程, 逻辑型, 公开, 可禁止他人有意结束某程序,并保护程序不被注入,打开程序,支持所有系统,32,WIN764位都可以 .参数 进程ID, 整数型, 可空, 可空,默认保护自身,可用的进程_名取ID()获取进程ID, .子程序 开启防各类调试, 逻辑型, 公开, 可禁止他人有意,用CE,VE,ME,GE,内存工具和WPE等程序,打开程序,支持所有系统,32,WIN764位都可以 .参数 进程ID, 整数型, 可空, 可空,默认保护自身,可用的进程_名取ID()获取进程ID, .子程序 隐藏模块, 逻辑型, 公开, 隐藏模块 (GetModuleHandle (“隐藏.dll”)) .参数 模块基地址, 整数型 .子程序 郁金香取消隐藏进程, 逻辑型, 公开, 取消隐藏进程 暂时无法取消隐藏 .参数 进程ID, 整数型, 可空, 可空,默认取消自身,可用进程_名取ID()获取进程ID, .子程序 郁金香隐藏进程, 逻辑型, 公开, 隐藏进程,,支持32位,和64位WIn7,与所有系统请自行测试 .参数 进程ID, 整数型, 可空, 可空,默认隐藏自身,可用进程_名取ID()获取进程ID, .子程序 置格盘陷阱, 逻辑型, 公开 .子程序 置蓝屏陷阱, 逻辑型, 公开, 利用蓝屏代码 绝对值蓝屏 .子程序 置死机陷阱, 逻辑型, 公开 .子程序 置重启陷阱, 逻辑型, 公开, 绝对值重启 .DLL命令 GetModuleHandle, 整数型, "kernel32", "GetModuleHandleA", 公开 .参数 lpModuleName, 文本型 .DLL命令 RtlMoveMemory, 整数型, , "RtlMoveMemory", 公开, _写内存3 .参数 dest, 整数型, 传址 .参数 Source, 整数型 .参数 len, 整数型, , 4
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鬼手56

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值