3. UEFI安全启动原理:固件启动流程、安全数据库与签名验证
安全启动(Secure Boot)这个话题,说白了就是解决一个问题:你怎么知道当前加载的这段代码是可信的? 我刚开始接触UEFI时,觉得这玩意儿就是个开关,打开就安全,关上就裸奔。后来踩了几个坑才明白,它背后是一整套密码学信任链。
咱们这一章,就把UEFI安全启动的底裤扒开看看。从固件怎么启动,到数据库怎么存证书,再到签名怎么验,一条线捋清楚。
3.1 UEFI固件启动流程:信任链的起点
先看整体流程。我习惯把UEFI启动分成四个阶段,每个阶段都有一道签名验证的关卡。
嗯,这里要注意:SEC阶段是硬件信任根。CPU上电后第一条指令就在SEC里,它负责验证PEI阶段的代码。如果SEC被篡改,整个信任链就崩了。我在某款国产平台上就遇到过SEC阶段签名校验没做完整,结果被植入恶意PEI模块的情况。
3.2 安全数据库:db、dbx、dbt
安全启动的核心,就是三个数据库。它们存储在固件的非易失性变量里,由平台密钥(PK)保护。
| 数据库 | 全称 | 作用 | 存储内容 |
|---|---|---|---|
| db | Allowed Database | 白名单,允许加载的签名者 | X.509证书、SHA-256哈希、EFI_CERT_TYPE |
| dbx | Forbidden Database | 黑名单,禁止加载的签名者 | 吊销的证书、已知恶意软件的哈希 |
| dbt | Timestamp Database | 时间戳吊销列表 | 吊销时间戳(用于检测过期签名) |
我的经验:dbx的更新非常敏感。我曾经帮客户处理过一个案例,他们误刷了错误的dbx更新包,导致所有旧版驱动都无法加载,整批设备变砖。最后只能拆机用编程器重写SPI Flash。所以,更新dbx前一定要做兼容性测试。
3.3 签名验证机制:到底怎么验?
签名验证不是简单比个哈希。它分三步走:
- 提取签名:从EFI镜像的WIN_CERTIFICATE结构中提取PKCS#7签名数据。
- 查黑名单:先查dbx,看签名者证书或镜像哈希是否被吊销。如果命中,直接拒绝。
- 查白名单:再查db,看签名者证书是否在信任链中。如果在,用公钥验签;如果不在,拒绝。
为什么会先查dbx再查db?你想想看,如果某个证书同时出现在db和dbx里,那应该以谁为准?dbx优先级更高。这是安全设计的基本原则——宁可错杀,不可放过。
关键数据结构:EFI镜像的签名信息嵌入在PE/COFF头中,具体位置在IMAGE_DIRECTORY_ENTRY_SECURITY指向的WIN_CERTIFICATE结构。验证时,UEFI固件会计算整个镜像的哈希(排除签名本身),然后用db中的公钥解密签名中的哈希,两者比对。
3.4 代码示例:验证一个EFI模块的签名
下面是我在实际项目中写的一段验证逻辑(简化版),展示了核心流程:
// 伪代码:UEFI安全启动签名验证
EFI_STATUS VerifyImageSignature(
IN VOID *ImageBase,
IN UINTN ImageSize,
OUT BOOLEAN *IsTrusted
) {
WIN_CERTIFICATE *Cert;
EFI_IMAGE_DATA_DIRECTORY *SecDir;
// 1. 定位安全目录
SecDir = GetSecurityDirectory(ImageBase);
if (SecDir == NULL || SecDir->Size == 0) {
*IsTrusted = FALSE;
return EFI_UNSUPPORTED;
}
// 2. 提取PKCS#7签名
Cert = (WIN_CERTIFICATE *)((UINT8 *)ImageBase + SecDir->VirtualAddress);
if (Cert->wCertificateType != WIN_CERT_TYPE_PKCS_SIGNED_DATA) {
*IsTrusted = FALSE;
return EFI_INVALID_PARAMETER;
}
// 3. 计算镜像哈希(排除签名区域)
UINT8 Hash[SHA256_DIGEST_SIZE];
CalculateImageHash(ImageBase, ImageSize, Cert, Hash);
// 4. 查dbx(黑名单)
if (IsInDbx(Cert, Hash)) {
*IsTrusted = FALSE;
return EFI_SECURITY_VIOLATION;
}
// 5. 查db(白名单)并验签
if (!VerifyAgainstDb(Cert, Hash)) {
*IsTrusted = FALSE;
return EFI_ACCESS_DENIED;
}
*IsTrusted = TRUE;
return EFI_SUCCESS;
}
我曾经踩过的坑:在第三步计算哈希时,一定要排除签名数据本身。否则你算出来的哈希包含了签名,而签名又依赖于哈希,这就成了死循环。UEFI规范里明确说了,计算范围是从镜像基址到签名数据起始位置,再加上签名数据之后的内容。我当时没注意偏移量计算,导致验签一直失败,排查了整整两天。
3.5 安全启动的三种模式
UEFI规范定义了三种安全启动模式,我习惯用一张表来对比:
| 模式 | 状态 | 验证行为 | 适用场景 |
|---|---|---|---|
| Setup Mode | 未配置PK | 不验证签名,允许安装任何密钥 | 工厂预装、OEM开发调试 |
| User Mode | 已配置PK | 严格验证签名,拒绝未签名模块 | 生产环境、用户设备 |
| Audit Mode | 已配置PK(审计标志位) | 验证签名但不阻止加载,仅记录日志 | 兼容性测试、问题排查 |
我个人习惯在开发阶段用Audit Mode。这样既能看到哪些模块签名有问题,又不会直接导致系统启动失败。等所有驱动都验证通过后,再切到User Mode。
3.6 避坑指南:安全启动常见问题
- 签名算法不匹配:db里存的是RSA-2048证书,但镜像用的是SHA-1哈希。UEFI 2.5之后已经弃用SHA-1,建议统一用SHA-256。
- 证书链不完整:中间CA证书没导入db,导致叶子证书验签失败。我遇到过某厂商只导入了根证书,结果所有子CA签发的驱动都无法加载。
- dbx更新后无法回滚:这是设计如此——吊销就是不可逆的。所以更新前一定要备份原始固件,或者准备硬件恢复方案。
- 时间戳问题:dbt里的吊销时间戳如果比系统RTC时间还晚,会导致已吊销的签名仍然通过验证。嗯,这里要注意RTC电池没电的情况。
一个小技巧:在调试安全启动问题时,可以打开UEFI的串口日志(通过
gEfiSerialDxeProtocolGuid),查看SecBoot相关的调试信息。它会打印出每次验证的详细结果,包括匹配了哪个证书、哈希值是多少。这比黑盒猜测要高效得多。