当前位置: 华文星空 > 知识

操作系统能否知道自己处于虚拟机中?

2019-12-03知识

目前虚拟机环境检测有两个「金标准」,分别是Al-khaser和Pafish。这两个开源项目几乎一网打尽了所有公开常见的VM检测技术。下面简要分析一下它们的技术原理。

一、硬件信息检测

首先大概说说操作系统是怎么知道这台计算机安了哪些设备的。计算机启动的时候,主板固件会给OS传两个信息表,分别是ACPI和SMBIOS。ACPI表有很多部分,其中硬件信息主要集中在DSDT和SSDT这两部分。

ACPI表的每个部分开头都有一个OEM ID和OEM Table ID, 这是第一个容易露馅的地方 ,例如QEMU默认将OEM ID写为BOCHS,将OEM Table ID写为BXPC + 部分名称,如DSDT部分就写成「BXPCDSDT」。

虚拟机的ACPI表中往往会存在一些现实中不存在的硬件,用于主客机间通讯, 这是第二个容易露馅的地方 ,例如QEMU的DSDT表中会有DBUG和FWCF这两个硬件。

计算机中大部分设备的信息并不写在ACPI和SMBIOS表中。像显卡、声卡、网卡、USB这些都属于PCI设备,与PCI控制器相连。PCI控制器本身提供一个接口,可以列出所有检测到的PCI设备。每个PCI设备有四个用来亮明身份的代号,分别是Vendor ID, Device ID, Subsystem ID, class ID。一般来说,OS检测到PCI设备时,会首先根据Vendor ID和Device ID搜索驱动;如果搜不到驱动,则会根据 class ID查找是否有这一类设备的通用驱动。

虚拟机模拟出的PCI设备,其身份代号往往采用特定数字, 这是第三个容易露馅的地方 ,例如QEMU模拟出的VirtIO设备,其Vendor ID多为0x1AF4。

相比于ACPI,SMBIOS对系统的正常运行没有那么重要。但是Windows的「系统信息」工具显示的内容,多半都来自SMBIOS表, 这是第四个容易露馅的地方 ,例如目前主流的虚拟机固件是OVMF,那么虚拟机里面「系统信息」就会显示出OVMF的信息。

有些恶意软件会通过检测系统是否有风扇和热区域(Thermal Zone)来判断是否处于虚拟环境。目前所有版本的Windows,都是从SMBIOS表中读取风扇信息,从ACPI表中读取Thermal Zone信息。但是正常的商业软件一般不使用此方法判定,因为很多正常的笔记本电脑也没有在SMBIOS表中写入风扇信息。

此外还有硬盘产品名、序列号、声卡ID、网卡MAC地址等容易带有虚拟机特征的地方。

二、CPU信息检测

x86 CPU本身有一条指令叫CPUID,用于探测该CPU所支持的功能,例如是否支持SSE指令集等。有些功能虚拟机无法模拟,就会屏蔽掉相关功能的信息, 这是第五个容易露馅的地方

早期虚拟化技术不完善的时候,虚拟机软件需要挪动一些重要数据结构的位置,例如中断表(IDT)等。著名的Red Pill程序就是靠读取这些结构的地址来判定虚拟环境。但是后来有了Intel VT-x等硬件虚拟化技术,以及KVM以后,这些检测方法就基本被淘汰了。

基于KVM的客机,如果将EAX寄存器置为0x40000000,并执行CPUID指令,会在EBX、ECX、EDX寄存器中读取到字符串「KVMKVMKVM」, 这是第六个容易露馅的地方

三、驱动信息检测

在有Linux KVM之前,各虚拟机软件用的几乎都是半虚拟化(Paravirtualization),也就是必须对客机软件做一定修改才能在虚拟机中正常运行。例如VMWare虚拟机需要在客机中安装一些驱动程序,这些驱动的信息中都带有VMWare标识, 这是第七个容易露馅的地方

KVM实现的是全虚拟化,不需要对客机做任何修改,所以不必担心这些问题。但是,按默认配置的QEMU虚拟机会带有很多VirtIO接口的设备,这些设备的驱动也会留下虚拟机的痕迹,需要当心。

四、计时检测

x86 CPU中有一个精度极高的计时器,称为TSC计时器,可以精确到CPU时钟周期数。那么可以执行一段CPU指令,并将消耗的时间与正常CPU上消耗的时间进行对比,如果明显高于正常值,就可判定处于虚拟机环境。

目前最常用的方法是,在两次读取TSC计时器之间,执行一次CPUID指令。前文说到,虚拟机软件一般会特殊处理CPUID指令,屏蔽掉一些无法模拟的功能的信息,执行这些操作所需的时间远多于正常CPU上执行一次CPUID所需的时间, 这是第八个容易露馅的地方

目前没有简单的办法可以骗过计时检测,所有已知方案都需要用特制的Linux内核和特制的QEMU软件配合。

但是近几年来,虚拟机环境检测已经没那么重要了。这是因为微软在Windows 10上大力推广Hyper-V技术,有相当数量的用户自己都不知道自己处于虚拟机环境。例如,只要在Windows 10 Home的Windows Defender中开启Memory Integrity或Core Isolation功能,就等同于开启Hyper-V,并让Windows运行在虚拟机环境下。

于是有很多网游反作弊系统,只要检测到开启了Hyper-V,就放弃检测虚拟机环境。于是在Linux界就有了一种神奇的操作,先用KVM开Windows虚拟机,然后在Windows中开启Hyper-V,这样就能愉快地玩各种3A巨作了。

当然这种操作需要CPU和Hypervisor支持Nested Virtualization,然而Windows的Hyper-V长期以来不支持AMD的Nesting,会在启动时卡死。直到2020年6月,微软才宣布Windows 10 Insider的Hyper-V开始支持嵌套虚拟化。最近国外Reddit上有人报告使用5.11.6版本的Linux内核(无需打补丁),配合4.2版本的QEMU,可以在AMD系统上正常启动Windows 10 20p并开启Hyper-V,可能是Linux KVM那边做了改进。

既然这么多人看我就再写一写怎么绕开这些虚拟机检测方法。我日常电脑装的是Linux,平时用Linux KVM + QEMU方案跑Windows虚拟机。QEMU是开源的虚拟机Hypervisor,命令行参数非常灵活,有另一个开源项目libvirt专门帮助配置QEMU的参数。下面的内容都是基于KVM + QEMU + libvirt。我从简单的操作写起,比较难隐藏的东西放到后面。

1)CPU信息:打开libvirt的XML配置,找到<cpu>段落,将mode设为host-passthrough,然后段落里面添加一行

<feature policy="disable" name="hypervisor"/>

找到<os>段落,里面添加一行

<smbios mode="host"/>

2)KVM Hypervisor信息:XML配置中找到<features>段落,里面添加几行

<hyperv> <vendor_id state="on" value="random"/> </hyperv> <kvm> <hidden state="on"/> </kvm>

3)硬盘产品名、序列号:将Disk bus设为SCSI,Serial随便填写,添加一个SCSI Controller,Model填为saslsi1068,然后在XML配置中找到<disk>段落,在里面添加几行

<vendor>Samsung</vendor> <product>20GB Harddisk</product>

此处Vendor和Product可随便填写

4)网卡:NIC Device Model选rtl8139,然后用这个网站随机生成一个MAC地址填进去。

5)QEMU硬编码的ID信息:修改这些ID有两种方法,一种是下载QEMU源代码,修改硬编码ID后重新编译;另一种是直接用radare2等二进制修改工具,在QEMU可执行文件上打补丁。下面列出应当隐藏的硬编码ID对应的代码位置。(QEMU源代码见github)

ACPI OEM ID:见hw/acpi/aml-build.c中的build_header函数

ACPI DSDT中的FWCF设备:见hw/i386/acpi-build.c中的build_dsdt函数

ACPI DSDT中的DBUG设备:见hw/i386/acpi-build.c中的build_dbg_aml函数

常规PCI设备ID:见hw/pci/pci.c中的pci_set_default_subsystem_id函数

VGA设备ID:见hw/display/vga-pci.c中的vga_pci_ class_init函数

声卡ID:见hw/audio/hda-codec-common.h中所有含有QEMU_HDA_ID_*宏的数据结构

VirtIO Serial设备ID:见hw/virtio/virtio-serial-pci.c中的virtio_serial_pci_ class_init函数

6)风扇和热区域:利用QEMU的-acpitable参数,伪造一个ACPI SSDT表,里面填入一个Thermal Zone的信息;再利用QEMU的-smbios参数,伪造一个SMBIOS Entry Type 27,里面填入一个风扇的信息。

ACPI标准第11.7节提供了ACPI Thermal Zone的例子

SMBIOS标准第7.28节提供了SMBIOS Entry Type 27的格式说明

7)计时检测:这个绕过很麻烦,需要重新编译Linux内核,主要原理是每当遇到CPUID这种需要Hypervisor专门处理的指令时,都把TSC计时器的当前值减掉一定量,这样就抵消掉处理这些指令花掉的时间。

详见Reddit上相关讨论:
https://www. reddit.com/r/VFIO/comme nts/g4pqkq/rdtsc_detection_workarounds/
https://www. reddit.com/r/VFIO/comme nts/i071qx/spoof_and_make_your_vm_undetectable_no_more/
https://www. reddit.com/r/VFIO/comme nts/i1xbue/here_is_my_take_on_rdtsc_offsetting/