使用 UEFI 安全启动掌控您的 PC

作者:Greig Paul

UEFI(统一可扩展固件接口)是过时的 BIOS 标准的开放、多供应商替代品,BIOS 标准最初于 1976 年出现在 IBM 计算机中。UEFI 标准非常广泛,涵盖了完整的启动架构。本文重点介绍 UEFI 的一个有用但通常被忽视的功能:安全启动。

您可能只在首次设置计算机时禁用 UEFI 安全启动时才遇到它,并且它经常受到诟病。事实上,安全启动的引入曾引发争议,争议的焦点是微软负责签署第三方操作系统代码,这些代码将在安全启动环境下启动。

在本文中,我们将探讨安全启动的基础知识以及如何掌控它。我们将介绍如何安装您自己的密钥并使用这些密钥签署您自己的二进制文件。我们还将展示如何构建一个独立的 GRUB EFI 二进制文件,这将保护您的系统免受篡改,例如冷启动攻击。最后,我们将展示如何使用全盘加密来保护整个硬盘,包括内核镜像(通常需要以未加密方式存储)。

UEFI 安全启动

安全启动旨在保护系统免受恶意代码在启动过程早期(操作系统加载之前)被加载和执行的影响。这是为了防止恶意软件安装“启动套件”并保持对计算机的控制以掩盖其存在。如果启用安全启动时加载了无效的二进制文件,则会提醒用户,并且系统将拒绝启动被篡改的二进制文件。

在每次启动时,UEFI 固件都会检查加载的每个 EFI 二进制文件,并确保它具有有效的签名(由本地信任的证书支持)或二进制文件的校验和存在于允许列表中。它还会验证签名或校验和是否未出现在拒绝列表中。受信任的证书或校验和列表作为 EFI 变量存储在 UEFI 固件环境使用的非易失性存储器中,以存储设置和配置数据。

UEFI 密钥概述

图 a 显示了用于安全启动的四个主要 EFI 变量。平台密钥(通常缩写为 PK)提供对安全启动密钥层次结构的完全控制。PK 的持有者可以安装新的 PK 并更新 KEK(密钥交换密钥)。这是第二个密钥,它可以直接签署可执行的 EFI 二进制文件,也可以用于签署 db 和 dbx 数据库。db(签名数据库)变量包含允许的签名证书列表或允许的二进制文件的加密哈希值。dbx 是 db 的逆变量,它用作特定证书或哈希值的黑名单,这些证书或哈希值原本会被接受,但不应能够运行。只有 KEK 和 db(以绿色显示)密钥可以签署可能启动系统的二进制文件。

图 a. 安全启动密钥

大多数系统上的 PK 由硬件制造商颁发,而 KEK 由操作系统供应商(例如微软)持有。硬件供应商通常也安装了自己的 KEK(因为可以存在多个 KEK)。要完全拥有使用安全启动的计算机,您需要更换(至少)PK 和 KEK,以防止在未经您同意的情况下安装新密钥。如果您想阻止商业签名的 EFI 二进制文件在您的系统上运行,您还应该更换签名数据库 (db)。

安全启动旨在允许对计算机具有物理控制权的人员掌控已安装的密钥。预安装的制造商 PK 只能通过使用现有 PK 对其进行签名来以编程方式更换。通过物理访问计算机以及访问 UEFI 固件环境,可以删除此密钥并安装新密钥。需要物理访问系统才能覆盖默认密钥是安全启动的一项重要安全要求,以防止恶意软件完成此过程。请注意,某些锁定的基于 ARM 的设备实施 UEFI 安全启动,但无法更改预安装的密钥。

测试程序

您可以在物理计算机上或 Intel Tianocore 参考 UEFI 实现的虚拟化实例中按照这些步骤进行操作。大多数 Linux 发行版中提供的 ovmf 软件包都包含此实现。QEMU 虚拟化工具可以启动 ovmf 的实例进行实验。请注意,fat 参数指定目录 storage 将作为持久存储卷呈现给虚拟化固件。在当前工作目录中创建此目录,然后启动 QEMU


qemu-system-x86_64 -enable-kvm -net none \
-m 1024 -pflash /usr/share/ovmf/ovmf_x64.bin \
-hda fat:storage/

启动 QEMU 时,此文件夹中存在的文件将作为卷显示给虚拟化 UEFI 固件。请注意,在启动 QEMU 后添加到其中的文件不会出现在系统中 — 重新启动 QEMU,它们就会出现。此目录可用于保存您要安装到 UEFI 固件的公钥,以及稍后在过程中启动的 UEFI 镜像。

生成您自己的密钥

安全启动密钥是采用 X.509 证书格式的自签名 2048 位 RSA 密钥。请注意,大多数实现目前不支持大于 2048 位的密钥长度。您可以使用以下 openssl 命令生成 2048 位密钥对(有效期为 3650 天或十年)


openssl req -new -x509 -newkey rsa:2048 -keyout PK.key \
-out PK.crt -days 3650 -subj "/CN=My Secure PK/"

CN 主题可以根据您的意愿进行自定义,并且其值并不重要。生成的 PK.key 是私钥,而 PK.crt 是相应的证书(包含公钥),您稍后将安装到 UEFI 固件中。您应该将私钥安全地存储在加密存储设备的安全位置。

现在您可以对 KEK 和 db 密钥执行相同的过程。请注意,db 和 KEK EFI 变量可以包含多个密钥(对于 db,还可以包含可启动二进制文件的 SHA256 哈希值),尽管为简单起见,本文仅考虑在每个变量中存储单个证书。这对于掌控您自己的计算机来说已绰绰有余。再次强调,.key 文件是私钥,应安全存储,而 .crt 文件是要安装到 UEFI 系统变量中的公钥证书。

取得所有权并安装密钥

每个 UEFI 固件界面都不同,因此无法提供有关如何安装您自己的密钥的分步说明。请参阅您的主板或笔记本电脑的说明手册,或在线搜索 UEFI 固件的制造商。进入 UEFI 固件界面,通常在启动时按住某个键,然后找到安全菜单。在这里,应该有一个安全启动的部分或子菜单。将模式控制更改为“自定义”模式。这应该允许您访问密钥管理菜单。

图 1. 启用安全启动并进入自定义模式

此时,您应该备份当前安装的 UEFI 平台密钥。您应该不需要此备份,因为您的 UEFI 固件界面中应该有一个选项可以恢复默认密钥,但谨慎一点总没坏处。应该有一个选项可以将当前密钥导出或保存到 USB 闪存驱动器。如果检测到任何问题,最好使用 FAT 文件系统格式化它。

将备份密钥复制到安全位置后,将您之前创建的公钥证书 (.crt) 文件加载到 USB 闪存驱动器上。注意不要将它们与之前的备份证书混淆。进入 UEFI 固件界面,并使用该选项重置或清除所有现有的安全启动密钥。

图 2. 擦除现有的平台密钥

这也可能被称为“取得”安全启动的“所有权”。您的系统现在处于安全启动“设置”模式,该模式将一直保持到安装新的 PK。此时,EFI PK 变量不受系统保护,可以从 UEFI 固件界面或计算机上运行的软件(例如操作系统)加载新值。

图 3. 从存储设备加载新密钥

此时,您应该暂时禁用安全启动,以便继续阅读本文。您新安装的密钥将在启用安全启动后仍然有效。

签署二进制文件

安装自定义 UEFI 签名密钥后,您需要签署您自己的 EFI 二进制文件。有多种不同的方法可以构建(或获取)这些文件。大多数现代 Linux 引导加载程序都与 EFI 兼容(例如,GRUB 2、rEFInd 或 gummiboot),并且 Linux 内核本身自 3.3 版本以来可以构建为可启动的 EFI 二进制文件。可以签署和启动任何有效的 EFI 二进制文件,尽管您在此处采取的方法取决于您的偏好。

一种选择是直接签署内核镜像。如果您的发行版使用二进制内核,则您需要在重新启动系统之前签署每个新的内核更新。如果您使用自行编译的内核,则需要在构建内核后签署每个内核。但是,这种方法需要您密切关注内核更新并签署每个镜像。这可能会变得繁琐,尤其是当您使用滚动发布发行版或测试主线候选版本时。另一种方法,也是我们在本文中使用的方法,是签署一个锁定的、与 UEFI 兼容的引导加载程序(本文中为 GRUB 2),并使用它从您的系统启动各种内核。

某些发行版配置 GRUB 以针对发行版指定的公钥(他们使用该公钥签署所有内核二进制文件)验证内核镜像签名,并在使用安全启动时禁用编辑内核 cmdline 变量。因此,您应该参考您的发行版的文档,因为在这种情况下,关于确保您的启动镜像已加密的部分不是必需的。

Linux sbsigntools 软件包可从大多数 Linux 发行版的存储库中获得,并且是签署 UEFI 二进制文件时的首选。UEFI 安全启动二进制文件应使用 Authenticode 格式签名进行签名。感兴趣的命令是 sbsign,其调用方式如下


sbsign --key DB.key --cert DB.crt unsigned.efi \
--output signed.efi

由于 UEFI 标准的实现存在细微差异,因此某些系统可能会拒绝来自 sbsign 的正确签名的二进制文件。我们发现的最佳替代方案是使用 osslsigncode 实用程序,它也生成 Authenticode 签名。虽然此工具并非专门用于安全启动,但它生成的签名符合所需的规范。由于 osslsigncode 似乎未普遍包含在发行版存储库中,因此您应该从其源代码构建它。该过程相对简单,只需运行 make 即可生成可执行二进制文件。如果您遇到任何问题,请确保您已安装 opensslcurl,它们是软件包的依赖项。(有关源代码存储库的链接,请参阅“资源”)。

二进制文件以类似于 sbsign 的方式使用 osslsigntool 进行签名(请注意,根据 UEFI 规范,哈希值定义为 sha256;不应更改此值)


osslsigncode -certs DB.crt -key DB.key \
-h sha256 -in unsigned.efi -out signed.efi
使用 UEFI 启动

在您签署了 EFI 二进制文件(例如 GRUB 引导加载程序二进制文件)之后,显而易见的下一步是测试它。使用传统 BIOS 启动技术的计算机从所选启动设备的 MBR(主引导记录)加载初始操作系统引导加载程序。MBR 包含用于加载磁盘内保存的更高级别(和更大)引导加载程序的代码,该引导加载程序加载操作系统。相比之下,UEFI 旨在允许一个驱动器上存在多个引导加载程序,而无需这些引导加载程序进行协作甚至知道彼此的存在。

可启动的 UEFI 二进制文件位于存储设备(例如硬盘)上的标准路径内。包含这些二进制文件的分区称为 EFI 系统分区。它在 gdisk 中具有 0xEF00 的分区 ID,gdisk 是与 fdisk 兼容的 GPT 等效项。此分区通常位于文件系统的开头,并使用 FAT32 文件系统格式化。然后,UEFI 可启动二进制文件作为文件存储在 EFI/BOOT/ 目录中。

如果此签名的二进制文件放置在 EFI 系统分区或外部驱动器内的 EFI/BOOT/BOOTX64.EFI 中,并设置为启动设备,则现在应该可以启动。在一个 EFI 系统分区上拥有多个可用的 EFI 二进制文件是可能的,这使得创建多启动设置更加容易。但是,为了使其工作,UEFI 固件需要在其非易失性存储器中创建启动项。否则,将使用默认文件名 (BOOTX64.EFI)(如果存在)。

要将新的 EFI 二进制文件添加到固件的可用二进制文件列表中,您应该使用 efibootmgr 实用程序。此工具可以在发行版存储库中找到,并且通常由流行引导加载程序(例如 GRUB)的安装程序自动使用。

此时,您应该在 UEFI 固件中重新启用安全启动。为了确保安全启动正常运行,您应该尝试启动未签名的 EFI 二进制文件。为此,您可以将二进制文件(例如未签名的 GRUB EFI 二进制文件)放置在 FAT32 格式化的 USB 闪存驱动器上的 EFI/BOOT/BOOTX64.EFI 中。使用 UEFI 固件界面将此驱动器设置为当前启动驱动器,并确保出现安全警告,从而停止启动过程。您还应该验证使用默认 UEFI 安全启动密钥签名的镜像是否无法启动 — Ubuntu 12.04(或更高版本)CD 或可启动 USB 驱动器应该允许您验证这一点。最后,您应该确保您的自签名二进制文件能够正确启动且没有错误。

安装独立 GRUB

默认情况下,GRUB 引导加载程序使用存储在 /boot/grub/grub.cfg 中的配置文件。通常,任何能够修改您的 /boot 分区内容的人都可以编辑此文件,无论是通过启动到另一个操作系统还是将您的驱动器放入另一台计算机中。

引导加载程序安全

在安全启动和 UEFI 出现之前,假定对计算机具有物理访问权限的人员可以完全访问该计算机。用户密码可以通过简单地将 init=/bin/bash 添加到内核 cmdline 参数来绕过,并且计算机将直接启动到 root shell 中,从而完全访问系统上的所有文件。

设置全盘加密是保护您的数据免受物理攻击的一种方法 — 如果硬盘的内容已加密,则必须先解密硬盘才能启动系统。在没有解密密钥的情况下,无法挂载磁盘的分区,因此数据受到保护。

另一种方法是防止攻击者更改内核 cmdline 参数。但是,在大多数计算机上,可以通过安装新的引导加载程序来轻松绕过此方法。此引导加载程序不必遵守原始引导加载程序施加的限制。在许多情况下,可能不需要更换引导加载程序 — GRUB 和其他引导加载程序可以通过单独的配置文件进行完全配置,可以编辑该文件以绕过安全限制,例如密码。

因此,签署 GRUB 引导加载程序实际上没有任何安全优势,因为签名的(和经过验证的)引导加载程序随后将从硬盘加载未签名的模块并使用未签名的配置文件。通过让 GRUB 创建一个包含所有必需模块和配置文件的独立可启动 EFI 二进制文件,您不再需要信任 GRUB 二进制文件的模块和配置文件。签署 GRUB 二进制文件后,未经安全启动拒绝并拒绝加载,就无法对其进行修改。此故障会提醒您有人试图通过修改引导加载程序来入侵您的计算机。

如前所述,在某些发行版上,此步骤可能不是必需的,因为它们的 GRUB 引导加载程序在启用安全启动时会自动对内核强制执行类似的限制和检查。因此,本节旨在为那些未使用此类发行版或希望出于学习目的而自行实施类似功能的人员提供帮助。

要创建独立的 GRUB 二进制文件,需要 grub-mkstandalone 工具。此工具应包含在最新的 GRUB2 发行软件包中


grub-mkstandalone -d /usr/lib/grub/x86_64-efi/ \
-O x86_64-efi --modules="part_gpt part_msdos" \
--fonts="unicode" --locales="en@quot" \
--themes=""  -o "/home/user/grub-standalone.efi" \
"boot/grub/grub.cfg=/boot/grub/grub.cfg"

有关此处使用的参数的更详细说明,请参见 grub-mkstandalone 的手册页。重要的参数是 -o,它指定要使用的输出文件,以及最后一个字符串参数,它指定当前 GRUB 配置文件的路径。生成的独立 GRUB 二进制文件可以直接启动,并包含一个 memdisk,其中包含配置文件和模块以及配置文件。现在可以签署此 GRUB 二进制文件并用于启动系统。请注意,当 GRUB 配置文件重新生成时,例如在添加新内核、更改启动参数或在列表中添加新操作系统后,应重复此过程,因为嵌入的配置文件将与常规系统配置文件过时。

许可警告

由于 GRUB 2 在 GPLv3(或更高版本)下获得许可,因此引发了一个需要注意的事项。虽然对于个人用户(他们只需安装新的安全启动密钥并启动修改后的引导加载程序)而言不是问题,但如果 GRUB 2 引导加载程序(或任何其他 GPL-v3 许可的引导加载程序)使用私有签名密钥进行签名,并且分布式计算机系统旨在阻止使用未签名的引导加载程序,则使用 GPL-v3 许可的软件将不符合许可。这是 GPLv3 的所谓反 Tivo 化条款的结果,该条款要求用户能够在其系统上安装和执行其自己修改的 GPLv3 软件版本,而不会受到技术限制。

锁定 GRUB

为了防止恶意用户修改您系统的内核 cmdline(例如,指向不同的 init 二进制文件),应设置 GRUB 密码。GRUB 密码在使用加密哈希函数进行哈希处理后存储在配置文件中。使用 grub-mkpasswd-pbkdf2 命令生成密码哈希,该命令将提示您输入密码。

PBKDF2 函数是一种慢哈希,旨在进行计算密集型运算并防止对密码进行暴力破解攻击。如果需要,可以使用 -c 参数调整其性能,以便通过在快速计算机上执行更多轮 PBKDF2 来进一步减慢该过程。默认值为 10,000 轮。复制此密码哈希后,应将其添加到您的 GRUB 配置文件中(通常位于 /etc/grub.d 或类似位置)。在文件 40_custom 中,添加以下内容


set superusers="root"
password_pbkdf2 root <generated password hash>

这将创建一个名为 root 的 GRUB 超级用户帐户,该帐户能够启动任何 GRUB 条目、编辑现有启动项并进入 GRUB 控制台。如果没有进一步的配置,则还需要此密码才能启动系统。如果您希望在每次启动时都设置另一个密码,则可以跳过下一步。但是,如果使用全盘加密,则几乎不需要在每次启动时都要求输入密码。

要删除在正常启动时输入超级用户密码的要求,请编辑标准启动菜单模板(通常为 /etc/grub.d/10-linux),然后找到创建常规菜单项的行。它应该看起来与此类似


echo "menuentry '$(echo "$title" | grub_quote)' 
 ↪${CLASS} \$menuentry_id_option 
 ↪'gnulinux-$version-$type-$boot_device_id' {" | sed
 ↪"s/^/$submenu_indentation/"

通过在左大括号之前添加参数 --unrestricted 来更改此行。此更改告诉 GRUB 启动此条目不需要密码提示。根据您的发行版和 GRUB 版本,该行的确切内容可能有所不同。生成的行应与此类似


echo "menuentry '$(echo "$title" | grub_quote)' ${CLASS}
 ↪\$menuentry_id_option 
 ↪'gnulinux-$version-$type-$boot_device_id'
 ↪--unrestricted {" | sed "s/^/$submenu_indentation/"

在添加超级用户帐户并配置启动密码的需求(或不需求)后,应重新生成主 GRUB 配置文件。此命令特定于发行版,但通常为 update-grubgrub-mkconfig。还应重新生成和测试独立的 GRUB 二进制文件。

保护内核

此时,您应该拥有一个能够启动签名的(和受密码保护的)GRUB 引导加载程序的系统。没有您的密钥的对手将无法修改引导加载程序或其配置或模块。同样,攻击者也无法更改引导加载程序传递给内核的参数。但是,他们可以修改您的内核镜像(通过将硬盘交换到另一台计算机中)。然后,这将由 GRUB 启动。尽管 GRUB 可以验证内核镜像签名,但这需要您重新签署每个内核更新。

另一种方法是使用全盘加密来保护整个系统,包括内核镜像、根文件系统和您的主目录。这可以防止某人移除您计算机的驱动器并访问您的数据或对其进行修改 — 在不知道您的加密密码的情况下,驱动器内容将不可读(因此不可修改)。

大多数在线指南将显示全盘加密,但为了便于启动,会留下一个单独的、未加密的 /boot 分区(其中包含内核和 initrd 镜像)。通过仅创建一个加密的根分区,硬盘上将不会存储未加密的内核或 initrd。当然,如果您愿意,您可以创建一个单独的启动分区并使用 dm-crypt 正常加密它。

考虑到各种特定于发行版的必要更改,执行包括启动分区在内的全盘加密的完整过程本身就值得写一篇文章。但是,一个好的起点是 ArchLinux Wiki(请参阅“资源”)。与传统加密设置的主要区别在于使用 GRUB GRUB_ENABLE_CRYPTODISK=y 配置参数,该参数告诉 GRUB 在加载主 GRUB 菜单之前尝试解密加密卷。

为了避免每次启动都输入两次加密密码,可以使用系统的 /etc/crypttab 使用密钥文件自动解密文件系统。然后,可以将此密钥文件包含在文件系统的(加密的)initrd 中(请参阅您的发行版文档以了解如何将其添加到 initrd,以便每次为内核更新重新生成 initrd 时都将其包含在内)。

此密钥文件应归 root 用户所有,并且不需要任何用户或组具有对其的读取访问权限。同样,您应该为 initrd 镜像(在启动分区中)提供相同的保护,以防止在系统启动并提取密钥文件时访问它。

最终考虑事项

UEFI 安全启动允许您掌控可以在您的计算机上运行的代码。安装您自己的密钥允许您防止恶意人员轻易地在您的计算机上启动他们自己的代码。将此与全盘加密相结合将使您的数据免受未经授权的访问和盗窃,并防止攻击者诱骗您启动恶意内核。

作为最后一步,您应该为您的 UEFI 设置界面应用密码,以防止物理攻击者访问您计算机的设置界面并安装他们自己的 PK、KEK 和 db 密钥,就像这些说明所做的那样。但是,您应该意识到,您的主板或笔记本电脑的 UEFI 实现中的漏洞可能会允许绕过或删除此密码,并且通过系统上的“救援模式”重新刷新 UEFI 固件的能力可能会清除 NVRAM 变量。尽管如此,通过掌控安全启动并使用它来保护您的系统,您应该能够更好地防范恶意软件或那些临时物理访问您的计算机的人员。

资源

有关第三方安全启动密钥的信息: http://mjg59.dreamwidth.org/23400.html

有关安全启动的密钥和内部工作原理的更多信息: http://blog.hansenpartnership.com/the-meaning-of-all-the-uefi-keys

osslsigncode 存储库: http://sourceforge.net/projects/osslsigncode

ArchLinux Wiki 关于完全加密系统的说明: https://wiki.archlinux.org.cn/index.php/Dm-crypt/Encrypting_an_entire_system#Encrypted_boot_partition_.28GRUB.29

关于包括内核镜像的全盘加密指南: http://www.pavelkogan.com/2014/05/23/luks-full-disk-encryption

Fedora Wiki 关于其安全启动实现的说明: https://fedoraproject.org/wiki/Features/SecureBoot

加载 Disqus 评论