深入理解 systemd v262 中的 NvPCRs

Lobsters Hottest 工具

摘要

本实践深入探讨了 systemd v262 中的 NvPCRs,这些是 TPM NV 内存中的额外类 PCR 寄存器,用于解决 PCR 稀缺问题,以支持磁盘加密和远程证明等安全功能,并着重介绍了重新设计的安全架构。

<p><a href="https://lobste.rs/s/ya3cl7/understanding_nvpcrs_systemd_v262">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/09/25 09:11

# 理解 systemd v262 中的 NvPCR **原文链接:** https://katexochen.aro.bz/posts/systemd-v262-nvpcrs/ *systemd 在 v262 中通过在 TPM 的 NV 内存中分配类 PCR 寄存器来应对 TPM PCR 资源短缺问题,并重新设计了其锚定机制。在这篇实践详解中,我们将从零开始针对软件 TPM 重新构建 systemd 的 NvPCR,过程中会介绍必要的 TPM 概念,并分析其设计的安全性。* ## 为什么 systemd 如此“青睐”这些 PCR? systemd 的许多安全功能都依赖 TPM PCR 度量:例如无密码全盘加密——若 PCR 度量值符合预期,磁盘即可自动解锁,这既能防止凭证被盗,又能支持远程机器的加密根文件系统在无人值守下重启。服务凭证可基于预期的 PCR 状态进行加密。还支持启动阶段绑定的凭证,确保密钥仅在 initrd 中可解密。通过远程证明,机器可以向第三方证明其启动内容及后续行为——TPM 会对其当前 PCR 状态(即所谓的“引用”)进行签名。所有这些都建立在 PCR 度量之上。 **TPM PCR 资源有限。** 在常见符合标准的 TPM 中,只有 24 个 PCR 可用。PCR 索引 0–7 由固件保留用于 UEFI 启动度量;索引 16 是可重置的调试 PCR,因此不可用;索引 17–22 预留给动态可信根度量(DRTM)[1];索引 23 则保留给应用程序支持。因此,仅剩 8–15 号 PCR 可供 systemd 用于所有操作系统相关的度量[2]。 从远程证明角度看,单个度量寄存器或许足以验证系统——我们可通过度量日志回放事件,解释最终观察到的值是如何产生的。但 PCR 不仅被远程验证者读取,还用于本地密钥的加锁,而这要求可预测的值:任何流入该 PCR 的事件必须事先已知,否则策略将失效。 有些度量本质上是不可预测的,例如取决于各平台闭源的厂商固件或用户行为。我们可能希望将登录事件纳入远程证明引用,但根磁盘仍应在登录后自动解锁。因此,那些嘈杂且不可预测的事件类型需要专用寄存器:它们应与密钥所依赖的 PCR 隔离,但仍需被度量并可用于证明。这正是 systemd 迅速触及瓶颈的地方:操作系统仅有八个 PCR 插槽,几乎没有空间为不同事件类型分配专用寄存器。 因此,systemd 在 v259 中引入了 NvPCR[3]——在 TPM 非易失性内存中分配的类 PCR 寄存器(名称由此而来)。它们承载那些我们不想放入真实 PCR 的事件类型,其值通过远程证明被使用。在 v262 中,NvPCR 的锚定机制经过重新设计[4]以增强其安全性。此前,锚定基于一个随机密钥——该密钥密封于 PCR 11 并存储在磁盘上,攻击者可通过启动另一个操作系统回放预期的 PCR 11 值来恢复该密钥,或直接替换为已知密钥。本文将描述 v262 中重新设计后的机制:其工作原理及安全性分析。 ## 跟随实验环境设置 我们将通过命令行针对软件 TPM 构建自定义 NvPCR,以此理解其基本原理并获得直观感受。如果只想阅读,也没问题——我将提供所有关键输出。 **前提条件:** 通过 nix 或 dnf 安装所需工具: ```bash nix shell nixpkgs#{tpm2-tools,xxd,swtpm,openssl} # 或 dnf install tpm2-tools vim-common swtpm openssl ``` 创建工作目录并启动软件 TPM: ```bash mkdir state swtpm socket \ --tpm2 \ --tpmstate "dir=$PWD/state" \ --ctrl "type=tcp,port=2322" \ --server "type=tcp,port=2321" \ --flags startup-clear \ --pid "file=$PWD/swtpm.pid" \ -d ``` 导出连接信息以便 tpm2-tools 找到 TPM: ```bash export TPM2TOOLS_TCTI="swtpm:host=127.0.0.1,port=2321" ``` 通过读取 PCR 检查 TPM 是否正常工作: ```bash tpm2_pcrread ``` 结果应显示 PCR 0–16 全为零。若非如此,可能连接了平台自身的 TPM 而非软件 TPM,请检查 `TPM2TOOLS_TCTI` 是否正确导出。这一点很重要,因为我们不希望后续实验影响平台本身密封的密钥。 ## 作为 PCR 替代方案的 TPM NV 索引 TPM NV 索引[5] 是一个通过唯一名称标识的非易失性存储槽位。NV 索引在重启后持久化,可保存用户定义的数据:不透明值、计数器、位域等。NV 索引的属性决定了其行为方式和用途:句柄、存储数据大小、控制操作/读取权限的属性集,以及可选的授权策略和授权值(后者为唯一非公开属性)。 每个索引具有一个 *nameAlg*(名称算法),用于根据索引公开属性计算唯一名称[6]: `Name = nameAlg || H_nameAlg(marshal(TPMS_NV_PUBLIC))` 让我们创建一个类 PCR 的 NV 索引!使用 tpm2-tools 中的 `tpm2_nvdefine`。`0x01000000` 是我们要定义的索引句柄(随机选择)。`--hierarchy=o` 标志选择定义索引时使用的授权层级:像我们这样的 NV 索引存在于 TPM 的所有者层级中,定义或取消定义需要所有者授权值[7]。在典型 Linux 系统中,该值为空,因此实际拥有 TPM 设备访问权限(通常是 root)的人都持有所有者授权。 `--hash-algorithm` 标志对应之前提到的 nameAlg,选择 sha256。然后我们为 NV 索引选择属性:`authread|authwrite` 结合空授权值允许任何设备访问者读写索引;`nt=extend` 表示我们希望它像 PCR 一样可扩展。 ```bash tpm2_nvdefine 0x01000000 \ --hierarchy=o \ --hash-algorithm=sha256 \ --attributes="nt=extend|authread|authwrite" ``` 使用以下命令查看结果: ```bash tpm2_nvreadpublic 0x01000000 ``` ```text 0x1000000: name: 000be9606b61ec27bc8deec096dd38a6f8961cb8b3ef2fe879b27de704ab2f3d44e3 hash algorithm: friendly: sha256 value: 0xB attributes: friendly: authwrite|nt=0x1|authread value: 0x40044 size: 32 ``` 我们可以看到配置的属性[8]、大小以及基于公开属性哈希生成的名称。由于你定义了与我完全相同的属性,你将获得完全相同的哈希作为索引名称。 现在我们可以像使用真正的 PCR 一样使用 NV 索引,通过测量(例如用户 Alice 的登录事件)来扩展它: ```bash printf 'user-alice-logged-in' > m1.bin tpm2_nvextend 0x01000000 --input=m1.bin ``` 然后读取其值: ```bash tpm2_nvread 0x01000000 --size=32 | xxd -p -c 64 ``` ```text bd9927a653c6c33297b7d884a8ad99df0f5b5b1c1e1e86f762b3aced8bc77f50 ``` 现在再次检查 NV 索引,我们会发现有趣的现象:索引获得了新属性 `written`,表明它已被写入一次或多次。由于属性集更新了,而索引名称是包含属性的哈希,因此它也获得了新名称!我们稍后会利用这一点。 ```text 0x1000000: name: 000b1191942a636c11a57571f0d435c290a1955788229305ce0aa8a2f393e9ed770c hash algorithm: friendly: sha256 value: 0xB attributes: friendly: authwrite|nt=0x1|authread|written value: 0x20040044 size: 32 ``` 如果你愿意,可以再测量一次 Bob 的登录事件: ```bash printf 'user-bob-logged-in' > m2.bin tpm2_nvextend 0x01000000 --input=m2.bin tpm2_nvread 0x01000000 --size=32 | xxd -p -c 64 ``` ```text e758d6e2e54620fd45b9cd1fd57cbba62757f12751d549fd2fc957ed1908ed29 ``` 此时 NV 索引的值为 `HASH(HASH(0x0 || m1) || m2)`[9]。第二次测量后索引名称并未改变。 至此,我们获得了一个像 PCR 一样可扩展的 NV 索引。但它能作为 PCR 的替代品吗?不能。我们缺少真实 PCR 的一个基本属性:若要使用度量链作为证明,在系统运行期间它必须不可重置或回放!否则攻击者在获取系统访问权限后可以重置度量历史并回放未被篡改系统的历史记录,使攻击无法通过远程证明发现。 遗憾的是,TPM 并未为我们的类 PCR NV 索引提供此属性:我们在系统运行时定义了索引,同样也可以取消定义它: ```bash tpm2_nvundefine 0x01000000 --hierarchy=o tpm2_nvreadpublic ``` 基于本节的两个示例测量,如果 Alice 有恶意并获取 root 访问权限(从而获得所有者授权),他们可以简单地取消定义并重新定义索引,然后回放 Alice 从未登录过的历史记录。重新定义的索引将获得完全相同的名称,我们无法察觉。我们需要的是一个任何人都可以扩展,但在系统运行期间无人可以重启的索引。**策略**是 TPM 用于表达此类条件的工具。 ## 策略探索 TPM 接口提供的一个非常强大的概念是**策略**[10]。策略是一个不透明摘要,与受保护对象一起存储在 `authPolicy` 属性中(我们已在 NV 索引的公开属性中见过)。要满足策略,我们向 TPM 请求策略会话。每个会话都有自己的上下文,包含名为 `policyDigest` 的摘要和一组可通过执行策略断言修改的约束。新会话的 `policyDigest` 全为零。 在会话内,我们可以对 TPM 运行不同的策略命令,每个命令断言某个条件。有些断言在命令执行时立即检查;其他则延迟:命令在会话上下文中记录约束,TPM 仅在会话用于授权时检查。无论哪种方式,每个命令都会使用以下逻辑扩展会话摘要: `policyDigest := H(policyDigest_old || commandCode || command-specific args)` 这种扩展方案与 PCR 完全相同,尽管 `policyDigest` 并未由真实 PCR 支持。调用者可以呈现不同类型的证据,每种证据扩展 `policyDigest`。如果断言累加使会话的 `policyDigest` 与索引的 `authPolicy` 匹配,则会话被授权,可以使用它调用所需命令。 策略作者通过在受信环境中试用会话或离线预计算来计算预期的 `authPolicy` 摘要。试用会话运行相同的摘要计算,但不验证任何条件,作为交换,它不能用于授权任何操作。这使作者能够为机器当前未处的状态计算策略。 ### `PolicyPCR` 很酷的是,策略可以通过锁定 PCR 的预期值来使权限依赖于机器状态。让我们创建这样一个策略! 首先,启动试用会话来定义要设置在 NV 索引上的策略。通过调用 `tpm2_startauthsession` 且不带 `--policy-session` 标志来完成: ```bash tpm2_startauthsession --session=trial.ctx ``` 然后调用 `tpm2_policypcr` 创建针对当前观测到的 PCR 值(通过 `--pcr-list=` 选择)的断言。在我们的实验中,使用 PCR 15(一个操作系统拥有的 PCR)。应用 PCR 23 看似是合适的实验对象,但像调试 PCR 16 一样,它可在运行时重置——这正是我们试图摆脱的属性: ```bash tpm2_policypcr --session=trial.ctx \ --pcr-list=sha256:15 \ --policy=pcr15.policy ``` ```text 7e247a603cd1052cabc095741b8ee2f7458aabeee960b8ec97d7f090171a039a ``` 结果策略摘要被打印并写入 `pcr15.policy`[11]。然后结束会话: ```bash tpm2_flushcontext trial.ctx ``` 现在重新创建之前的 NV 索引,这次用刚创建的策略保护其写入访问权限: ```bash tpm2_nvdefine 0x01000000 \ --hierarchy=o \ --hash-algorithm=sha256 \ --attributes="nt=extend|authread|policywrite" \ --policy=pcr15.policy ``` 注意我们同时添加了 `--policy=` 标志和 `policyWrite` 属性。我们保留 `authRead` 不变。也有 `policyRead`,但通常限制谁可以扩展索引要有趣得多。 ```text 0x1000000: name: 000b14a6915e5830ff2b83ebc87cdf5ac481fb29637c246489bdab1bba19948bb729 hash algorithm: friendly: sha256 value: 0xB attributes: friendly: policywrite|nt=0x1|authread value: 0x40048 size: 32 authorization policy: 7E247A603CD1052CABC095741B8EE2F7458AABEEE960B8EC97D7F090171A039A ``` 现在尝试像之前那样扩展会因授权错误而失败: ```bash tpm2_nvextend 0x01000000 --input=m1.bin ``` ```text ERROR: Esys_NV_Extend(0x12F) - tpm:error(2.0): authValue or authPolicy is not available for selected entity ``` 相反,要写入索引,我们需要启动策略会话并通过呈现当前 PCR 15 状态[12]来满足策略。之后,我们可以执行扩展,将会话作为授权呈现。并记得最后刷新会话上下文。 ```bash tpm2_startauthsession --session=s.ctx --policy-session tpm2_policypcr --session=s.ctx --pcr-list=sha256:15 tpm2_nvextend 0x01000000 \ --hierarchy=0x01000000 \ --auth=session:s.ctx \ --input=m1.bin tpm2_flushcontext s.ctx ``` 如果 PCR 15 推进,策略将无法再满足。运行以下命令扩展 PCR: ```bash echo something | tpm2_pcrevent 15 ``` 现在重试之前基于 PCR 解锁会话的三步操作。扩展会因 `tpm:session(1):a policy check failed` 而失败,因为 PCR 已推进,其当前值(及任何未来值)不再匹配策略中包含的值。对 PCR 绑定索引的访问权限已过期,在机器运行期间无法恢复。 最后,取消定义索引: ```bash tpm2_nvundefine 0x01000000 --hierarchy=o ``` 使用 `PolicyPCR` 锁定 PCR 非常酷,但它也很脆弱[13]:在上一节末尾,度量值改变了,我们的访问权限随之过期且无法恢复。这是个问题,因为 PCR 值常因合法原因变化。想想引言中的无密码磁盘解锁:磁盘密钥密封在启动链的 PCR 状态下,下次内核更新会改变该状态。磁盘将无法解锁,尽管没有发生任何坏事,我们当然不希望每次更新都重新加密磁盘。 我们更需要的是一个在批准状态变化时保持稳定的策略。幸运的是,还有另一种可用于创建策略的机制:**`PolicyAuthorize`**。它引入了一层间接和委派,允许我们使用公钥而非系统状态创建策略。要授权,你需要...

相似文章

Qualcomm NPU 编译器的逆向工程

Lobsters Hottest

逆向工程 Qualcomm NPU 编译器揭示了未文档化的 VTCM 内存管理、基于 MILP 的布局、自动精度更改,以及一个用于边缘部署优化的隐藏分析模拟器(Hextimate)。

NX位不仅仅关乎安全

Lobsters Hottest

一位开发者描述了在postmarketOS的ARM64裸机虚拟机管理程序中调试一个复杂bug的过程,涉及指令缓存一致性问题和与NX位相关的硬件特定行为。