Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第八章 · PCR 7 的盲区:Evil Maid 攻击与 Secure Boot 的意义

第六章结尾我们留了一个悬念:把 LUKS 卷密钥绑到 PCR 7 之后,开机确实自动解锁了,但 PCR 7 到底度量了些什么?如果有人趁你不在动过 /boot 里的文件,TPM 会拒绝给密钥吗?

答案是:不会。而且不是“碰巧不会“,是“在设计上就不归它管“。

这一章把这个盲区彻底推演清楚。它是全书思辨性最强的一章,理解透了,你就明白为什么 Secure Boot(安全启动)这个常被嫌麻烦、甚至被人教你怎么关掉的东西,在可信启动体系里是必需品而不是摆设。

本章目标

  • 搞清楚 PCR 7 里到底度量了什么,为什么它管不着内核和 initramfs
  • 对着第六章的实验环境,完整推演一次 Evil Maid(恶意的女仆)攻击
  • 讲透“度量只是记录,不是拒绝“这个最容易被误解的认知
  • 比较两种防御哲学:预防(Secure Boot)与检测(systemd-pcrlock)
  • 可选:在实验虚拟机上亲手复现一次攻击,看着防御失效

背景知识:PCR 分工速查表

先回顾 PCR 的分工。不同固件的度量细节有差异,下面这张表以我们的实验环境(OVMF 固件 + Limine 引导 + UKI + systemd)为准,对应第五章里你重放过的那份度量日志:

PCR度量内容谁度量
0OVMF 固件代码与配置固件
4Limine、UKI 镜像(引导管理器及其加载的内核映像)固件 / bootloader
5GPT 分区表、启动配置变化固件
7SecureBoot 开关、PK / KEK / db / dbx 密钥库固件
11UKI 各段(内核、initramfs、cmdline 等)+ systemd 启动阶段标记systemd

注意:这张表不是从标准文档抄来的理想分工,而是实验环境实测出来的。换一台机器、换一个固件,同一编号的 PCR 里装的东西可能不同。所以第五章练的重放功夫不是白练的——想知道某台机器的 PCR 里有什么,去读它自己的 event log。

盯着 PCR 7 那一行多看几秒:它度量的是 Secure Boot 的验证规则和密钥库,而不是被验证的代码本身。这个区别是整章的钥匙。

PCR 7 里到底有什么

翻开第五章你重放过的日志,PCR 7 对应的事件类型是 EV_EFI_VARIABLE_DRIVER_CONFIG(EFI 驱动配置变量)。用 tpm2_eventlog 过滤出来看,大致是这样的四笔:

- EventNum: ...
  PCRIndex: 7
  EventType: EV_EFI_VARIABLE_DRIVER_CONFIG
  ...
  UnicodeName: SecureBoot
- EventNum: ...
  PCRIndex: 7
  EventType: EV_EFI_VARIABLE_DRIVER_CONFIG
  ...
  UnicodeName: PK
- EventNum: ...
  PCRIndex: 7
  EventType: EV_EFI_VARIABLE_DRIVER_CONFIG
  ...
  UnicodeName: KEK
- EventNum: ...
  PCRIndex: 7
  EventType: EV_EFI_VARIABLE_DRIVER_CONFIG
  ...
  UnicodeName: db

一笔一个变量:

  1. SecureBoot 变量:Secure Boot 开关的值(0 或 1)
  2. PK(Platform Key,平台密钥):平台的最高信任根
  3. KEK(Key Exchange Key,密钥交换密钥):用于更新 PK 和签名数据库
  4. db / dbx(签名数据库与吊销列表):哪些签名被信任、哪些被拉黑

注意看 EventType——它是“变量配置“,度量的动作发生在固件读取这些变量的时刻,而不是发生在“用这些变量验签某个文件“的时刻。固件度量的是这几个变量的内容哈希,和“我用这些规则验证过的那些文件“没有任何关系。所以 PCR 7 能回答的问题只有一个:

“Secure Boot 的配置和密钥库,和绑定那一刻相比,变了吗?”

它完全不知道、也不关心 Limine 是什么版本、内核是哪个构建、initramfs 里打了什么包——这些东西的哈希一个都不在 PCR 7 里。

而我们的实验环境 Secure Boot 是关着的(第二章为了折腾方便就没开)。也就是说 PCR 7 里躺着的是“SecureBoot=0 加一套默认密钥库“的哈希,一个基本静止的值。

Evil Maid 攻击:完整推演

现在把攻击走一遍。Evil Maid 得名于一个经典场景:你把笔记本留在酒店房间,清洁人员(或者任何能物理接触设备的人)趁你出门动了它。前提假设:攻击者有物理接触,但没有你的任何口令。

攻击面:明文的 /boot

回顾第六章实验环境的分区布局:

/dev/vda
├── vda1  /boot(vfat,明文,不在 LUKS 里)
└── vda2  /(LUKS 全盘加密,TPM 自动解锁)

/boot 为什么必须明文?因为固件要读它。OVMF 从 vda1 加载 Limine 和 UKI(Unified Kernel Image,统一内核映像)的时候,LUKS 还没解锁、TPM 也还没给密钥——这个阶段的磁盘内容对固件必须是可读的明文。

推论是:vda1 上的 UKI,任何能把磁盘挂到自己机器上的人都能读、能改。 LUKS 保护了根分区,但没有保护启动文件。

攻击动作

酒店场景,攻击者拿到 10 分钟物理接触:

  1. 引导一个自己带的 U 盘系统(或直接拆盘镜像)
  2. 把 vda1 上的 UKI 换成动了手脚的版本——比如在 initramfs 的启动脚本里加一行:拿到卷密钥解锁根分区之后,把密钥 curl 到攻击者的服务器;或者更经典地,植入一个 rootkit 再正常挂载根分区,什么都不耽误
  3. 原样放回,走人

整个过程不需要任何口令,因为 vda1 是明文。

期望 TPM 救命,但落空了

你回到酒店,开机。此时我们“期望“的安全链条是:

  • OVMF 加载被篡改的 UKI,会度量它 → PCR 4 和 PCR 11 的值变了 ✓(这一步确实发生,度量系统是忠实的)
  • TPM 发现 PCR 值和绑定时不一致 → 拒绝释放 LUKS 密钥 ✗

第二步永远不会发生。回忆第六章绑定时的那条命令:

sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 /dev/vda2

--tpm2-pcrs=7,白纸黑字:我们告诉 TPM,解锁的条件有且仅有 PCR 7。于是:

  • Secure Boot 还是关着的,没人去动它
  • PK / KEK / db / dbx 一个字节都没变
  • 所以 PCR 7 的值和绑定那一刻一模一样
  • TPM 核对策略:PCR 7 匹配,密钥照给

思考:这个场景里 TPM 出错了吗?没有,它忠实执行了你给的策略:“PCR 7 是这个值就放密钥”。度量系统出错了吗?也没有,PCR 4/11 忠实地记下了篡改。问题出在策略声明的关心范围里根本不包括 UKI。

攻击成功

恶意 initramfs 从 TPM 拿到卷密钥,解开根分区,攻击者的代码在一个完全解密的系统上运行。密钥外泄、rootkit 落地,都发生在系统正常启动的掩护下。

更隐蔽的版本:恶意 initramfs 拿到密钥、外泄之后,链式加载(kexec 或直接转交)回正版 initramfs,系统照常进桌面。你什么异常都看不到,而密钥已经在攻击者手里。

复盘整条链

整个链条里每个组件都“尽职尽责“:TPM 忠实执行了解封策略,度量系统忠实记录了篡改(PCR 4/11 确实变了),LUKS 忠实地用拿到的密钥解了密。

锁的钥匙,挂在了一扇没装锁的门上。

关键认知:度量只是记录,不是拒绝

这是本章最重要的一句话,值得单独讲透。

PCR 值的变化本身不阻止任何事。 PCR 是个只增不改的日志本(第三章说过,PCR 不能随意重置、只能 extend),它的职责是留证据,不是当保安。恶意代码被度量进了 PCR 4,恶意代码照样跑——度量和执行是两条平行的线。

真正起“门禁“作用的是你绑在 PCR 上的策略。当你把密钥 Seal 到 PCR 7,你实际是在向 TPM 声明:

“我只在乎 PCR 7 这份证据。其他 PCR 记得再准,与我无关。”

PCR 4/11 把篡改记录得清清楚楚,但你的策略没有引用它们,所以这些证据在解锁决策中被完全无视。篡改发生了、证据留存了、防御失效了——三位一体,互不干扰。

:很多人(包括我一开始)默认“绑了 TPM 就等于防篡改“。这是把“度量“和“度量 + 策略覆盖“混为一谈。防不防得住,取决于你的策略引用了哪些 PCR,以及那些 PCR 里度量的是什么。引用之前,先搞清楚里面有什么。

那绑 PCR 4/11 行不行?

既然 PCR 4/11 里有 UKI 的哈希,把密钥绑到它们上面,Evil Maid 不就解不开了吗?

行,攻击者确实解不开了。但你很快会发现自己也解不开了。

PCR 4/11 度量的是内核、initramfs、cmdline 这些每次升级都会合法变化的东西。Arch 上滚一次 pacman -Syu,内核更新了、initramfs 重建了,下次开机 PCR 值就变了——TPM 拒绝给密钥,你被锁在自己的系统门外。

除非你每次升级前都能预测新值、预先重绑(这正是下面哲学二干的事),否则这种绑定在生产上不可维护。这也是第六章选 PCR 7 的原因:不是因为它最强,而是因为它最稳——稳到连篡改都“稳定“地检测不到。

两种防御哲学

盲区讲清楚了,怎么补?业界有两条思路,哲学完全不同。

哲学一:预防——Secure Boot

思路:让攻击动作本身失败,而不是事后检测到。

Secure Boot 开启后,固件只执行带有合法签名的 EFI 文件。攻击者换上的恶意 UKI 没有你的签名,OVMF 直接拒绝加载——攻击在第一步就死了,根本轮不到 TPM 出场。

此时绑 PCR 7 反而就够了。PCR 7 绑定的语义变成了“Secure Boot 必须开着,且密钥库必须是我的“。逐条推演攻击者的选项:

  • 跑未签名的恶意 UKI? 固件验签失败,拒绝执行。攻击失败。
  • 进固件设置把 Secure Boot 关掉? SecureBoot 变量的值变了 → PCR 7 变化 → TPM 拒绝给密钥。攻击失败。
  • 换掉密钥库,用自己的密钥给恶意 UKI 签名? PK / KEK / db 的内容变了 → PCR 7 变化 → TPM 拒绝。攻击失败。

三条路全部堵死。而且内核升级不影响 Secure Boot 密钥库,PCR 7 保持稳定,升级友好。这就是为什么生产中普遍选 PCR 7:它不是偷懒,而是和 Secure Boot 配套使用时刚刚好——一个管“规则没被绕过“,一个管“代码必须合法“。

注意:这里有个隐含前提——固件设置的入口要可靠(比如设了固件口令)。否则攻击者进 BIOS 关 Secure Boot、顺手清掉 TPM 重新绑定,PCR 7 这条防线也就没了。纵深防御,没有哪一层是孤立的。

哲学二:检测 + 可预测度量——systemd-pcrlock

思路:接受 PCR 11 会合法变化,但让变化变得可预测

PCR 11 度量的 UKI 各段是可以预先计算的:升级前你已经知道新内核、新 initramfs 的内容,就能算出升级后 PCR 11 会是什么值。systemd-pcrlock(systemd 253 之后引入)干的就是这件事——升级时预测新的 PCR 11 值并预先重绑,让新旧值都能解开。

这条路线比 Secure Boot 更细:initramfs 的内容、内核 cmdline 的每个字节都纳入绑定范围,连“只改 cmdline 注入 init=/bin/bash“这种不动任何文件的攻击都能防住。代价是工具链复杂,升级流程和绑定流程耦合,对滚动发行版的运维是不小的负担。

业界参照

Windows BitLocker 的经典做法是混合绑定:PCR 0 + 2 + 4 + 11,再叠加 Secure Boot。固件、引导链、内核映像全在绑定范围里,由 Secure Boot 保证这些组件不会被非法替换,由 PCR 绑定保证验证规则不会被关闭。两条哲学它都要。

把两条哲学并排看一眼:

哲学一:Secure Boot + PCR 7哲学二:systemd-pcrlock + PCR 11
防御时机事前预防,恶意代码跑不起来事后检测,篡改留下证据并拒绝解密
覆盖粒度整条启动链(凡未签名皆拒)UKI 各段逐字节,含 cmdline
内核升级无感,PCR 7 不动需要升级前预测并重绑
运维复杂度低,一次配置长期有效高,升级与绑定流程耦合

没有标准答案,只有你愿意为多大的覆盖面付多大的运维成本。

可选动手:亲手看一次防御失效

百闻不如一见。在实验虚拟机上,这次攻击五分钟就能复现:

# 先备份!玩坏了 /boot 里唯一的 UKI 就进不了系统了
sudo cp /boot/EFI/Linux/arch-linux.efi /boot/EFI/Linux/arch-linux.efi.bak

# 记下当前的 PCR 4 和 PCR 7
sudo tpm2_pcrread sha256:4,7

然后模拟 Evil Maid,把 UKI 改动一个字节(追加一段垃圾即可):

echo "evil maid was here" | sudo tee -a /boot/EFI/Linux/arch-linux.efi

重启,观察三件事:

  1. 系统照常自动解锁,正常进桌面——TPM 给了密钥,LUKS 解开了,一切“正常“
  2. 再读一次 PCR,PCR 4 的值和改之前不一样了——篡改被忠实记录
  3. PCR 7 纹丝不动——绑定策略没有感知到任何事
sudo tpm2_pcrread sha256:4,7
sha256:
  4 : 0x...(和改之前不同的值)
  7 : 0x...(和改之前完全相同的值)

篡改发生、证据留存、防御失效,三位一体。这就是你在第六章搭起来的系统的真实防御水位。玩完记得把备份的 UKI 还原回去再重启,别带着一个“被攻击过“的系统继续往下做实验。

Secure Boot 实操路线(简述)

既然 Secure Boot 是正解,为什么第六章没开?因为在虚拟机上完整配置它要另外一番功夫,本书不展开,只给路线图,做过功课的读者可以自行实践:

  1. sbctl create-keys 生成自己的 PK / KEK / db
  2. sbctl enroll-keys 把自有密钥注入 OVMF 的固件变量(OVMF 需处于 Setup Mode)
  3. sbctl sign 给 Limine 和所有 UKI 签名
  4. 在 OVMF 设置里开启 Secure Boot
  5. PCR 7 变了(SecureBoot 从 0 变 1,密钥库从默认变成你的)——必须重新 enroll LUKS 密钥,把新的 PCR 7 值绑进去

第 5 步本身就是个很好的印证:开启 Secure Boot 后 PCR 7 的变化,恰好说明它度量的是“规则“——规则换了,值就变。

小结

  • PCR 7 度量的是 Secure Boot 的配置和密钥库,即“验证规则“,不含内核 / initramfs 的任何哈希
  • Evil Maid 攻击利用明文的 /boot 替换 UKI;只绑 PCR 7 的系统会照常交出密钥——TPM 没出错,是策略的关心范围太窄
  • 度量只是记录,不是拒绝;PCR 变了不阻止任何事,只有策略引用了那份证据,它才起作用
  • 直接绑 PCR 4/11 防得住攻击但扛不住日常升级;两条现实路线:Secure Boot + PCR 7(预防,升级友好),或 systemd-pcrlock + PCR 11(检测,预测式重绑,覆盖更细)
  • Secure Boot 不是可以顺手关掉的麻烦,而是 PCR 7 绑定策略成立的前提

下一章换一个方向:启动链到内核为止已经讲完了,但系统跑起来之后,每次执行的程序、加载的库谁来度量?这就是 IMA 登场的地方。