intel:x86架构VT虚拟化(四):x64 无痕hook/shadow walker/页面读写分离
前面费老大劲学习VT的基本原理和框架代码,到底能用来干啥了?
VT中,host通过exit事件监控guest的一举一动,稍微“大”一点的动作(进程切换、读写msr、执行cpuid等)都会在guest触发exit,回到host的handle函数处理,在VT框架中,host对guest有绝对的监控和处理的全力,所以业界通常把VT框架下的程序称为-1环,比操作系统的0环都低,很形象地说明了host的权限范围;VT中非常重要的一个模块EPT,gtest中任何读写实际物理内存的操作都需要通过EPT转换一遍,这个转换环节一旦出任何问题,导致转换到了错误的物理地址,都会导致guest读写物理内存失败;本文利用这个原理,让guest对host同一块物理地址的读和写分离:第三方程序(比如CE、PChunter、某些程序自带的CRC检测功能、windows自带的patch guard等)读物理页的时候返回一个结果,执行的时候又返回一个结果,以此骗过第三方程序对物理页内容的检查,这就是业界俗称的shadow walker。此技术可用于无痕hook!
1、本次拿IDT的0x0E号中断page fault举例:熟悉操作系统的人都不陌生,操作系统的缺页异常无时不在,换句话说这个函数无时无刻都在被调用。现在通过PChunter能查到函数的入口地址:
windbg也能正常看到函数的入口代码:
这个虚拟地址对应的物理地址:0x220bb40;考虑到页对齐,物理页首地址应该是0x220b000;
正常情况下,页面可执行必然是可读的,但是现在把这里设置一下,可以让页面可执行,但是不可读;
我这里0x48c寄存器最后1位是1,说明支持这种方式:可执行但不可读
2、这里回顾一下EPT的原理:虚拟机的GPA要转成HPA,必须经过如下各级页表的转换,下面是各层级的归纳总结:
(1)绿色的PTE:一个entry占用8byte,可以映射到一个物理页;一个PTE有4096byte,能容纳512个entry,也就能管理512*4K=2M的内存(一个绿块占用4KB,最大能管理2MB内存)
(2)橙色的PDE:一个entry占用8byte,可以映射到一个PTE;一个PDE有4096byte,能容纳512个entry,也就能管理512个PTE,那么一个PDE能管理512*2M=1GB的内存(一个橙块占用4KB,最大能管理1GB内存)
(3)红色PDPTE:一个entry占用8byte,可以映射到一个PDE;一个PDPTE有4096byte,能容纳512个entry,也就能管理512个PDE,那么一个PDPTE能管理512*1GB=512GB的内存(一个红块占用4KB,最大能管理512GB内存)
(4)蓝色PML4E:同上,一个PML4E管理512个PDPTE,512个PML4E一共能管理512*512GB=256T内存(一个蓝块占用4KB,最大能管理256T内存)
本人测试的虚拟机内存2GB为例,按照上面的推算方式,申请内存时需要蓝色小块1个页,红色小块1个页,橙色小块2个页,绿色小块1024个页;
代码如下,注意核心都在注释了:
EptPteEntry* g_fake_page; ULONG64 g_fake_page_pa; EptPteEntry* fake_PteEntry; EptPml4Entry* EptInitialization() { EptPml4Entry* ept_PML4T; PHYSICAL_ADDRESS FirstPtePA, FirstPdePA, FirstPdptePA; ept_PML4T = (EptPml4Entry*)(ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, 'aa'));//蓝 RtlZeroMemory(ept_PML4T, PAGE_SIZE); EptPdpteEntry* ept_PDPTE = (EptPdpteEntry*)(ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, 'aa'));//红 RtlZeroMemory(ept_PDPTE, PAGE_SIZE); FirstPdptePA = MmGetPhysicalAddress(ept_PDPTE); ept_PML4T->Read = 1; ept_PML4T->Write = 1; ept_PML4T->Execute = 1; ept_PML4T->PhysAddr = FirstPdptePA.QuadPart >> 12; g_pPml4T = ept_PML4T; g_pPdpteTable = ept_PDPTE; //生成一个假页面 g_fake_page = (EptPteEntry* )ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE,'fake'); RtlZeroMemory(g_fake_page, PAGE_SIZE); g_fake_page_pa = MmGetPhysicalAddress(g_fake_page).QuadPart; for (ULONG64 a = 0;a < NUM_PAGES;a++)//红,循环一次管理1GB;本人虚拟机2GB内存,所以循环2次 { EptPdeEntry* ept_PDE = (EptPdeEntry*)(ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, 'aa')); // 普通模式 RtlZeroMemory(ept_PDE, PAGE_SIZE); FirstPdePA = MmGetPhysicalAddress(ept_PDE); ept_PDPTE->Read = 1; ept_PDPTE->Write = 1; ept_PDPTE->Execute = 1; ept_PDPTE->PhysAddr = FirstPdePA.QuadPart >> 12; ept_PDPTE++; g_pPdeTable[a] = ept_PDE; for (int b = 0;b < 512;b++)//橙,循环一次管理2MB,所有循环完成管理1GB { EptPteEntry* ept_PTE = (EptPteEntry*)(ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, 'aa')); // 普通模式 g_pPteTable[a][b] = ept_PTE; RtlZeroMemory(ept_PTE, PAGE_SIZE); FirstPtePA = MmGetPhysicalAddress(ept_PTE); ept_PDE->PhysAddr = FirstPtePA.QuadPart >> 12; ept_PDE->Read = 1; ept_PDE->Write = 1; ept_PDE->Execute = 1; ept_PDE++; for (int c = 0;c < 512;c++)//绿,循环一次管理4KB;所有循环完成管理2MB { ept_PTE->PhysAddr = (a * (1 << 30) + b * (1 << 21) + c * (1 << 12)) >> 12; if(0x220b000 == (((a * (1 << 30) + b * (1 << 21) + c * (1 << 12))) & 0xffffffff)){ //Asm_int3(); //FGP_VT_KDPRINT(("ept_PTE->PhysAddr = 0x%x\n", *((PULONG64)ept_PTE->PhysAddr)));//这里会异常,因为这块内存末尾3byte都是0,读写执行都不允许 ept_PTE->Read = 0; //我们的目标页面,只能执行,不能读写;当CE、pchunter读这个页面时就会产生异常,进入exithandler处理 ept_PTE->Write = 0; ept_PTE->Execute = 1; ept_PTE->MemoryType = EPT_MEMORY_TYPE_WB;//write-back fake_PteEntry = ept_PTE;//导出pte项指针 } else //其他页面正常可读可写可执行 { ept_PTE->Read = 1; ept_PTE->Write = 1; ept_PTE->Execute = 1; ept_PTE->MemoryType = EPT_MEMORY_TYPE_WB; } ept_PTE++; } } } return ept_PML4T; }
对应的异常处理函数HandleEptViolation()中每次遇到读写都挂上假页面:
if (pEpt_Attribute->Read)// Read Access { //假页面给挂载上,同时允许可读可写 fake_PteEntry->PhysAddr = g_fake_page_pa>>12; fake_PteEntry->Read = 1; fake_PteEntry->Write = 1; fake_PteEntry->Execute = 0; } if (pEpt_Attribute->Write)// Write Access { //假页面给挂载上,同时允许可读可写 fake_PteEntry->PhysAddr = g_fake_page_pa >> 12; fake_PteEntry->Read = 1; fake_PteEntry->Write = 1; fake_PteEntry->Execute = 0; }
效果:连windbg都被骗了,这个页面读出来的全是0!
参考: