Menci's Blog
念念不忘,必有回响
基于 TPM 2.0 与 Secure Boot 的 LUKS 自动解密
  1. 1. 前置知识
    1. 1.1. Secure Boot
    2. 1.2. TPM 2.0
    3. 1.3. LUKS
  2. 2. 实践
    1. 2.1. 准备分区与加密
    2. 2.2. 配置 Secure Boot
      1. 2.2.1. 生成 MOK 证书
      2. 2.2.2. 安装 shim
      3. 2.2.3. 签名并安装 GRUB
      4. 2.2.4. 签名内核
    3. 2.3. 配置 TPM
      1. 2.3.1. 自动解密
    4. 2.4. 系统更新
    5. 2.5. 工具脚本
  3. 3. 攻击面
    1. 3.1. 冷启动攻击
    2. 3.2. Evil Maid 攻击
  4. 4. 参考资料

为了保护用户数据,现代设备与操作系统往往都支持全盘加密的功能,其中最为典型的是 Microsoft Windows 的 BitLocker。它既可以对系统分区进行加密,又可以在确认引导过程可信的情况下,不需要用户交互即可自动解密,这利用到了 TPM 和 Secure Boot。而在 GNU/Linux 系统下,如果用常用的 LUKS 加密方案进行全盘加密,则需要在每次开机时输入密码。

本篇文章将参考 BitLocker 的做法,利用 Secure Boot 和 TPM 2.0 来实现 LUKS 全盘加密的自动解密。

前置知识

Secure Boot

Secure Boot(安全启动)是 Microsoft 在 UEFI 中引入的一项安全功能,旨在防止恶意代码在进入操作系统之前被加载,破坏操作系统的安全。

Secure Boot 的原理是信任链,其信任根是 UEFI 中的 CA 证书,这里 CA 证书可以内置在 UEFI 固件中,也可以由机器所有者手动设置。在开启 Secure Boot 时,UEFI 固件在加载 EFI 可执行文件前,会首先检验其签名,如果其签名不是来自于 CA,则会拒绝加载。一般主板的 UEFI 固件均内置 Microsoft 的两个 CA 证书,分别为:

  • Microsoft Windows Production PCA 2011,为 Windows 引导程序签名
  • Microsoft Corporation UEFI CA 2011,为 Linux 相关的引导程序签名

Microsoft 为多个 Linux 发行版的引导程序提供了签名,如 Ubuntu、Debian 和 Fedora 等。除此之外,Microsoft 也为 PreLoader 和 shim 两个特殊的引导程序提供了签名,他们的作用是在不修改设备 CA 的情况下,让用户通过安装自己的证书来加载由用户自己签名的其他引导程序。这种证书叫做 MOK(Machine Owner Key)证书。有了 PreLoader / shim 与 MOK,用户可以方便地让系统信任用户自己的引导程序。

Secure Boot 仅提供对第一阶段引导程序的签名检验,而之后的代码检验则交给被加载的引导器,以及被引导器加载的系统内核所完成。在 Secure Boot 开启时,UEFI 会提供一个接口来获得当前设备上的可信证书列表,包含 CA 证书和(由 PreLoader / shim 管理的)MOK 证书,以便后续的检验过程使用。例如,默认情况下,GRUB 在 Secure Boot 开启时,会使用 UEFI 提供的证书列表检验其所加载的内核的代码签名。内核也可以被配置为检验其加载的 .ko 模块文件的签名。

提示

除第一步 UEFI 固件对第一阶段引导程序的签名检验强制使用 UEFI 中的 Secure Boot CA 证书外,之后的过程如何校验均不是受 UEFI 所控制的。即使 UEFI 提供了通用的接口来允许引导程序或操作系统获得其信任的证书列表(包括 CA 和 MOK),第一阶段之后的引导程序仍然可以不使用这些证书,或者使用这些证书外加其自身信任的证书来进行代码签名检验。例如,UEFI 本身不信任 MOK,但被 UEFI 信任的 PreLoader / shim 信任 MOK,所以使用 MOK 签名的引导程序可以被 PreLoader / shim 加载。

另一个典型的例子是,Linux 内核在编译时,可以使用一个自有的证书对随内核编译的模块进行签名,并将该证书内置在编译好的内核中,自动信任这些一同编译的模块,而同时也会信任用户自行编译并使用 UEFI CA / MOK 签名的模块。

TPM 2.0

TPM(Trusted Platform Module,可信平台模块)是一种安全芯片,提供了硬件级别的一些安全相关的功能。TPM 可以在内部实现不受恶意软件干扰的密码学操作。早年的 TPM 一般作为独立的模块,插入到计算机主板上的专用接口上,而近年的 CPU 往往内置了 TPM 的功能(如 Intel PTT 与 AMD fTPM),不再需要独立硬件。TPM 的规范由 TCG(Trusted Computing Group,可信计算组织)制定,目前最为广泛使用的版本是 2.0。

本文中,我们主要用到 TPM 2.0 的完整性测量(Integrity Measurement)的功能。TPM 中定义了一些 PCR(Platform Configuration Register,平台配置寄存器),用于在系统引导过程中记录相关的配置。在 GRUB + Linux 的环境中,常用的一些 PCR 的所测量的值如下:

PCR 编号测量值备注
0UEFI BIOS 固件代码当升级 BIOS 固件时变化
1UEFI BIOS 固件配置当修改 BIOS 设置时变化,改回后会恢复
2Option ROM 固件代码一些硬件设备(如 PCIe 设备)在启动时执行的代码
3Option ROM 固件配置
4UEFI 引导程序代码即引导过程中加载的 .efi 可执行文件
(由 UEFI 设置,shim 也会设置
5GPT 分区表
6S4/S5 挂起恢复状态
7Secure Boot 配置关闭 Secure Boot 或修改 MOK 后会变化
8GRUB 命令与内核参数由 GRUB 设置
9GRUB 读取的所有文件
(包括 vmlinuzinitrd
由 GRUB 设置

由于这些值均是在引导过程中由固件/引导器/操作系统的代码所设置,为了确保这些值不被篡改,TPM 所定义的唯一修改 PCR 的接口是 ,其中:

其中 表示二进制或,哈希函数目前可选 SHA-1 与 SHA-256。这种算法使得 PCR 中的测量值的变化是不可预测且不可逆的,在可信的引导组件将测量值正确地传递给 TPM 后,恶意代码即使已知期望的 PCR 测量值,也无法将其写入 TPM,从而无法欺骗 TPM 的完整性测量。

在这样的前提下,TPM 提供了安全存储密钥的功能。操作系统可以指定一系列 PCR 寄存器的编号,令 TPM 通过这些 PCR 目前的测量值计算出检验值,并将检验值和密钥持久化存储。在之后的引导中,操作系统可以再次告知 TPM 所检验的 PCR 值列表,当 TPM 验证这些 PCR 的当前测量值的校验和与之前保存的一致时,才会向 CPU 输出密钥,否则不会输出密钥。这样一来,当攻击者植入恶意代码,或者修改固件配置,导致引导阶段的完整性被破坏时,处于安全威胁中的操作系统将无法获得密钥。

LUKS

LUKS(Linux Unified Key Setup,Linux 统一密钥设置)是 Linux 下最常用的磁盘加密方案。LUKS 基于 DM(Device Mapper,设备映射器)的 dm-crypt 功能,将被加密块设备解密后的内容映射为虚拟块设备,对文件系统透明。LUKS 提供了多种加密算法与密钥派生函数,目前的默认值是 AES-XTS-512Argon2i。LUKS 有 8 个密钥槽,即可以分别设置 8 个解密密码。

密钥派生

PBKDF(Password-Based Key Derivation Function,基于密码的密钥派生函数)是一种哈希函数,被用于将用户设置的便于记忆的密码转换为加密算法接受的固定长度密钥。

加密算法一般接受长度为 256 或 512 位的密钥,而用户设置的密码复杂度一般在 10 个可见字符左右,包含的信息熵远低于加密算法所接受的密钥长度。如果使用一般的哈希函数(如 SHA-512),由于其计算速度较快,穷举密码的复杂度远远小于穷举密钥,将会降低整个加密系统的安全性。所以,为了抵抗穷举密码攻击,密钥派生函数往往被设计为单次计算需要消耗较多时间。于此同时,一些密钥派生函数会被设计为计算时需要占用较大的内存,来加大并行计算的难度。

当密码的信息熵足够大时(如直接使用随机生成的 512 字节密钥文件作为密码),密码与派生后密钥的穷举空间相同,此时不需要使用密钥派生函数。但在 LUKS 中无法关闭密钥派生这一步骤,只能使用相关参数降低密钥派生的计算复杂度。

为了实现包括根分区在内的全盘加密,常用的各个发行版均在 initrd 中加入了在引导过程中进行解密的支持。initrd 中的脚本会解析 /etc/crypttab 文件,加载其中的加密分区,用户可以设置解密时要求输入密码,或通过命令读取密码。本文将会实现在引导时使用储存在 TPM 中的密钥作为密码进行解密,并同时支持输入由用户设置的密码进行解密。

实践

本文的实践部分基于 Debian 11,其他发行版可能需要修改一些命令和软件包名称。

在进行系统外的离线操作时,推荐使用 Arch Linux 安装镜像,其中包含了大部分维护工具和 arch-chroot 命令。

注意

由于 Arch Linux 不使用 /usr/sbin 目录,所以该路径不会包含在 Arch Linux 安装镜像中的默认 $PATH 中。所以使用 chroot 进入一个 Debian / Ubuntu 根文件系统后,一些命令会找不到,此时需要将该路径手动添加到 $PATH 中:

PATH=$PATH:/usr/sbin

如果目标文件系统是 ZFS,则需要安装 archiso-zfs

准备分区与加密

在一个加密的系统上,硬盘分区分为两部分:加密的部分和未加密的部分。加密的部分的分区是可信的,而未加密的部分分区是不可信的,需要在引导过程中被检验。我们可以将硬盘分为三个分区(按顺序):EFI 引导分区,/boot 分区和需要加密的数据分区(下简称数据分区),并在加密数据分区上使用 LVM(Logical volume Manager,逻辑卷管理)来管理逻辑分区。当然,LVM 不是必须的,如果不需要对加密部分进行分区,可以直接将加密空间作为一个根分区来使用。如果原本没有 EFI 分区,说明是使用 CSM 引导,此时需要参考其他教程迁移到 EFI 引导。如果原本没有 /boot 分区,则需要调整分区后手动创建,将原本 /boot 内容移动进去,并加入到 /etc/fstab 中。

注意

一些教程会将 /boot 分区一并加密,这会使得配置过程更加繁琐。在我们的设置中,保持 /boot 未被加密并不会降低安全性,因为在调用 TPM 获取密钥进行自动解密时,TPM 会确保启动链未被篡改。而即使加密了 /boot,在启动链未被验证的系统上,恶意代码仍然能够被通过 EFI 注入。

对于已安装的系统,我们需要将现有的数据分区备份,最简单的方式是使用 dd 命令复制块设备。注意 LUKS 加密会占用一部分空间用于存储元数据,所以备份原分区前需要先将其缩小,推荐的步骤是:

  1. 将原分区的文件系统缩小 2 GiB(使用 pvresizebtrfs filesystem resizeresize2fs 等命令)
  2. 将原分区的分区删除,并在原位置重建,重建时大小缩小 1 GiB(使用 cfdisk 等管理工具)
  3. 将新分区的大小自动扩大到文件系统大小(使用第一步相同的命令,通常默认参数会自动扩大到文件系统大小)

这种方式可以避免计算和在命令中填写具体大小,同时留有一定的余量,方便调整/创建 EFI 和 /boot 分区。

将原有的 EFI 和 /boot 中的数据备份(如果没有多系统引导则不需要备份 EFI 分区),并将缩小后的数据分区备份,之后对硬盘进行安全擦除,防止加密前的数据被恢复。擦除后重新建立分区表,推荐为 EFI 分配 128 MiB(如果不使用 EFISTUB),为 /boot 分配至少 1 GiB,剩余的空间作为加密数据分区。

假设为加密数据准备的分区是 /dev/sda3,执行以下命令创建 LUKS 加密分区(注意输入大写的 YES):

cryptsetup luksFormat /dev/sda3

WARNING!
========
This will overwrite data on /dev/sda3 irrevocably.

Are you sure? (Type 'yes' in capital letters): YES
Enter passphrase for /dev/sda3:
Verify passphrase:

创建加密分区后,将加密分区打开。打开加密分区时需要指定一个名称。成功打开后,系统将在 /dev/mapper/ 下创建解密后的块设备,例如指定名称为 lvm-encrypted

cryptsetup open /dev/sda3 lvm-encrypted
Enter passphrase for /dev/sda3:

提示

为了提高加密后 SSD 的 I/O 性能,可以禁用工作队列,执行以下命令(选项将保存在 LUKS 元数据中):

cryptsetup --perf-no_read_workqueue --perf-no_write_workqueue --persistent refresh lvm-encrypted

将备份好的数据分区使用 dd 恢复到该块设备中:

dd if=/path/to/backup/drive/bak.img of=/dev/mapper/lvm-encrypted status=progress bs=128M

此时,对于 LVM,可以使用 vgscan 命令识别原有的卷组,并使用 vgchange -ay <vgname> 激活。

由于我们缩减文件系统时留下了较大的空闲空间,此时可以将文件系统大小调整到到 LUKS 创建的块设备的大小上限。同理使用 pvresizebtrfs filesystem resizeresize2fs 等命令即可完成。

将根文件系统挂载。如果 /boot 分区是新创建的,需要从原有的根分区中将原有的文件移动过去。重新创建的 EFI 和 /boot 分区的 UUID 会与原有的不同,需要添加或者修改 /etc/fstab 中的条目。最简单的方式是将 /mnt/boot/mnt/boot/efi 挂载后,使用 genfstab -U /mnt 命令生成(对于加密分区或者 LVM 分区,建议不使用 UUID,而是使用 /dev/mapper/<name> 挂载)。

之后执行 arch-chroot /mnt,进入加密系统的根文件系统中执行。要使得加密后的系统能够正常引导,需要告知系统在 initrd 中打开加密分区。在根文件系统中创建 /etc/crypttab 文件:

lvm-encrypted UUID=72fa928c-4df7-6448-ae04-11c909ff071e none luks,discard

文件的每一行共有四列,分别表示名称、块设备(请将 UUID 替换为加密分区的文件系统 UUID)、密钥文件(none 表示不使用固定的密钥解密)和选项。选项中的 discard 表示启用 SSD TRIM,启用后可能会导致一些测信道信息泄露,例如其中文件系统的类型和用量可能能够被从被丢弃块的特征中推算出来,但这通常不被认为是一个安全威胁。

ZFS 注意事项

在构建 initrd 时,cryptsetup 的脚本会通过 /proc/mounts 中的信息判断当前系统的根目录属于哪一个加密块设备,进而将对应的 /etc/crypttab 条目放入 initrd 中,在引导时进行解密。但由于 ZFS 根目录不是通过块设备挂载的,所以 update-initramfs 会提示找不到根目录所对应的加密设备条目。

此时需要手动在 /etc/crypttab 的对应条目中加入 initramfs 选项,将其指定为在引导时进行解密(即改为 luks,initramfs,discard)。

如果 cryptsetup 命令不存在,则需要手动安装 cryptsetup 包,否则直接运行 update-initramfs -u -k all 重建 initrd 即可。

如果原有的引导方式为 EFI GRUB,则此时重建 GRUB 后即可正常引导原有系统,系统会在开机时提示输入密码解密 LUKS 分区。接下来的步骤可以在 chroot 中完成,也可以在成功引导的系统中完成。

配置 Secure Boot

首先,安装 shim-signedsbsigntoolmokutil 包,前者包含带有 Microsoft 签名的 shim 二进制文件,后两者是用于签名和管理设备上的 MOK 证书。

生成 MOK 证书

创建一个目录,为安全起见设为仅 root 可访问,使用 openssl 命令生成自签名 MOK 的证书与私钥(其中 CN= 之后的名字可以自定义):

mkdir -p /var/lib/shim-signed/mok/
chmod 700 /var/lib/shim-signed/mok/
cd /var/lib/shim-signed/mok/
openssl req -nodes -new -x509 -newkey rsa:2048 \
              -keyout MOK.key -outform DER -out MOK.der -days 36500 \
              -subj "/CN=My Secure Boot MOK/"
openssl x509 -inform der -in MOK.der -out MOK.pem

上述命令生成了 DER 格式的证书,以及 PEM 格式的私钥和证书,因为 mokutil 需要 DER 格式,而 sbsign 需要 PEM 格式。

使用 mokutil 请求将 MOK 证书导入到系统 PKI 中,这里需要设置一个一次性的密码,以便于 shim 在启动时能够验证证书是由可信的管理员所请求添加的(注意,只有在 shim 被加载时,MOK 与 MOK 安装请求才会生效):

mokutil --import MOK.der
input password:
input password again:

安装 shim

将 shim 安装到 EFI 分区中:

rm -rf /boot/efi/EFI/BOOT && mkdir -p /boot/efi/EFI/BOOT
cp /usr/lib/shim/shimx64.efi.signed /boot/efi/EFI/BOOT/BOOTx64.EFI
cp /usr/lib/shim/mmx64.efi.signed /boot/efi/EFI/BOOT/mmx64.efi

为 shim 创建 EFI 引导条目,并设为默认(假设 EFI 分区是 /dev/sda1):

efibootmgr --unicode --disk /dev/sda --part 1 --create --label "Shim" --loader /EFI/BOOT/BOOTx64.EFI

此时,开机时将首先加载 BOOTx64.EFI,该文件是 shim 的主程序,它会检测是否有 MOK 安装请求,如果是则会调用 MOK 管理界面 mmx64.efi 进行处理,否则会默认引导 grubx64.efi 程序。接下来我们需要安装 GRUB 到 grubx64.efi,来让 shim 能够找到它。

签名并安装 GRUB

如果没有安装 GRUB(或未安装 EFI 版的 GRUB),需要先安装。Debian 下的包名为 grub-efi-amd64-bin

由于在 Secure Boot 开启时,GRUB 不允加载文件系统上的任何模块,所以所有需要的模块都要被打包进 grubx64.efi 中。Ubuntu 提供了一个列表,默认情况下,将列表中的模块全部加入即可(找不到的模块可以删除)。假设 GRUB 模块列表在 GRUB_MODULES 环境变量中。

另一方面,shim 要求其加载的 EFI 程序具有 SBAT 段。GRUB 安装器可以在生成 EFI 程序时直接指定 SBAT 文件,创建 /tmp/sbat.csv 保存为以下内容:

sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md
grub,1,Free Software Foundation,grub,2.04,https://www.gnu.org/software/grub/

然后安装 GRUB 到目标位置(安装时不需要修改 EFI 设置)并使用 sbsign 对其进行签名:

grub-install --efi-directory=/boot/efi/ \
               --sbat=/tmp/sbat.csv \
               --modules="$GRUB_MODULES" \
               --no-nvram \
               --no-bootsector \
               --no-uefi-secure-boot \
               --bootloader-id=BOOT
update-grub
sbsign --key MOK.key --cert MOK.pem --output /boot/efi/EFI/BOOT/grubx64.efi /boot/efi/EFI/BOOT/grubx64.efi

签名内核

由于 GRUB 也会验证内核的签名,我们同样也需要对内核进行签名:

for KERNEL in /boot/vmlinuz*; do sbsign --key MOK.key --cert MOK.pem --output "$KERNEL" "$KERNEL"; done

重启并开启 Secure Boot,此时 shim 应当会被加载,并出现 MOK 管理界面。安装 MOK 后再次启动,shim 会加载带有签名的 GRUB,并由 GRUB 加载带有签名的内核。

内核模块签名

对于开启了模块签名检验CONFIG_MODULE_SIG)的内核,在开启 Secure Boot 的平台上引导时,仅允许加载带有有效签名的模块。内核在检验模块签名时,会同时信任内核编译时使用的证书和 UEFI 提供的可信证书(包含 MOK 证书)。所以,一般来说,随内核提供的模块不需要手动进行签名(由 CONFIG_MODULE_SIG_ALL 控制,编译内核时自动对模块签名)。对于手动编译的内核模块,常见的实践是使用 dkms 的 SIGN_TOOL 选项指定一个脚本来实现自动签名

CONFIG_MODULE_SIG 开启的前提下,Secure Boot 被关闭时,开启选项 CONFIG_MODULE_SIG_FORCE 或者内核参数 module.sig_enforce 也可以实现仅允许加载带有有效签名的模块的功能。

配置 TPM

首先安装用于操作 TPM 2.0 的软件包 tpm2-toolstpm2-initramfs-tool

使用 TPM 安全地生成一个 64 字节(512 位)的密钥,以十六进制形式保存(注意设为仅 root 可读写):

(tpm2_getrandom --hex 32; tpm2_getrandom --hex 32) > /luks-key
chmod 600 /luks-key

将密钥添加到 LUKS 中(由于 512 位密钥包含的信息熵已经足够大,PBKDF 相关参数通过减少密钥派生函数的复杂度来提升性能):

cryptsetup luksAddKey /dev/sda3 /luks-key --pbkdf-force-iterations=4 \
                                            --pbkdf-parallel=1 \
                                            --pbkdf-memory=32
Enter any existing passphrase:

将 TPM 清除(只有第一次写入时需要先清除),并将密钥写入 TPM:

tpm2_clear
tpm2-initramfs-tool seal --data "$(cat /luks-key)" --pcrs 0,1,2,3,4,5,6,7,8,9

命令表示将密钥的文本数据写入到 TPM 中,以当前 PCR 0 - 9 的所有度量值进行认证。注意,tpm2-initramfs-tool 会默认将写入 TPM 的数据输出到标准输出,如果在脚本中运行,建议 > /dev/null 以防密钥泄露到日志中。

执行完成后,即可使用 unseal 操作读取 TPM 中的值。在重启后,如果被选定的任一 PCR 的度量值与写入时的值不符,则读取将会失败。

tpm2-initramfs-tool unseal --pcrs 0,1,2,3,4,5,6,7,8,9

自动解密

要使得开机时系统能够自动从 TPM 中获取密钥并解密,我们需要将 tpm2-initramfs-tool 放入 initrd 中,并编写解密脚本。

/etc/crypttab 提供了 keyscript= 选项来支持通过脚本获取解密密钥:

lvm-encrypted UUID=72fa928c-4df7-6448-ae04-11c909ff071e none luks,discard,keyscript=/etc/initramfs-tools/tpm2-cryptsetup

编写 /etc/initramfs-tools/tpm2-cryptsetup 脚本(并添加执行权限):

#!/bin/sh

unseal_tpm() {
    tpm2-initramfs-tool unseal --pcrs 0,1,2,3,4,5,6,7,8,9
}

ask_password() {
    stty -echo
    echo -n "Please enter the passphrase for $CRYPTTAB_NAME ($CRYPTTAB_SOURCE): " >&2
    read pass
    echo >&2
    stty echo
    echo -n $pass
}

if KEY="$(unseal_tpm)"; then
    echo -n "$KEY"
else
    ask_password
fi

该脚本会首先尝试从 TPM 中读取密钥,如果失败则会回退到提示用户手动输入密钥。根据 keyscript 的要求,密钥被输出到标准输出中。

编写一个 initramfs Hook,如 /etc/initramfs-tools/hooks/tpm2-cryptsetup-setup,来将 tpm2-initramfs-tool 工具和加密脚本加入到 intiramfs 中:

. /usr/share/initramfs-tools/hook-functions

copy_exec /usr/lib/x86_64-linux-gnu/libtss2-tcti-device.so.0
copy_exec /usr/bin/tpm2-initramfs-tool

最后执行 update-initramfs -u -k all 来重建 initrd,成功后重启,应该会先看到从 TPM 读取密钥的错误信息,再看到输入密码的提示,这是由于 initrd 改变使得 GRUB 写入的 PCR 9 的度量值改变,导致 TPM 拒绝输出密钥。进入系统后重新执行上述 tpm2-initramfs-tool seal 命令,再次重启后即可自动解密。

系统更新

当内核更新时,我们需要对新安装的内核进行签名。而对于 initrd,由于它的哈希值是 TPM 测量值的一部分,所以更新 initrd 一定会使 TPM 解锁失败。对于可以直接控制的设备,可以通过输入密码解决,而对于没有显示的服务器设备,我们可以暂时生成一个新的密钥,添加到 LUKS 中,并明文存储在硬盘上,在确保安全的情况下重启计算机,使计算机通过临时密钥解密。再次进入系统后,TPM 的测量值成为新值,此时将临时密钥从 LUKS 删除,并将原有的密钥再次存入 TPM 即可。这种方式与 BitLocker 的解决方案类似。

注意

当临时密钥存在时,请务必确保计算机重启过程是安全的。此时攻击者可以轻易获得明文密钥进行解密。对于开启 BitLocker 的 Windows 系统的系统更新也是如此。

工具脚本

我编写了一个用于自动化处理 TPM 2 解密设置的脚本 tpm2-luks-helper,具有以下功能:

  • 在更新内核时自动进行签名(通过 initramfs 的 post-update 来实现)
  • 在更新 GRUB 时自动重新签名并安装 GRUB 到 EFI 分区(通过 APT Hook 来实现)
  • 使用临时密钥来在更新系统后 TPM 不可用时自动解密,并通过检查内核参数 / initrd / GRUB EFI 是否变化来提示是否需要开启临时密钥(称之为 trusted-boot

将仓库下载到 /opt/tpm2-luks-helper(在 config.sh 中更改配置)并安装脚本主程序和 systemd 服务:

git clone https://github.com/Menci/tpm2-luks-helper /opt/tpm2-luks-helper
vim /opt/tpm2-luks-helper/config.sh # 修改配置
ln -sf /opt/tpm2-luks-helper/script.sh /usr/local/bin/tpm2-luks-helper
ln -sf /opt/tpm2-luks-helper/tpm2-luks-helper-on-boot.service /etc/systemd/system/
systemctl enable --now tpm2-luks-helper-on-boot

该 systemd 服务会在开机时记录内核参数以及 initrd 和 GRUB EFI 的哈希值,用于判断是否需要使用临时密钥,并自动删除使用后的临时密钥。

安装 initramfs post-update 脚本,来实现自动对新内核进行签名:

ln -sf /opt/tpm2-luks-helper/initramfs/post-update-sign-kernels /etc/initramfs/post-update.d/

/etc/crypttab 中的 keyscript 改为仓库中的脚本,并添加 initramfs Hook(注意,仓库中的脚本增加了 trusted-boot 使用临时密钥解密的功能,与上述脚本不同):

vim /etc/crypttab # 改为 keyscript=/opt/tpm2-luks-helper/initramfs/tpm2-cryptsetup
ln -sf /opt/tpm2-luks-helper/initramfs/tpm2-cryptsetup-setup /etc/initramfs-tools/hooks/
update-initramfs -u -k all

安装 APT Hook,在 GRUB 更新时自动重新安装 GRUB:

ln -sf /opt/tpm2-luks-helper/grub/99update-and-sign-grub /etc/apt/apt.conf.d/
# 执行 apt install --reinstall grub-efi-amd64-bin 来确认

修改 .bashrc,在执行重启 / 关机命令时检查下次启动是否可以正常解密:

alias reboot="tpm2-luks-helper check-next-boot && reboot"
alias shutdown="tpm2-luks-helper check-next-boot && shutdown"

如果检查到不满足自动解密的条件,脚本会提示:

reboot
Check failed: initramfs changed
  -   Current: "/boot//initrd.img-5.15.30-2-pve:a47803f6bbe1430a3d6fb50f75002c8c"
  - Next boot: "/boot//initrd.img-5.15.30-2-pve:06cae583f012d98371d34f3fd379e161"
Please run "tpm2-luks-helper trust-next-boot" and do a *attended* reboot.

此时运行 tpm2-luks-helper trust-next-boot 生成临时密钥,在下次引导时脚本将会自动从临时密钥解密。成功启动后临时密钥将被自动删除,之后继续使用 TPM 中的密钥解密。

攻击面

一般来说,存储在 TPM 中的密钥足够安全,难以被攻击者直接获得,而 Secure Boot 和 TPM 测量的存在也使得密钥在引导过程被篡改时无法获得密钥。但在常见的设备上,仍然有一些方式可以攻击使用 TPM 的硬盘加密。

冷启动攻击

用于加解密磁盘数据的密钥往往被存储在内存中。在通常条件下,DRAM 内存中的数据会在断电后迅速消失,而在低温条件下,DRAM 内存中的数据可以保持很久。攻击者可以启动计算机,等待系统从 TPM 中读取密钥并解密,然后使用制冷剂对内存冷冻,并从外部设备引导计算机,或将冷冻的内存移动到另一台计算机上,进入特制的操作系统读取数据。

一些现代 CPU 具有内存加密功能,可以在 CPU 读写内存时透明地对 DRAM 中的数据进行加解密,以抵御冷启动攻击。在纯软件的层面上,可以通过将密钥保持在 CPU 寄存器中而不写入内存的方式来阻止攻击者从内存中取得密钥,但内存中的其他敏感信息仍然能够被取得。

Evil Maid 攻击

在攻击者能够在计算机拥有者者不知情时物理接触到计算机的情况下,攻击者可以对计算机进行一些修改,制作一个假的输入密码界面,欺骗拥有者 TPM 解密失败,从而诱惑拥有者输入密码解密。对于纯软件层面的修改,拥有者可以通过检查 BIOS 设置、检查 Secure Boot 证书、检查未加密部分文件来识别这种攻击行为。对于硬件层面上的修改,拥有者无法检查,但一些安全设备可以通过物理熔断的方式来检测其是否被拆卸,从来识别这种攻击。

参考资料