这篇博客将记录通过hvpp框架对intelVT学习的过程
如果对VT技术一点了解都没有可以先看看B站上周壑老师的VT入门视频, 感觉讲的还是很好的, 至少让我听懂了个大概(
VT的基本思想
实际上和之前写过的Debug-Block非常相像, VT启动后提供一个虚拟CPU来执行后续的所有指令 这个环境下触发任何异常都会检查VMCS中相应异常的vmexit使能位, 如果使能位激活那么VT驱动会接管这个异常进行处理从而实现全面监控客机的行为.
hvpp项目学习
目前项目源码中的src/hvpp/hvpp/vmexit/vmexit_passthrough.cpp
有明显语法错误 L29处的find_if
圆括号未闭合:
其次当前VS2022自带的C++编译工具链(V143)会在std::find
处报外部符号解析错误, 如果不想长考可以将skip_prefixes
直接替换为以下代码:
1 | static const uint8_t* skip_prefixes(const uint8_t* first, const uint8_t* last) noexcept |
项目结构
hvpp-lib
中就是VT框架代码, 下面用到的部分会进行解读.
hvppctrl
的编译目标是可执行文件, 用于对VT框架进行功能测试以及VT框架使用示例.
hvppdrv
的编译目标是驱动, 作为VT框架的载体可以用于和ring3程序进行通信以及实现用户自定义vmexit handler
hvppdrv_c
为hvppdrv的C版本
后续将会基于hvppctrl
与VT框架的互动来对hvpp的VT框架进行学习, 以应用加深对VT的理解
hvppctrl
一共进行了3个测试:
1 | int main() |
下面一个个进行解读
TestCpuid
1 | void TestCpuid() |
cpuid
作为一条硬件信息获取指令, VCPU在执行时会触发vmexit, 接下来看看VT框架的反应:
1 | void vmexit_custom_handler::handle_execute_cpuid(vcpu_t& vp) noexcept |
当执行cpuid
且客机eax值为’ppvh’时, 驱动会将客机的几个寄存器设定为预设值, 这几个寄存器对应的就是CpuInfo
, 也就是客机的cpuid
指令返回的信息可以被VT驱动修改为任意值, 凭这点可以拦截软件对硬件信息的访问以绕过某些限制.
另外从这个handler可以看出, hvpp框架不需要用户实现每一种vmexit的对应handler, vmexit_custom_handler
扩展自vmexit_passthrough_handler
, vmexit_passthrough.cpp
中实现了各种vmexit相当于无事发生的handler供使用, 如果用户需要自定义vmexit handler只需要在vmexit_custom.h
中编辑vmexit_custom_handler
对默认handler进行重载即可:
更进一步地, hvpp框架实现了vmexit handler执行结束后到vmresume前的所有善后措施, 包括调整客机RIP寄存器, 若不进行调整就会无限执行会触发vmexit的指令.
TestHook
该测试展示了VT对内存的虚拟化的运用, 也就是EPThook.
首先函数获取了待hook函数ZwClose
的虚拟地址并获取了其所在的页(4kb)的起始地址为后续备份hook前的页做准备.
完成备份后使用detours
库进行inline hook, 也就是修改函数头为jmp
指令以执行用户代码, 此时读取ZwClose
处的内存会发现已经被修改, 然后程序进一步进行了hide操作(通过lambda表达式实现):
1 | auto Hide = [](void* PageRead, void* PageExecute) |
其中Hide
第一个参数指向客机读到的页, 第二个参数指向实际执行的页, 然后带着这两个参数执行了vmcall
.
vmcall
指令是guest主动和host通信最常用的方式, 会直接触发vmexit, 来看看host的对应处理:
1 | void vmexit_custom_handler::handle_execute_vmcall(vcpu_t& vp) noexcept |
guest呼叫host时的第一个参数为0xC1时, host先是计算了两个页的物理地址, 然后进行了对性能开销很重要的一步 – 重建guest的EPT结构(对于EPT可以参考科普下VT EPT).
虽然EPT会根据GPA进行4级页表直到检索到最小粒度4kb, 总共覆盖了512 * 512 * 512 * 512 * 4kb内存, 但是实际上因为每次guest涉及到内存的操作都需要计算两次转化(linear addr -> GPA -> HPA), 设计EPT结构时出于性能考量会减少一级页表, 但是总共能够覆盖的范围实际不会变, 也就是512 * 512 * 512 * 2mb, 此时EPT的粒度为2mb, 下称大页.
而VT框架中执行的split_2mb_to_4kb()
为待EPThook的大页新建了一张映射表, 通过512项的指针指向这个大页中的每一个页(2mb / 512 = 4kb), 后续使用map_4kb
来建立只可执行页从GPA到HPA的映射, 当任何guest试图访问ZwClose
所在的页都会触发ept violation
.
随后执行的invept_single_context
用于清除掉先前CPU缓存的EPT地址防止不经过运算直接从缓存中得到了旧的映射.
接下来看看host对ept violation
的处理:
1 | void vmexit_custom_handler::handle_ept_violation(vcpu_t& vp) noexcept |
可以看到当退出理由为试图读写只可执行的页时会通过map_4kb
来重建GPA到HPA的映射, 实际读到和写到的是备份的内存, 最后很重要的一步是执行suppress_rip_adjust
, 上面说过hvpp框架实现了自动调整guest的RIP的功能, 执行这个函数就是告诉框架不需要调整, 因为实际指令还没有执行, 还需要guest再执行一遍指令读到host重建映射后的虚假页.
至此简单以例子说明了EPThook技术的原理和实现