虚拟机环境检测

First Post:

Last Update:

Word Count:
3.2k

Read Time:
12 min

虚拟机检测技术及绕过方法

ref: https://km.woa.com/articles/show/471394?kmref=search&from_page=1&no=1

0x00 虚拟化技术概述

目前我们的操作系统大多都是基于x86架构,它设计为直接运行在硬件设备上的系统。内核可以直接访问硬件和内存,它使用特权指令、中断等完成需要Ring0级权限的操作,而Ring3层应用想执行一些高权限的操作如读写磁盘等就需要执行系统调用,从Ring3切换到Ring0,操作完成后再切换到Ring3。

由于宿主机系统运行在Ring0,所以虚拟出来的客户机系统就不能再运行在Ring0了,但客户机系统无法感知这一点,当执行到某些高权限操作时因为没有权限就会导致执行出错。CPU的机制使客户机操作系统执行特权指令时会触发异常,因此通过客户机系统访问硬件的实现原理不同,虚拟化技术又分为三种:全虚拟化、半虚拟化和硬件辅助的半虚拟化。

  • 全虚拟化:hypervisor[1]捕获异常,在异常处理中进行指令翻译和模拟,处理执行完成后再返回。这种方式实现的虚拟化可以最佳兼容客户机操作系统,但由于每一条指令都需要进行翻译,导致性能十分低下,常见的VMware Workstation、QEMU就是利用这种方式实现;
  • 半虚拟化:修改客户机操作系统,替换掉不能虚拟的指令,通过hypercall直接和底层虚拟化管理程序通信,相比全虚拟化大大提高了效率,像XEN就是这种半虚拟化的方式实现,由于这种方式要修改客户机操作系统,所以不能虚拟化Windows系统;
  • 硬件辅助的虚拟化:客户机操作系统运行在非root模式下,而hypervisor运行在root模式下,CPU在遇到特权指令时就转到root模式下执行,它需要在两种模式之间切换,会有一定的开销但性能接近半虚拟化。其特性决定它需要CPU支持虚拟化技术,常见的VMware ESXi、Microsoft Hyper-v和KVM等就是这种方式实现。

0x01 常见的虚拟机检测方法

目前检测虚拟机的方式主要可分为两个大类:基于特殊指令的检测和基于虚拟机特征的检测。

特殊指令

在x86体系架构中,某些获取硬件信息的指令执行时并不产生异常,如cpuid、sidt、sgdt及sldt等指令。为了避免全虚拟化带来的性能损失,VMware等虚拟机允许特定指令在没有正确虚拟化的前提下执行,这就导致这些指令在物理机和虚拟机下的执行结果不一致,利用这点可以检测程序是否在虚拟机中运行。

cpuid指令

cupid指令通常用来获取CPU信息,其隐式参数为EAX(或ECX)寄存器,该参数指示指令完成的功能(Intel最新术语称之为CPUID Leaf,以下称之为功能号),指令执行完成后返回结果存放在EAX、EBX、ECX和EDX四个寄存器中。

cpuid常用的功能号:

序号 功能号(EAX) 功能描述
1 0x00000000 返回cpu供应商签名信息,始终为GenuineIntel
2 0x00000001 返回cpu系列型号等特征信息
3 0x40000000-0x4FFFFFFF 无效功能号,一般用于其他厂商定制软件使用

检测虚拟机常用到的功能号就是0x01和0x40000000,更详细的功能号说明参考芯片手册:Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 2 (2A, 2B & 2C): Instruction Set Reference, A-Z

Intel和AMD保留cpuid功能号0x40000000~0x400000FF提供给hypervisor使用,以下是hypervisor的CPUID接口规范部分内容 (参考cpuid.h):

序号 功能号 返回值描述
1 0x01 ECX第31位指示当前hypervisor,虚拟cpu该值设置为1,物理cpu则设置为0
2 0x40000000 hypervisor支持的最大功能号和供应商签名信息
3 0x40000001 - 0x4000000F 特定功能号,含义取决于hypervisor支持的最大功能号
4 0x40000010 - 0x400000FF 通用功能号,保留给不同虚拟机厂商使用

常见供应商签名特征:

序号 虚拟机类型 签名信息
1 VMware “VMwareVMware”
2 KVM “KVMKVMKVM\0\0\0”
3 Microsoft Hyper-V “Microsoft Hv”
4 Xen “XenVMMXenVMM”
5 VirtualBox “VBoxVBoxVBox”

hypervisor检测cpuid功能号为0x01时ECX第31位是否为1:(电脑上打开了hyper-v或其他虚拟化软件,都会被检测出hypervisor)

1
2
3
4
5
6
7
8
9
10
11
12
1. #include <intrin.h>
2. bool cpuid_is_hypervisor()
3. {
4. int CPUInfo[4] = { -1 };
5.
6. /* Query hypervisor precense using CPUID (EAX=1), BIT 31 in ECX */
7. __cpuid(CPUInfo, 1);
8. if ((CPUInfo[2] >> 31) & 1)
9. return true;
10. else
11. return false;
12. }

获取hypervisor供应商签名信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
1. wstring cpuid_hypervisor_vendor()
2. {
3. int CPUInfo[4] = { -1 };
4. char szHypervisorVendor[0x40];
5. wstring vendor;
6.
7. __cpuid(CPUInfo, 0x40000000);
8. memset(szHypervisorVendor, 0, sizeof(szHypervisorVendor));
9. memcpy(szHypervisorVendor, CPUInfo + 1, 12);
10
11 vendor = ascii_to_wide_str(szHypervisorVendor);
12 return vendor;
13 }
in指令

in指令检测主要针对VMware,VMware通过查询特定的IO端口(0x5658)判断软件是否运行在虚拟机中。 在端口0x5658上执行IN指令并传入以下参数

1
2
3
4
EAX = 0x564D5868 (VMware hypervisor魔术值VMXh)
EBX = 0xFFFFFFFF (UINT_MAX)
ECX = 0xA (0xA:获取VMWare版本命令, 0x14:获取内存大小)
EDX = 0x5658 (hypervisor端口号)

VMware执行完该指令后会修改EBX寄存器的值为0x564D5868,通过判断该数值可检测是否为虚拟机。

检测代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
1. bool vmware_io()
2. {
3. bool bFound = true;
4. __try
5. {
6. __asm
7. {
8. push edx
9. push ecx
10 push ebx
11 mov eax, 'VMXh'
12 mov ebx, 0
13 mov ecx, 10
14 mov edx, 'VX'
15 in eax, dx
16 cmp ebx, 'VMXh'
17 setz[bFound]
18 pop ebx
19 pop ecx
20 pop edx
21 }
22 }
23 __except (EXCEPTION_EXECUTE_HANDLER)
24 {
25 bFound = false;
26 }
27 return bFound;
28 }
sidt、sgdt和sldt指令

在保护模式下有3个很重要的系统表:全局描述符表GDT、局部描述符表LDT和中断描述符表IDT,这些表包含段描述符,它描述一个段的大小、地址及权限等各种信息。 sidt、sgdt和sldt指令能读取这些表并存入特定的地址中,可以根据物理机和虚拟机中这些表地址的差异性来区分当前环境为实体机还是虚拟机。

通常物理机中LDT为0,IDT为0x80xxxxxx,而虚拟机中LDT不为0,IDT为0xFFxxxxxx。

检测LDT:

1
2
3
4
5
6
7
8
9
1. bool ldt_check()
2. {
3. unsigned short ldt_addr = 0;
4. unsigned char ldtr[2];
5.
6. _asm sldt ldtr
7. ldt_addr = *((unsigned short *)&ldtr);
8. return ldt_addr != 0x00000000;
9. }

检测GDT:

1
2
3
4
5
6
7
8
9
1. bool gdt_check()
2. {
3. unsigned int gdt_addr = 0;
4. unsigned char gdtr[6];
5.
6. _asm sgdt gdtr
7. gdt_addr = *((unsigned int *)&gdtr[2]);
8. return (gdt_addr >> 24) == 0xff;
9. }

不过VMware经过这么多的版本更迭早已对这些地方打了补丁,低版本的虚拟机还能生效,高版本的都把这些表补丁模拟成真机地址了。

硬件信息、注册表、服务等其他特征

虚拟机和物理机的硬件存在差异,物理机更多样化,而虚拟机往往呈现一定的规律性。主机型号、磁盘名称、MAC地址及注册表等等软硬件通常包含比较明显的特征,如虚拟机的MAC地址的前面几位通常表示网络适配器的供应商,通过检索网卡MAC地址可以判断是否为虚拟机。

常见MAC前缀对应的供应商:

序号 MAC前缀 供应商
1 08:00:27 VirtualBox
2 52:54:00 QEMU
3 00:16:3E Xen
4 00:50:56 VMware
5 00:05:69 VMware
6 00:0C:29 VMware
7 00:1C:14 VMware

MAC地址、注册表、服务名等信息可以通过多种方式查询到,如Windows API、WMI及SystemFirmwareTable等方式。(篇幅原因就不展开赘述了,更详细的特征及检测方法可参考github开源项目:https://github.com/LordNoteworthy/al-khaser)

常用的绕过方式:

序号 检测方法 绕过方式
VMware KVM
1 cupid 修改vmx配置cpuid.v0 源码patch
2 in 修改vmx配置backdoor -
3 sldt 关闭VMware加速 -
4 sidt、sgdt和str等 指令Nop/动态patch -
5 硬件信息 驱动过滤 源码patch

(具体操作参考下文绕过VMP反虚拟机的部分)

第二部分 几种常见保护软件反虚拟机检测及绕过方法

本节主要分析常见如VMProtect、Themida及Enigma等保护软件反虚拟机检测及绕过方法。

目前市面上主流的几款保护软件中VMProtect对虚拟机的检测强度最大,尤其对于VMware。

VMProtect

以下表格是VMP各版本主要的检测方法及绕过方式。

序号 版本 检测方式 绕过方式
VMware KVM
1 2.0~3.0 cpuid 配置hypervisor.cpuid.v0 = “FALSE” 源码patch
in 配置monitor_control.restrict_backdoor = “TRUE” 源码patch
2 3.0+ cupid+in+VMware特征 修改vmx配置+驱动 -
  • VMP 2.0到3.0版本:
    • 主要通过cpuid和in指令来检测虚拟机,对于VMware workstation,修改虚拟.vmx配置文件,向其中添加以下两条配置即可绕过检测:
      monitor_control.restrict_backdoor = "TRUE" // 过掉in指令检测
      hypervisor.cpuid.v0 = "FALSE" // 过掉cpuid检测
  • VMP 3.0+版本:
    • 在前面版本的基础上增加了对硬件信息的检测,配合驱动VmwareHardenedLoader可过掉对VMware的检测
    • 该驱动通过hook tSystemFirmwareTable动态修改虚拟机的硬件信息,过滤掉BIOS、电源等设备中包含VMware和Virtual的特征
  • 对于KVM虚拟机:
    • 通过对QEMU源码进行Patch,修改其中cpuid指令的返回值以及KVM相关的特征可以VMP2.0以上的反检测
    • 实测主要对cpu.c和kvm.c这两个文件中cpuid指令的执行结果和KVM相关的字符串特征进行补丁就可以绕过:

Themida、Enigma和Winlicense

这几种保护软件对于虚拟机的检测手段较少,部分版本甚至可以在VMware和KVM虚拟机里面直接运行,无额外的虚拟机检测手段,基本上进行简单配置即可实现绕过,VMware和KVM虚拟机的测试情况如下。

序号 名称 检测手段 绕过方法
VMware KVM
1 Themida 2.3.7 cupid 修改.vmx配置 源码patch
2 Themida 2.4+ - - -
3 Winlicense 2.4.6 - - -
4 Enigma 6.0 cupid 修改.vmx配置 源码patch

后记:KVM反虚拟机之单步异常与eip

问题与现象

在分析绕过KVM虚拟机时,对源码进行Patch后依然不能绕过VMP的虚拟机检测,分析发现在vmp执行cpuid检测代码的地方有这么一段代码(这是vmp2.12版本的代码,其余版本可能有差异但功能类似)

1
2
3
4
1. 9D       popfd
2. 0F A2 cpuid
3. 90 nop
4. 60 pushad

在cpuid处下断点后会进入单步异常,正常情况下触发单步异常后,eip会指向下一条指令nop,在KVM虚拟机中触发单步异常后eip却指向pushad。

分析异常处理函数发现VMP判断如果触发异常时eip指向pushad,则检测到虚拟机,如果eip指向nop,则未检测到虚拟机。

原理与解决

通过google发现这是一个古老的问题,根本原因是旧版本的虚拟机内核处理cpuid指令bug,导致同一段代码在虚拟机和物理机上有不同的表现。

VM会将CPU分为ROOT和NON-ROOT两种模式,一般的虚拟机都运行在NON-ROOT模式,但cpuid属于特权指令,执行时无条件进入ROOT模式。ROOT状态下,hypervisor模拟cpuid执行,指令执行后eip会加2(cpuid机器码为0F A2占2个字节)。

看一段检测虚拟机的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1.  int filter(EXCEPTION_POINTERS * pException, bool *p_found ) {
2. if(*(BYTE*)pException->ContextRecord->Eip == 0x90) {
3. *p_found = false;
4. }
5. return EXCEPTION_EXECUTE_HANDLER;
6. }
7
8. bool test() {
9. bool b_found = true;
10. __try {
11. __asm {
12. mov eax, 0x12345678
13. pushfd;
14. or DWORD ptr [esp], 0x100;
15. popfd;
16. cpuid;
17. nop;
18. int 3;
19. }
20. }
21. __except(filter(GetExceptionInformation(), &b_found ));
22. return b_found;
23. }

上面的test函数在虚拟机中运行会返回true,在物理机上运行会返回false。

正常情况下,检测代码执行到cpuid指令时EFLAGS的TF标记位为1即触发单步异常,物理机中单步异常后eip会指向nop的地址,而在虚拟机中cpuid模拟执行完将eip+2会指向int 3的地址,所以可以在异常处理函数中判断单步异常时eip指向的是nop还是int 3确定是否为虚拟机。

VirtualBox和QEMU均有此问题。不过Linux内核在5.3+版本已修复,使用Linux5.3以上版本可避免出现该问题,相关issue;但VirtualBox似乎还未修复完全,相关issue

打赏点小钱
支付宝 | Alipay
微信 | WeChat