放假期间看了一下Hyperplatform的VT框架,代码还是相当值得阅读学习的,在此分享下学习心得~,期间感谢@leeqwind大佬的悉心指导
源码戳这里:
DdiMon 0x01 前言
现在绝大多数CPU都已经支持虚拟化,目前这部分技术运用于VMWare,VirtualBox,ifyou大佬专注的Hyper-V... 还有除这些产品之外的 漏斗挖掘,游戏保护以及核晶防护(^_^ 是啥我也说不清楚,总之很牛逼)。跟进程隔离机制类似,虚拟化技术将进程隔离的概念扩大到了操作系统,即若干个隔离的操作系统互不影响。而其中的工作原理想必大家也都有所了解,LZ对这方面的概念也不是很清楚,所以只是简单介绍下这个概念。即由VMM管理着所有VM的资源请求,VM即我们所说的客户机,当客户机需要寻址某个页面内存时,实际上除了Windows内核的页表寻址机制之外,还需要经过一个页面表的转换来使得每个客户机操作系统访问不同的物理内存,从而达到隔离的目的。那么这里就引出了新页表,它被称之为扩展页表(Extend Page Table)。当然这只是一方面,虚拟化处理的事情远远大于这个机制。VMM使用VMXON区域对一些数据进行记录和维护,而每个VM也需要有自己对应的VMCS区域,一个VM发生异常通过vmexit指令来到VMM,VMM通过该VM的VMCS区域了解它到底是发生了什么异常以决定是否对他进行处理。入门级有个这样的概念就差不多了,剩下的慢慢消化《处理器虚拟化技术》就行了
0x02 虚拟化处理器核心
虚拟化是CPU的硬件支持,因此多核情况下只能对每个核心进行虚拟化处理。这里主要看 VmInitialization ---> VmpStartVm ---> AsmInitializeVm + VmpInitializeVm这块逻辑。主要要提及的是这里用到了汇编
AsmInitializeVm
去调用
VmpInitializeVm 派发函数。这个是因为驱动在DriverEntry入口点调用的处理器虚拟化函数,当为每一个CPU准备好VM结构后会调用到 vmlaunch指令,这个指令会直接call入Guest机器环境中,如果不明确给Guest确定他的代码入口点为DriverEntry的后半段以及栈区是后半段的栈区 的话那么驱动就无法完成后续的执行工作,系统就会进入HANG的状态。
检测CPU的虚拟化支持以及CPU对EPT的支持这里就不再做仔细介绍了,详细的大家翻阅Inter手册会比我说的好~
这里需要详细说明的是EPT的构造这块逻辑...... 首先明确下一个事情,就是Hyperplatform中的虚拟化处理的是整个内核空间的,架设的VM也就有一台,而这台VM的内核空间也就是主机的内核空间,因此EPT的构造就需要达到这样一个目的:GUEST使用的GPA在被EPT转换后依然访问的是原始地址,也就是GPA==HPA。这样当CPU处理在GUEST模式下的时候访问nt!NtDeviceIoControlFile不至于发生数据紊乱而访问到win32k!NtUserBuildHwndList+0x17d。因此对于x64的四级页面表的构造函数EptInitialization的原理就在于此,就是把物理地址A对应的EPT项填为A的物理页面项。注意他调用的EptpConstructTables设计的比较好,可以递归构造每一级的页面表
紧接着就是堆栈的构建,这块堆栈就是GUEST与HOST交互时进行数据传输时使用的堆栈内存;在调用vmlaunch之前,需要初始化VMM用于管理VM的VMCS域,(必须调用完__vmx_vmptrld设置VMCS之后才可以用vmwrite写vmcs域)
其实很多VMCS域的填充就是当前系统下一些寄存器的值,但是有几个域还是非常重要的:
VmcsField::kExceptionBitmap域,决定了VM_EXIT时可以捕获哪些异常
VmcsField::kGuestRsp域,决定了GUEST模式下执行的堆栈地址
VmcsField::kGuestRip域,决定了GUEST模式下执行的入口点地址
VmcsField::kGuestSysenterEsp
域,决定了
GUEST模式下快速系统调用的堆栈地址
VmcsField::kGuestSysenterEip
域,决定了GUEST模式下快速系统调用入口点地址
VmcsField::kHostRsp域,决定了GUEST退出到VMM时的堆栈地址
VmcsField::kHostRip域,
决定了GUEST退出到VMM时的入口地址
error |= UtilVmWrite(VmcsField::kExceptionBitmap, exception_bitmap);
error |= UtilVmWrite(VmcsField::kGuestRsp, guest_stack_pointer);
error |= UtilVmWrite(VmcsField::kGuestRip, guest_instruction_pointer);
error |= UtilVmWrite(VmcsField::kGuestSysenterEsp, UtilReadMsr(Msr::kIa32SysenterEsp));
error |= UtilVmWrite(VmcsField::kGuestSysenterEip, UtilReadMsr(Msr::kIa32SysenterEip));
error |= UtilVmWrite(VmcsField::kHostRsp, vmm_stack_pointer);
error |= UtilVmWrite(VmcsField::kHostRip, reinterpret_cast<ULONG_PTR>(AsmVmmEntryPoint));
0x03 利用VT绕过PG执行Hook
1. 为目标函数创建两个页面(如果有重合的页面,那么复用同一个),一个具有只可执行属性 shadow_exec;另外一个可读可写可执行 shadow_rw;并为每一个Hook点构建Trampline函数 【Original Code Bytes】+ 【jmp Target+offset】; 并将
shadow_exec页面中函数所在偏移位置写入一个字节 0xCC。然后调用vmcall指令退回VMM状态(只有在该状态下才可以修改EPT),将Hook函数所在页面的EPT项替换为
shadow_exec (含有INT 3指令的不可读写页面);那么这样下次如果函数被调用时就会触发#BP,相应的VM_EXIT退回
VMM 状态时就可以修改GuestRip,替换到Fake_xxx;紧接着Fake_xxx再调用下原函数就完成了第一步。
2. 那么既然现在函数所在的物理页属性已经被修改为不可读写状态(虽然被Patch成了INT 3),但是PatchGuard检测时就会触发EptVolation,VM_EXIT到VMM状态,那么这个时候将函数所在的EPT项替换为shadow_rw页面(可读可写可执行,代码没有被Patch过)就可以巧妙的躲过PG的检测;但是问题来了,那么这样的话后面Hook点就失效了~
3. 所以在替换成
shadow_rw的物理页的同时,还设置了VmcsField::kCpuBasedVmExecControl域里面的一个标志位叫做 MTF(Monitor Trap Flag),这个标志位的作用是单步执行完一条执行之后触发一次VM_EXIT(reason:MonitorTrapFlag) ,那么在这次VMM执行机会中就可以再而将Hook点所在的 shadow_rw 页面替换为
shadow_exec 所在物理页,完成偷梁换柱的操作;从此Hook点将保持下去。
0x04 存在的问题
虚拟化的强大之处在于他可以接管Guest内部的几乎所有的内部机制,那么这样的话就存在效率的问题,同样利用VT绕过PG检测时不断的来回替换物理页也是很消耗时间的一个方法;其次,目前Hyperplatform这个VT框架目前只能够以独占方式运行,如果存在多种这样的模块,那么只能是第一个生效,后续的都不能够再次虚拟化处理器核心;当然可能还有一些其他方面的问题,本文只讨论到这里~
如果有说的不到位的地方,还请大佬们指出~
现在绝大多数CPU都已经支持虚拟化,目前这部分技术运用于VMWare,VirtualBox,ifyou大佬专注的Hyper-V... 还有除这些产品之外的 漏斗挖掘,游戏保护以及核晶防护(^_^ 是啥我也说不清楚,总之很牛逼)。跟进程隔离机制类似,虚拟化技术将进程隔离的概念扩大到了操作系统,即若干个隔离的操作系统互不影响。而其中的工作原理想必大家也都有所了解,LZ对这方面的概念也不是很清楚,所以只是简单介绍下这个概念。即由VMM管理着所有VM的资源请求,VM即我们所说的客户机,当客户机需要寻址某个页面内存时,实际上除了Windows内核的页表寻址机制之外,还需要经过一个页面表的转换来使得每个客户机操作系统访问不同的物理内存,从而达到隔离的目的。那么这里就引出了新页表,它被称之为扩展页表(Extend Page Table)。当然这只是一方面,虚拟化处理的事情远远大于这个机制。VMM使用VMXON区域对一些数据进行记录和维护,而每个VM也需要有自己对应的VMCS区域,一个VM发生异常通过vmexit指令来到VMM,VMM通过该VM的VMCS区域了解它到底是发生了什么异常以决定是否对他进行处理。入门级有个这样的概念就差不多了,剩下的慢慢消化《处理器虚拟化技术》就行了
0x02 虚拟化处理器核心
虚拟化是CPU的硬件支持,因此多核情况下只能对每个核心进行虚拟化处理。这里主要看 VmInitialization ---> VmpStartVm ---> AsmInitializeVm + VmpInitializeVm这块逻辑。主要要提及的是这里用到了汇编
AsmInitializeVm
去调用
VmpInitializeVm 派发函数。这个是因为驱动在DriverEntry入口点调用的处理器虚拟化函数,当为每一个CPU准备好VM结构后会调用到 vmlaunch指令,这个指令会直接call入Guest机器环境中,如果不明确给Guest确定他的代码入口点为DriverEntry的后半段以及栈区是后半段的栈区 的话那么驱动就无法完成后续的执行工作,系统就会进入HANG的状态。
检测CPU的虚拟化支持以及CPU对EPT的支持这里就不再做仔细介绍了,详细的大家翻阅Inter手册会比我说的好~
这里需要详细说明的是EPT的构造这块逻辑...... 首先明确下一个事情,就是Hyperplatform中的虚拟化处理的是整个内核空间的,架设的VM也就有一台,而这台VM的内核空间也就是主机的内核空间,因此EPT的构造就需要达到这样一个目的:GUEST使用的GPA在被EPT转换后依然访问的是原始地址,也就是GPA==HPA。这样当CPU处理在GUEST模式下的时候访问nt!NtDeviceIoControlFile不至于发生数据紊乱而访问到win32k!NtUserBuildHwndList+0x17d。因此对于x64的四级页面表的构造函数EptInitialization的原理就在于此,就是把物理地址A对应的EPT项填为A的物理页面项。注意他调用的EptpConstructTables设计的比较好,可以递归构造每一级的页面表
紧接着就是堆栈的构建,这块堆栈就是GUEST与HOST交互时进行数据传输时使用的堆栈内存;在调用vmlaunch之前,需要初始化VMM用于管理VM的VMCS域,(必须调用完__vmx_vmptrld设置VMCS之后才可以用vmwrite写vmcs域)
其实很多VMCS域的填充就是当前系统下一些寄存器的值,但是有几个域还是非常重要的:
VmcsField::kExceptionBitmap域,决定了VM_EXIT时可以捕获哪些异常
VmcsField::kGuestRsp域,决定了GUEST模式下执行的堆栈地址
VmcsField::kGuestRip域,决定了GUEST模式下执行的入口点地址
VmcsField::kGuestSysenterEsp
域,决定了
GUEST模式下快速系统调用的堆栈地址
VmcsField::kGuestSysenterEip
域,决定了GUEST模式下快速系统调用入口点地址
VmcsField::kHostRsp域,决定了GUEST退出到VMM时的堆栈地址
VmcsField::kHostRip域,
决定了GUEST退出到VMM时的入口地址
error |= UtilVmWrite(VmcsField::kExceptionBitmap, exception_bitmap);
error |= UtilVmWrite(VmcsField::kGuestRsp, guest_stack_pointer);
error |= UtilVmWrite(VmcsField::kGuestRip, guest_instruction_pointer);
error |= UtilVmWrite(VmcsField::kGuestSysenterEsp, UtilReadMsr(Msr::kIa32SysenterEsp));
error |= UtilVmWrite(VmcsField::kGuestSysenterEip, UtilReadMsr(Msr::kIa32SysenterEip));
error |= UtilVmWrite(VmcsField::kHostRsp, vmm_stack_pointer);
error |= UtilVmWrite(VmcsField::kHostRip, reinterpret_cast<ULONG_PTR>(AsmVmmEntryPoint));
0x03 利用VT绕过PG执行Hook
虚拟化是CPU的硬件支持,因此多核情况下只能对每个核心进行虚拟化处理。这里主要看 VmInitialization ---> VmpStartVm ---> AsmInitializeVm + VmpInitializeVm这块逻辑。主要要提及的是这里用到了汇编
AsmInitializeVm
去调用
VmpInitializeVm 派发函数。这个是因为驱动在DriverEntry入口点调用的处理器虚拟化函数,当为每一个CPU准备好VM结构后会调用到 vmlaunch指令,这个指令会直接call入Guest机器环境中,如果不明确给Guest确定他的代码入口点为DriverEntry的后半段以及栈区是后半段的栈区 的话那么驱动就无法完成后续的执行工作,系统就会进入HANG的状态。
检测CPU的虚拟化支持以及CPU对EPT的支持这里就不再做仔细介绍了,详细的大家翻阅Inter手册会比我说的好~
这里需要详细说明的是EPT的构造这块逻辑...... 首先明确下一个事情,就是Hyperplatform中的虚拟化处理的是整个内核空间的,架设的VM也就有一台,而这台VM的内核空间也就是主机的内核空间,因此EPT的构造就需要达到这样一个目的:GUEST使用的GPA在被EPT转换后依然访问的是原始地址,也就是GPA==HPA。这样当CPU处理在GUEST模式下的时候访问nt!NtDeviceIoControlFile不至于发生数据紊乱而访问到win32k!NtUserBuildHwndList+0x17d。因此对于x64的四级页面表的构造函数EptInitialization的原理就在于此,就是把物理地址A对应的EPT项填为A的物理页面项。注意他调用的EptpConstructTables设计的比较好,可以递归构造每一级的页面表
紧接着就是堆栈的构建,这块堆栈就是GUEST与HOST交互时进行数据传输时使用的堆栈内存;在调用vmlaunch之前,需要初始化VMM用于管理VM的VMCS域,(必须调用完__vmx_vmptrld设置VMCS之后才可以用vmwrite写vmcs域)
其实很多VMCS域的填充就是当前系统下一些寄存器的值,但是有几个域还是非常重要的:
VmcsField::kExceptionBitmap域,决定了VM_EXIT时可以捕获哪些异常
VmcsField::kGuestRsp域,决定了GUEST模式下执行的堆栈地址
VmcsField::kGuestRip域,决定了GUEST模式下执行的入口点地址
VmcsField::kGuestSysenterEsp
域,决定了
GUEST模式下快速系统调用的堆栈地址
VmcsField::kGuestSysenterEip
域,决定了GUEST模式下快速系统调用入口点地址
VmcsField::kHostRsp域,决定了GUEST退出到VMM时的堆栈地址
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2019-1-2 14:11
被FaEry编辑
,原因: