[博客翻译]绕过具有自动TPM2解锁功能的系统上的磁盘加密


原文地址:https://oddlama.org/blog/bypassing-disk-encryption-with-tpm2-unlock/


🔗 绕过带有自动TPM2解锁功能的磁盘加密

2025-01-16 #安全 #Linux
你是否使用TPM2和systemd-cryptenrollclevis设置了自动磁盘解锁?那么,攻击者只需短暂物理接触你的机器,就有很大可能解密你的磁盘——经过一些准备,10分钟就足够了。在本文中,我们将探讨基于TPM2的磁盘解密是如何工作的,并理解为什么许多设置容易受到一种文件系统混淆攻击。我们将通过攻击两个不同的真实系统(Fedora + clevis,NixOS + systemd-cryptenroll)来演示这一过程。

# 用于将密钥注册到TPM的示例命令。你的系统是否受此问题影响,不取决于你在这里选择的PCR。
systemd-cryptenroll --tpm2-pcrs=0+2+7 --tpm2-device=auto <设备>
clevis luks bind -d <设备> tpm2 '{"pcr_bank":"sha256","pcr_ids":"0,1,2,7"}'

TL;DR: 大多数TPM2解锁设置未能验证解密分区的LUKS身份。由于initrd必须位于未加密的引导分区中,攻击者可以检查它,了解它如何解密磁盘以及它期望在内部找到的文件系统类型。通过使用已知密钥重新创建LUKS分区,我们可以混淆initrd,使其执行恶意的init可执行文件。由于TPM状态不会因这个假分区而改变,原始的LUKS密钥可以从TPM中解封。之后,可以完全恢复初始磁盘状态,并使用获得的密钥解密。
如果你额外使用PIN解锁TPM,或者使用正确验证LUKS身份的initrd(这需要手动操作,所以你可能知道是否如此),那么你是安全的。

🔗 基于TPM2的磁盘解密背后的原理

安全且无需密码的磁盘解密的理念是,TPM2可以存储一个额外的LUKS密钥,只有在TPM处于预定的、已知的良好状态时,系统才能检索到该密钥。这种状态记录在所谓的平台配置寄存器(PCR)中,标准合规的TPM中有24个这样的寄存器。它们的预期用途在Linux TPM PCR注册表中有描述,但也在systemd-cryptenroll(1)手册页中作为表格进行了简洁总结。
这些寄存器存储哈希值,这些哈希值在启动过程中根据引导加载程序哈希、使用的固件、启动的内核、initrd镜像等信息逐步更新。通过建立从引导加载程序到Linux用户空间的所有组件的信任链,我们可以确保更改任何组件都会影响一个或多个PCR。在TPM中存储数据时,你需要选择一个PCR列表,TPM将确保只有在这些PCR处于与注册密钥时相同的状态时,才能再次检索数据。
其中一些寄存器有约定的用途,并更新了有关系统的特定信息,例如主板的固件、BIOS配置、OptionROM(POST后从外部设备如PCIe设备加载的额外固件)、安全启动策略等。以下是手册页中的部分内容,包含了一些对我们重要的寄存器:

通常,加密卷将绑定到PCR 7、11和14的某种组合(如果使用shim/MOK)。为了允许固件和操作系统版本更新,通常不建议使用PCR 0和2等PCR,因为它们覆盖的程序代码应该已经通过测量到PCR 7中的证书间接覆盖。通过证书哈希进行验证通常比通过直接测量进行验证更可取,因为它在操作系统/固件更新时更不容易出错:每次更新时测量值都会改变,但签名应保持不变。有关更多讨论,请参阅Linux TPM PCR注册表。
如果你注册了自己的安全启动密钥并使用统一内核镜像(UKI),那么仅使用PCR 7就足以确保完整性,直到我们需要解锁磁盘为止。一些发行版则预签了EFI可执行文件,使用微软的密钥签名,这使得它们能够默认启用安全启动,而无需用户生成和注册任何内容。由于这也意味着用户无法签名他们的内核和/或initrd镜像,通常会使用一个受信任的预签名shim,在执行内核和initrd之前将其哈希值测量到PCR 9中,在这种情况下,我们希望使用PCR 9。另一种方法是让用户生成所谓的机器所有者密钥(MOK),如果他们想签名某些内容,在这种情况下,也应使用PCR 14。
因此,确切的PCR选择可能会根据用户的设置有所不同。快速在GitHub上搜索在互联网上搜索会发现,许多人仍然选择使用额外的PCR,如0和2,以及7,这当然没问题,但可能会导致在BIOS或某些固件更新时密钥无法访问——这可能会很烦人。

🔗 常见的(易受攻击的)设置

如果你已经设置了安全启动,为LUKS分区配置TPM2解锁通常非常简单。大多数指南会使用systemd-cryptenrollclevis,它们是不同的实现,内部执行以下某种变体:

  1. 将新生成的密钥添加到你的LUKS分区
  2. 根据你选择的PCR将此密钥密封在TPM中
  3. 将加密的TPM上下文存储在LUKS令牌元数据中,这是稍后解封密钥所必需的

clevissystemd-cryptenroll都可以以其他方式存储令牌,例如使用FIDO2密钥。我发现clevis还支持从网络资源检索令牌,但除此之外,这两个工具的功能非常相似。systemd-cryptenroll只是预装在systemd中,因此通常使用起来更简单。以下是一个示例:

systemd-cryptenroll --tpm2-pcrs=7 --tpm2-device=auto /dev/nvme0n1p3

理论上,假设内核命令行无法编辑,磁盘现在得到了适当的保护,对吗?只有在PCR 7未更改的情况下才能解密磁盘,我们对引导加载程序、内核或initrd所做的任何操作都会影响PCR 7。
当然,如果没有什么小问题,我就不会问了:假设所有磁盘都已正确挂载,initrd可以确定到目前为止没有任何代码被修改。但它并不能自动确保磁盘上的数据是真实的。作为最后一步,initrd将执行真实系统的init可执行文件,该文件通常在执行前不会经过任何类型的签名检查。为什么需要呢——毕竟它是加密根分区的一部分,攻击者无法修改。

🔗 漏洞利用

首先,重要的是要知道,如果TPM解锁因任何原因失败,initrd将回退到密码提示。BIOS更新可能会导致安全启动数据库被更改(从而使PCR 7无效),或者有人在更新系统时出错,忘记正确签名内核和initrd。在这种情况下,你不想完全被锁在系统外,因此要求输入密码是合理的。
但这意味着如果我们用一个新的LUKS分区替换加密分区(我们选择密码),那么TPM解密将失败,我们将被要求输入密码,而密码由我们控制。输入密码后,initrd现在会认为它已经正确解密了分区并继续。如果我们设法在假的LUKS分区中放入正确类型的文件系统,以便实际挂载成功,我们可以提供一个恶意的init二进制文件,该文件现在可以完全访问解锁的TPM,从而允许我们解密原始文件系统,我们必须在创建恶意分区之前备份原始文件系统。
现在你可能会认为initrd可以简单地验证文件系统UUID,然后再挂载它,因为我们无法从磁盘读取它,但请记住,initrd知道的任何内容都是公开的,因为引导分区和initrd镜像没有加密。因此,如果有必要,我们可以重用相同的LUKS UUID和文件系统UUID。

🔗 保护系统

为了解决这个问题,我们需要能够在访问加密卷上的任何文件之前验证所有加密卷的身份。在2022年10月Lennart Poettering的这篇文章中,他描述了systemd中安全启动的状态,并提到了如何使系统安全的过程。这有点复杂,所以让我重申一下重要的部分。
在磁盘解锁后,我们希望从其卷密钥(用于加密所有数据的主密钥)派生一个值,并使用该值扩展PCR 15。这确保了任何假卷都会更改此值,因为原始卷密钥无法被知道。使用systemd-cryptsetup而不是cryptsetup已经可以通过在crypttab文件中添加tpm2-measure-pcr=yes来处理这个问题。
如果我们现在确保磁盘解密顺序是确定性的,那么我们可以将PCR 15中的值与initrd中已知的签名值进行比较。如果观察到错误的值,initrd现在可以在执行任何恶意操作之前中止启动过程。

🔗 许多错误的指南

有许多指南更详细地描述了如何设置基于TPM2的磁盘解锁,虽然概念总是相同的,但你肯定会找到一个适合你最喜欢的发行版的指南。以下是我在网上找到的指南列表,按日期排序:

不幸的是,我没有找到任何解决这个问题的指南,因此大多数用户设置可能都受到这个问题的影响。不过,公平地说,这是否是一个问题显然取决于你的威胁模型。如果你只是使用TPM来解锁你的家庭服务器,而其他人没有物理访问权限,那么这可能对你来说不是问题。但如果你使用它来保护笔记本电脑上的数据以防盗窃,那么你可能希望设置TPM PIN或实现PCR 15验证,如上所述。
值得注意的是,我发现systemd-cryptenroll的ArchWiki条目在文章末尾的警告中承认了这个问题:

仅绑定到预启动测量的PCR(PCR 0-7)会打开来自恶意操作系统的漏洞。一个带有从真实根文件系统复制的元数据(如分区UUID)的恶意分区可以模仿原始分区。然后,initramfs将尝试将恶意分区挂载为根文件系统(解密失败将回退到密码输入),保持预启动PCR不变。由攻击者控制的恶意根文件系统仍然能够接收真实根分区的解密密钥。有关更多信息,请参阅《勇敢的新信任启动世界》和BitLocker文档。
虽然这是正确的,但仅使用任何PCR 8-23也不会自动保护你的数据。initrd仍然需要确保在执行系统的init二进制文件之前更改相应的PCR,而默认情况下不会这样做。

🔗 对Fedora机器的概念验证利用

现在,让我们来看一个真实系统,我们将以与其他人相同的方式设置它。我选择了上面的一篇Fedora文章,但你可以预期这也适用于其他发行版。总结一下,我的设置包括以下步骤:

  • 安装Fedora 41,我选择了带有ext4的LUKS加密根分区
  • 在BIOS中启用安全启动(并安装微软密钥,因为Fedora是用这些密钥签名的)

我们立即注意到的一个有趣的事情是,Fedora引导加载程序是使用微软密钥签名的。我们已经在开头简要讨论过这一点,这意味着它根本无法签名initrd。相反,他们有一个签名的shim,在引导加载程序之后执行,它将计算内核和initrd的哈希值,并将这些值扩展到PCR 9中。因此,当我们将密钥注册到TPM时,现在必须包括PCR 9,否则initrd可能会被修改。
这种方法的优点是用户不必处理自定义安全启动密钥,但缺点是每次内核或initrd更新都会影响PCR 9中的值,因此需要在每次系统更新后重新注册密钥。以下是我将密钥注册到TPM时PCR的快照。这也是我们稍后需要达到的状态。

[root@localhost]# systemd-analyze pcrs
NR NAME        SHA256
0 platform-code    8c2af609e626cc1687f66ea6d0e1a3605a949319514a26e7e1a90d6a35646fa5
1 platform-config   299b0462537a9505f6c63672b76a3502373c8934f08a921e1aa50d3adf4ba83d
2 external-code    3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969
3 external-config   3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969
4 boot-loader-code  5fdbd66c267bd9513dbc569db0b389a37445e1aa463f9325ea921563e7fb37eb
5 boot-loader-config 38a281376260137602e5c70f7a9057e4c55830d22a02bb5a66013d6ac2576d2f
6 host-platform    3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969
7 secure-boot-policy 4770a4fb1dac716feaddd77fec9a28bb2015e809a34add1a9d417eec36ec1e17
8 -          e3e23c0da36fa31767885aec7aee3180fb2f5e0b67569c3a82c2a1c3ca88a651
9 kernel-initrd    091f6917b0c8788779f4d410046250e6747043a8cd1bd75bf90713cc6de30d99
10 ima         2566bdf57c3aa880f7b0c480f479c0a88e0e72ae7ef3c1888035e7238bbe9257
11 kernel-boot     0000000000000000000000000000000000000000000000000000000000000000
12 kernel-config    0000000000000000000000000000000000000000000000000000000000000000
13 sysexts       0000000000000000000000000000000000000000000000000000000000000000
14 shim-policy     17cdefd9548f4383b67a37a901673bf3c8ded6f619d36c8007562de1d93c81cc
15 system-identity   0000000000000000000000000000000000000000000000000000000000000000
16 debug        0000000000000000000000000000000000000000000000000000000000000000
17 -          ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
18 -          ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
19 -          ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
20 -          ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
21 -          ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
22 -          ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
23 application-support 0000000000000000000000000000000000000000000000000000000000000000

🔗 检查系统

现在,让我们假装我们对系统一无所知,只是获得了对机器的物理访问权限,而机器已经关闭。
我们首先将主磁盘取出并放入我们的机器中。如果所有者没有从他们的BIOS中删除微软密钥以支持他们自己的密钥,你也可以启动Fedora或Debian的实时镜像。启动后,我们开始调查磁盘布局和分区:

[root@localhost]# blkid
/dev/nvme0n1p1: LABEL_FATBOOT="EFI" LABEL="EFI" UUID="E2