KVM 基础知识
kvm是一个内核模块,它实现了一个/dev/kvm的字符设备来与用户进行交互,通过调用一系列ioctl函数可以实现qemu和kvm之间的切换。
KVM结构体
KVM结构体在 KVM的系统架构中代表一个具体的虚拟机,当通过 VM_CREATE_KVM 指令创建一个新的 KVM结构体对象。
struct kvm结构体如下:
1
|
struct kvm {
|
KVM结构体对象包含了 vCPU、内存、APIC、IRQ、MMU、Event时间管理等信息,该结构体中的信息主要在 KVM虚拟机内部使用,用于跟踪虚拟机的状态。
在 KVM中,连接了如下几个重要的结构体成员,他们对虚拟机的运行有重要作用。
-
struct kvm_memslots *memslots;KVM虚拟机所分配到的内存slot,以数组形式存储这些slot的地址信息。
由于客户机物理地址不能直接用于宿主机物理MMU进行寻址,所以需要把客户机物理地址转换成宿主机虚拟地址(Host Virtual Address, HVA),为此,KVM用一个kvm_memory_slot数据结构来记录每一个地址区间的映射关系,此数据结构包含了对应此映射区间的起始客户机页帧号(Guest Frame Number, GFN),映射的内存页数目以及起始宿主机虚拟地址。于是KVM就可以实现对客户机物理地址到宿主机虚拟地址之间的转换,也即首先根据客户机物理地址找到对应的映射区间,然后根据此客户机物理地址在此映射区间的偏移量就可以得到其对应的宿主机虚拟地址。进而再通过宿主机的页表也可实现客户机物理地址到宿主机物理地址之间的转换,也即 GPA 到 HPA 的转换。 -
struct kvm_vcpu *vcpus[KVM_MAX_VCPUS];KVM虚拟机中包含的vCPU结构体,一个虚拟机CPU对应一个vCPU结构体。 -
struct kvm_io_bus *buses[KVM_NR_BUSES];KVM虚拟机中的I/O总线,一条总线对应一个kvm_io_bus结构体,如ISA总线、PCI总线。 -
struct kvm_vm_stat stat;KVM虚拟机中的页表、MMU等运行时的状态信息。 -
struct kvm_arch arch;KVM的软件arch方面所需要的一些参数。
KVM初始化过程
1
|
// 获取 kvm 句柄
|
源码分析
参考网上有的 Hitcon 2018 abyss 题目的源码,整体分析一下这类题目的大致逻辑。
整个题目由3个binary,hypervisor.elf、kernel.bin与user.elf组成:
hypervisor.elf是一个利用KVM API来做虚拟化的程序,它会加载一个小型的内核kernel.bin,这个kernel就只实现了内存管理和中断处理的功能,提供了loader启动和libc加载需要的一些常见syscall,然后解析ELF启动一个用户态程序。这里直接加载ld.so.2来装载用户态程序user.elf。
user.elf就是一个标准的x86-64 ELF文件,也可以直接在host上启动。kernel.bin在处理syscall时,将一些与IO有关的例如read/write等通过 I/O Port (CPU的in/out指令) 交给hypervisor来处理。例如open这个syscall,kernel在做检查之后,直接通过hypercall传给hypervisor处理,然后hypervisor会在host上打开一个文件,并将其fd做一个映射返回给kernel. 所以实际上VM内做的open是可以打开host的文件的。
hypervisor
首先是 main函数,主要重点有 kvm_init、copy_argv和 execute函数
1
|
int main(int argc, char *argv[]) {
|
kvm_init
kvm_init函数的整体逻辑和上面说的 KVM初始过程差不多,主要实现了初始化和创建 kvm,创建了KVM内存和CPU, 然后拷贝了用户代码。
1
|
VM* kvm_init(uint8_t code[], size_t len) {
|
setup_regs
主要设置了 KVM运行时的寄存器,包括代码运行点,内存大小等
1
|
/* set rip = entry point
|
设置寄存器的值时,有一个很重要的点是我们要关注的,即是否设置了 ERREF寄存器,如果这个寄存器的值为 0x800(1<<11),那么意味着 hypervisor开了 NEX即数据执行保护,这对于我们后续的 EXP的编写影响很大。
此处我们可以看到未设置 ERREF寄存器,也就是未 开启 NEX,我们后续可以直接执行 shellcode.
setup_long_mode
主要是设置了段页的各项属性,包括pml4、pdp、pd、cr3等指定页表映射等关系的内存和寄存器。这一块页表映射还有点不太懂。
1
|
/* Maps:
|
copy_argv
copy_argv函数将一些参数拷贝到内核栈上
1
|
/* copy argv onto kernel's stack */
|
execute
最后会调用execute函数,我们可以看到开始循环运行KVM虚拟机,如果发生了中断会进入中断处理流程。其中我们重点关注 KVM_EXIT_IO,该流程会根据 io.port去调用 hp_handler来处理,如果处理失败才会退出虚拟机。
1
|
void __attribute__((noreturn)) execute(VM* vm) {
|
hp_handle
hp_handle定义了hypervisor接受内核发出的 IO中断时的处理函数,可以看到主要处理了 open、read、 write、 lseek、 fclose、 fstat、 exit、 acces、 ioctl、 panic等函数。而其中 ioctl函数为对参数做检查,可以在host上以任意参数来调用一个ioctl函数。
1
|
int hp_handler(uint16_t nr, VM* vm) { |