商业虚拟化平台中的ACPI检查规避技术
概述
数十种虚拟机检查方法散布在各种开源项目中。从商业应用到成熟的恶意软件,你会看到许多相同的检查方法。这些检查通常涉及查找驱动程序、设备、进程、注册表项、自定义供应商信息、时序攻击等。大多数这些方法可以通过调整虚拟机配置轻松缓解。
对于像VMware、VirtualBox、Xen、Hyper-V、Parallels等商业虚拟化工具,有一些通用方法可以确定你的应用程序是否在虚拟环境中运行。本文介绍的方法提供了额外的检查位置,并提供了一些绕过检查的方法。主要目的是展示一些有吸引力的替代方案,以替代大多数应用程序使用的非常公开和众所周知的检查。这不是一种新的检测方法,只是我研究的内容以及如何缓解它。然而,我确实注意到,许多具有大量虚拟机检查的大型项目并没有利用这一点,这有点令人惊讶。
ACPI表条目
我进行了一些探索,试图确定一种简单的方法来检测应用程序是否在虚拟环境中运行以及它是哪种虚拟环境。约束是必须从用户模式完成,并且实现保持紧凑。假设公共检查已被缓解,我搜索了真实机器和几种商业虚拟化平台之间的不一致之处。有很多有趣的地方可以利用,例如PCI总线和PnP设备信息,但它们从用户模式有点难以触及(除非你通过注册表)。我再次翻阅了ACPI文档,并决定深入研究如何规避这种检查。当我阅读各种表及其用途时,我记得供应商可以设置OEM ID和ACPI表头中的大多数字段。碰巧的是,ACPI表可以使用EnumSystemFirmwareTables/GetSystemFirmwareTable从用户模式查询,或者通过NtQuerySystemInformation使用SystemFirmwareTableInformation类直接访问源。
注意:是的,这些可以通过kernel32上的钩子、NtQuerySystemInformation的IAT钩子或系统调用钩子、DKOM等来缓解,但我们感兴趣的是更优雅的解决方案。
考虑到这一点,我快速编写了一个工具,可以转储所有ACPI表及其头,以便我可以将我的真实机器与一些商业/流行的开源工具(如QEMU)进行比较。结果不言自明。
ACPI表枚举(真实硬件)
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
29
|
10:47:25 26-02-2023 => [ACPI TABLE] DBGP
10:47:25 26-02-2023 => [ACPI TABLE] MCFG
10:47:25 26-02-2023 => [ACPI TABLE] FACP
10:47:25 26-02-2023 => [ACPI TABLE] APIC
10:47:25 26-02-2023 => [ACPI TABLE] HPET
10:47:25 26-02-2023 => [ACPI TABLE] FPDT
10:47:25 26-02-2023 => [ACPI TABLE] SSDT
10:47:25 26-02-2023 => [ACPI TABLE] SSDT
10:47:25 26-02-2023 => [ACPI TABLE] FIDT
10:47:25 26-02-2023 => [ACPI TABLE] SSDT
10:47:25 26-02-2023 => [ACPI TABLE] SSDT
10:47:25 26-02-2023 => [ACPI TABLE] SSDT
10:47:25 26-02-2023 => [ACPI TABLE] SSDT
10:47:25 26-02-2023 => [ACPI TABLE] NHLT
10:47:25 26-02-2023 => [ACPI TABLE] LPIT
10:47:25 26-02-2023 => [ACPI TABLE] WSMT
10:47:25 26-02-2023 => [ACPI TABLE] SSDT
10:47:25 26-02-2023 => [ACPI TABLE] SSDT
10:47:25 26-02-2023 => [ACPI TABLE] DBG2
10:47:25 26-02-2023 => [ACPI TABLE] SSDT
10:47:25 26-02-2023 => [ACPI TABLE] BGRT
10:47:25 26-02-2023 => [ACPI TABLE] WPBT
10:47:25 26-02-2023 => [ACPI OEMID] ALASKA
10:47:25 26-02-2023 => [ACPI OEMID] ALASKA
10:47:25 26-02-2023 => [ACPI OEMID] ALASKA
10:47:25 26-02-2023 => [ACPI OEMID] ALASKA
10:47:25 26-02-2023 => [ACPI OEMID] ALASKA
10:47:25 26-02-2023 => [ACPI OEMID] ALASKA
[...]
|
ACPI表枚举(VMware)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
07:54:39 26-02-2023 => [ACPI TABLE] MCFG
07:54:39 26-02-2023 => [ACPI TABLE] FACP
07:54:39 26-02-2023 => [ACPI TABLE] SRAT
07:54:39 26-02-2023 => [ACPI TABLE] WAET
07:54:39 26-02-2023 => [ACPI TABLE] APIC
07:54:39 26-02-2023 => [ACPI TABLE] HPET
07:54:39 26-02-2023 => [ACPI TABLE] WSMT
07:54:39 26-02-2023 => [ACPI OEMID] VMWARE
07:54:39 26-02-2023 => [ACPI OEMID] INTEL
07:54:39 26-02-2023 => [ACPI OEMID] VMWARE
07:54:39 26-02-2023 => [ACPI OEMID] VMWARE
07:54:39 26-02-2023 => [ACPI OEMID] VMWARE
07:54:39 26-02-2023 => [ACPI OEMID] VMWARE
07:54:39 26-02-2023 => [ACPI OEMID] VMWARE
|
ACPI表枚举(QEMU)
1
2
3
4
5
6
7
8
9
10
11
|
07:58:21 26-02-2023 => [ACPI TABLE] RSDT
07:58:21 26-02-2023 => [ACPI TABLE] FACP
07:58:21 26-02-2023 => [ACPI TABLE] APIC
07:58:21 26-02-2023 => [ACPI TABLE] MCFG
07:58:21 26-02-2023 => [ACPI TABLE] WAET
07:58:21 26-02-2023 => [ACPI OEMID] BOCHS
07:58:21 26-02-2023 => [ACPI OEMID] BOCHS
07:58:21 26-02-2023 => [ACPI OEMID] BOCHS
07:58:21 26-02-2023 => [ACPI OEMID] BOCHS
07:58:21 26-02-2023 => [ACPI OEMID] BOCHS
07:58:21 26-02-2023 => [ACPI OEMID] BOCHS
|
在安装了最流行的虚拟化应用程序(Hyper-V、VirtualBox、Parallels等)后,很容易看到它们都有特定的OEMID(以及OEM表ID和创建者ID),这些ID指示了托管工具是什么。你可能已经注意到几件事,比如表的数量显著减少,以及一个特定的表出现在所有这些平台上——WAET。
在记录了差异和每个平台的标识符后,我快速编写了一个检查,如下所示。
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
struct acpi_table_header_format
{
uint32_t signature;
uint32_t length;
uint8_t revision;
uint8_t checksum;
uint8_t oem_id[ 6 ];
uint64_t oem_table_id;
uint32_t oem_revision;
uint32_t creator_id;
uint32_t creator_revision;
};
std::unordered_set<std::string> disallowed_acpi_entries = {{"WAET"}, [...]};
std::unordered_set<std::string> disallowed_oem_ids = {
{"VMWARE"}, {"VBOX"}, {"BOCHS"}, {"VRTUAL"}, {"PRLS"},
};
void check_firmware_tables() {
constexpr unsigned long acpi_signature = 'ACPI';
buffer<unsigned long> buf;
buf.resize(0x1000);
enumerate_fw_tables(acpi_signature, buf.get(), 0x1000);
std::vector<std::pair<std::string, unsigned long>> firmware_table_ids{};
for (auto it = 0; buf.get()[it] != 0; it++) {
char tid[6] = {0};
memcpy(tid, &buf.get()[it], sizeof(unsigned long));
firmware_table_ids.emplace_back(std::make_pair(tid, buf.get()[it]));
if (disallowed_acpi_entries.contains(tid)) elog::critical("acpi entry indicates virtual environment (%s).", tid);
}
for (const auto &table_id : firmware_table_ids | std::views::values) {
buffer<uint8_t> fwt_buffer;
fwt_buffer.resize(0x100);
if (const auto ret = get_fw_table_info( acpi_signature, table_id, fwt_buffer.get(), fwt_buffer.size()); ret == 0)
continue;
auto *hdr = reinterpret_cast<acpi_table_header_format *>(fwt_buffer.get());
char oem_id[8] = {0};
memcpy(oem_id, hdr->oem_id, 0x6);
if (disallowed_oem_ids.contains(oem_id)) elog::critical("acpi oem id indicates virtual environment (%s).", oem_id);
}
}
|
如果你在VMware中运行一个实现了上述检查的应用程序,你会得到以下结果:
1
2
|
07:54:39 31-02-2023 => [crit] acpi entry indicates virtual environment (WAET).
07:54:39 31-02-2023 => [crit] acpi oem id is blacklisted (VMWARE).
|
当然,这仅对实现WAET或修改ACPI表头的工具有用。如果你读过我的其他文章,你可能期望我会详细讨论WAET和其他一些几乎无用的信息……你是对的。
WAET,别走
Windows ACPI模拟设备表(WAET)的目的在Microsoft提供的文档中非常直接,但如果你不想阅读,那么要知道这是一个表,如果虚拟机监控程序模拟特定设备(如ACPI PM计时器或RTC),则需要实现它。这个表背后的想法是通知Windows存在一个模拟的RTC/ACPI PM计时器并且功能正常;“这防止了不必要的变通方法,这些方法在操作系统访问设备时可能引入开销或不正确的行为。”(Microsoft WAET文档)。
值得注意的是,表的存在改变了客户机的行为。例如,如果设置了ACPI_WAET_PM_TIMER_GOOD标志,客户机将只执行一次ACPI PM计时器读取,而不是多次,减少不必要的VM退出并提高性能。例如,VMware设置了ACPI_WAET_PM_TIMER_GOOD标志;QEMU也设置了ACPI_WAET_PM_TIMER_GOOD。除了一个可见的表指示模拟之外,没有额外的权衡被注意到或被观察者 discernible。实现这一点的平台这样做是因为它对每个人都更好(在性能方面)。
ACPI表可见性
你也不必止步于此;ACPI表是客户机可见的,这意味着你可以找到额外的表及其相关字段,这些字段因机器而异。有些只存在于像VMware、QEMU等工具上,而其他一些存在于真实硬件上,但根据是否是虚拟环境呈现不同的标志。探索一下,看看在特定平台上可以找到哪些不同的表和字段配置。不一致之处很容易发现,但也容易缓解;深入研究规范并定位更具体的检测对于任何虚拟机检查的 longevity 更好,尽管它可能不是“通用的”。
也就是说,你可以从ACPI命名空间中删除这个表而没有任何问题,并且对此的检查是无效的。我假设你可以通过向VMX配置中添加acpi.skiptables=“WAET"来实现这一点。
OEM ID、OEM表ID、修订号等
检查的另一部分是查看ACPI表头中的OEMID字段。顾名思义,这些字段由OEM设置以标识自己。我查看的每个虚拟化工具都修改了OEM信息以反映它是哪个平台。如果你希望打破这种类型的检查以发现 hardened VM,你可能需要更深入地研究是否可以在环境构建时传递或直接修改OEM ID。对于VMware,VMX文件中的一个设置允许SMBIOS反映主机信息——SMBIOS.reflectHost = TRUE。我假设可能有类似的设置用于ACPI OEM信息,但我无法通过典型配置实现。
那么,到底怎么了?没有优雅的解决方案?嗯……我有一个想法——为什么不尝试通过配置选项注入ACPI表?我不确定除了在运行时尝试拦截和修改信息之外的替代方案……而且 personally,我会选择更好的解决方案。无论如何,我们如何做到这一点?嗯,似乎VMware早在2012年就添加了对从客户机中删除ACPI表的支持,适用于EFI和传统BIOS。我们能够使用acpi.addtable.filename = “path/to/acpi_table.tbl"和acpi.skiptables = “WAET,MCFG,etc"来添加表。文档说明skiptables功能在添加表之前执行,这允许我们使用此扩展来替换ACPI表。将ACPI表注入/自定义现有表 for VMware 并不是我计划在本文中涵盖的内容,因为我打算简洁,但我会快速涵盖 overall thought process。在未来的文章中,我可能会深入研究与ACPI黑客和真实硬件相关的更有趣的内容。
缓解VMware的ACPI/FIRM检查
- 确定用于构建目标表的ASL编译器(Intel ASL编译器/Microsoft ASL编译器)。
- 从ACPICA(ACPI组件架构)项目下载编译器/反编译器。
- 反编译并查看目标表源(例如WAET)。
- 转储感兴趣的ACPI表。
- 修改ACPI表头和其他特征。
- 重新编译表并将配置选项附加到VMX文件中以替换表。
- 启动并验证。
我们将以修改WAET表为例。反汇编表后,它看起来像这样:
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
29
30
31
|
/*
* Intel ACPI Component Architecture
* AML/ASL+ Disassembler version 20221020 (32-bit version)
* Copyright (c) 2000 - 2022 Intel Corporation
*
* Disassembly of waet.dat, Fri Mar 10 11:08:30 2023
*
* ACPI Data Table [WAET]
*
* Format: [HexOffset DecimalOffset ByteLength] FieldName : FieldValue (in hex)
*/
[000h 0000 004h] Signature : "WAET" [Windows ACPI Emulated Devices Table]
[004h 0004 004h] Table Length : 00000028
[008h 0008 001h] Revision : 01
[009h 0009 001h] Checksum : 61
[00Ah 0010 006h] Oem ID : "VMWARE"
[010h 0016 008h] Oem Table ID : "VMW WAET"
[018h 0024 004h] Oem Revision : 06040001
[01Ch 0028 004h] Asl Compiler ID : "VMW "
[020h 0032 004h] Asl Compiler Revision : 00000001
[024h 0036 004h] Flags (decoded below) : 00000002
RTC needs no INT ack : 0
PM timer, one read only : 1
Raw Table Data: Length 40 (0x28)
0000: 57 41 45 54 28 00 00 00 01 61 56 4D 57 41 52 45 // WAET(....aVMWARE
0010: 56 4D 57 20 57 41 45 54 01 00 04 06 56 4D 57 20 // VMW WAET....VMW
0020: 01 00 00 00 02 00 00 00 // ........
|
我们可以看到所有明显的指示器,我们将修改它们而不是VMWARE。我拿了现有的头,修改了它,重新编译了它,并更新了校验和以反映更改。现在,我将以下内容添加到我们的.vmx文件中。
1
2
|
acpi.skiptables = "WAET"
acpi.addtable.filename="waet.dat"
|
让我们启动我们的VM(希望它能启动)并查看结果……
优秀。除了表的签名仍然“有问题”之外,其他检查的字段将不再被标记。我们可以对其他表重复这个过程,或者模仿一个真实的硬件设置,其中ACPI条目由相当于no-op的内容组成。ACPI和ASL/AML解释器的细节将不在这里涵盖。如果你有兴趣了解更多关于这些内容并自己探索,你可以在文章末尾的推荐阅读部分找到更多信息。
QEMU的荣誉提及
对于那些希望在QEMU中实现相同结果的人,你可以庆幸开发者提供了一种方便的方法来设置ACPI表的OEM ID和相关字段,使用类似这样的东西来覆盖默认值:qemu -acpidefault oem_id=ALASKA,oem_table_id=AMI。它应该传播到所有使用默认值的其他表。用户定义的ACPI表不包括在内。
HPET验证
值得注意的是,即使调整了ACPI表头以匹配“真实硬件”