kvm + guest kernel
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) { |