4、固件镜像格式:常见固件格式(如U-Boot、UEFI、Linux内核镜像)及镜像结构解析
在Secure Boot的验签流程中,固件镜像的格式决定了签名覆盖的范围、摘要计算的方式以及验签触发的时机。如果对镜像格式理解不透彻,即使签名算法正确,也可能因为签名覆盖区域与加载器实际校验区域不一致而导致启动失败或安全绕过。本章将深入解析U-Boot、UEFI和Linux内核镜像三种主流固件格式的布局结构,并说明各自在验签流程中的关键字段与边界。
上图展示了三种固件格式在验签视角下的核心差异。U-Boot的签名通常内嵌在镜像头部或独立签名块中,UEFI固件采用PE/COFF的Authenticode签名机制,而Linux内核镜像则依赖引导加载器对vmlinuz整体或分段的哈希校验。下面分别展开。
4.1 U-Boot镜像格式:uImage与fitImage
4.1.1 核心原理:传统uImage与新型fitImage
U-Boot作为嵌入式系统中最常用的引导加载器,其镜像格式经历了从传统uImage到设备树+fitImage的演进。传统uImage格式由U-Boot工具mkimage生成,头部固定64字节,包含镜像类型、加载地址、入口地址、数据大小和CRC32校验值。其结构如下:
typedef struct image_header {
uint32_t ih_magic; /* 镜像魔数 0x27051956 */
uint32_t ih_hcrc; /* 头部CRC32 */
uint32_t ih_time; /* 时间戳 */
uint32_t ih_size; /* 数据大小 */
uint32_t ih_load; /* 加载地址 */
uint32_t ih_ep; /* 入口地址 */
uint32_t ih_dcrc; /* 数据CRC32 */
uint8_t ih_os; /* 操作系统类型 */
uint8_t ih_arch; /* CPU架构 */
uint8_t ih_type; /* 镜像类型 */
uint8_t ih_comp; /* 压缩类型 */
uint8_t ih_name[32]; /* 镜像名称 */
} image_header_t;
fitImage(Flattened Image Tree)则基于Device Tree语法,将内核、设备树、ramdisk等多个组件打包在一个ITB文件中,每个组件可以独立配置哈希算法和签名密钥。fitImage的签名信息存放在/signature节点下,支持RSA2048/4096和ECDSA等算法。
4.1.2 判断方法与操作流程:验签覆盖范围
在Secure Boot场景下,U-Boot的验签流程分为两个阶段:
- 第一阶段(SPL/TPL):验证U-Boot主镜像的签名,通常使用固定在ROM中的公钥哈希。
- 第二阶段(U-Boot主程序):验证fitImage中各组件的签名,公钥从已验证的U-Boot环境中获取。
对于传统uImage,U-Boot只校验头部CRC和数据CRC,不提供内建签名验证机制。若需验签,必须在U-Boot源码中启用CONFIG_FIT_SIGNATURE并改用fitImage格式。操作流程如下:
# 生成RSA密钥对
openssl genrsa -out dev.key 2048
openssl rsa -in dev.key -pubout -out dev.pub
# 使用mkimage生成带签名的fitImage
mkimage -f fit.its -k keys/ -K u-boot.dtb fit.itb
# 在U-Boot配置中启用验签
CONFIG_FIT_SIGNATURE=y
CONFIG_RSA=y
CONFIG_RSA_VERIFY=y
4.1.3 案例拆解:fitImage验签失败排查
某工业控制板卡在升级U-Boot后出现启动失败,日志显示Verifying Hash ... Bad Hash。排查过程如下:
- 初始现象:设备在加载fitImage时校验失败,但镜像文件在PC上使用
mkimage -l检查正常。 - 关键参数:U-Boot版本为2020.04,fitImage使用SHA256+RSA2048签名,公钥存放在u-boot.dtb中。
- 分析判断:使用
dumpimage -l fit.itb查看镜像内部结构,发现签名节点中的key-name-hint指向dev,但u-boot.dtb中实际密钥名称为dev-key,名称不匹配导致公钥查找失败。 - 解决步骤:重新生成fitImage时指定
-K u-boot.dtb并确保key-name-hint与设备树中节点名称一致,重新烧写后启动正常。
风险提示:fitImage的签名只覆盖各组件的数据段,不包含fitImage的头部描述信息。攻击者可以修改组件描述(如加载地址)而不破坏签名,因此必须同时启用CONFIG_FIT_ENABLE_SHA256_SUPPORT对描述节点进行哈希锁定。
4.1.4 常见误区与总结
- 误区一:认为uImage也支持签名验签。实际上传统uImage的CRC只是完整性校验,不具备防伪能力。
- 误区二:忽略
required属性。在fitImage的签名节点中,required = "conf"表示强制验证配置节点,若缺失此属性,U-Boot可能跳过验签。 - 总结:U-Boot验签的核心在于明确签名覆盖的数据边界,以及公钥的信任链传递。建议使用fitImage并配合
CONFIG_FIT_SIGNATURE,同时将公钥哈希固化在ROM或OTP中。
4.2 UEFI固件格式:PE/COFF与Authenticode签名
4.2.1 核心原理:UEFI的安全启动链
UEFI固件采用PE/COFF(Portable Executable / Common Object File Format)作为可执行文件格式,其安全启动(Secure Boot)机制基于UEFI签名数据库(db、dbx、KEK、PK)。UEFI固件镜像本身(如Shell.efi、grubx64.efi)在加载时由固件中的安全启动驱动验证签名。PE/COFF镜像的关键结构包括:
- DOS头:包含
e_lfanew字段,指向PE头偏移。 - PE头:包含签名信息偏移量(
SecurityDirectory),指向WIN_CERTIFICATE表。 - WIN_CERTIFICATE:包含Authenticode签名,覆盖整个镜像文件(除证书表自身和校验和字段)。
4.2.2 判断方法与操作流程:验证签名覆盖范围
UEFI的Authenticode签名计算范围是从文件头到WIN_CERTIFICATE之前的所有字节,加上证书表中的dwLength字段之后的内容。具体验证流程:
- 解析PE头,定位
SecurityDirectory的偏移和大小。 - 读取WIN_CERTIFICATE结构,获取证书类型(
WIN_CERT_TYPE_PKCS_SIGNED_DATA)。 - 计算整个文件(排除证书表和
CheckSum字段)的哈希值。 - 使用db/dbx数据库中的公钥验证PKCS#7签名。
# 使用pesign工具查看签名信息
pesign -i grubx64.efi -l -c
# 使用sbverify验证签名
sbverify --cert db.crt grubx64.efi
4.2.3 案例拆解:UEFI Secure Boot签名绕过
某服务器厂商在固件更新后,发现Linux发行版无法启动,提示Verification failed: (0x1A) Security violation。分析过程:
- 背景:新固件启用了Secure Boot,但未将发行版的shim签名加入db数据库。
- 关键日志:
efi: Security violation,且事件日志中记录SignatureType: X509。 - 分析判断:使用
sbverify --list检查shim.efi的签名者,发现其证书链指向Microsoft UEFI CA,但固件db中只包含OEM自定义证书。 - 解决步骤:通过固件设置界面将Microsoft UEFI CA证书导入db,或使用
KeyTool.efi在启动时手动添加。重启后系统正常引导。
经验要点:UEFI Secure Boot的验签不仅验证镜像签名,还验证签名证书是否在db数据库中且未被dbx吊销。实际部署中,建议使用shim+grub的二级引导链,shim由Microsoft签名,grub由发行版签名,避免每次更新都需修改db。
4.2.4 常见误区与总结
- 误区一:认为UEFI只验证EFI系统分区中的文件。实际上,Option ROM、UEFI驱动、甚至NVRAM中的启动项都会被验证。
- 误区二:忽略
imageSize字段。PE头中的SizeOfImage与实际文件大小不一致时,可能导致签名验证范围错误。 - 总结:UEFI验签的关键在于理解Authenticode的覆盖范围和证书信任链。建议使用
sbverify和pesign工具进行预验证,并确保固件中的db/dbx与发行版签名策略一致。
4.3 Linux内核镜像格式:vmlinuz与签名附加
4.3.1 核心原理:内核镜像的组成与签名方式
Linux内核镜像通常以vmlinuz(压缩内核)形式存在,其结构包括:
- boot_params:实模式启动参数,包含
hdr结构,其中kernel_info_offset指向内核签名信息。 - 压缩内核数据:使用gzip、xz或zstd压缩的vmlinux二进制。
- 附加签名:在压缩数据之后,可以附加PKCS#7签名或独立的哈希表。
内核验签通常由引导加载器(如GRUB)或内核自身的CONFIG_MODULE_SIG机制完成。对于Secure Boot场景,GRUB在加载vmlinuz时会调用verify_pe_file或verify_linux函数进行验签。
4.3.2 判断方法与操作流程:内核签名验证
GRUB的linux命令加载内核时,会执行以下验签流程:
- 读取vmlinuz文件头部的
boot_params,检查kernel_info_offset是否有效。 - 定位到签名数据区域,提取PKCS#7签名。
- 计算从文件头到签名数据之前的所有字节的哈希值。
- 使用db数据库中的证书验证签名。
# 使用sbsigntool对内核镜像签名
sbsign --key db.key --cert db.crt --output vmlinuz.signed vmlinuz
# 使用sbverify验证签名
sbverify --cert db.crt vmlinuz.signed
4.3.3 案例拆解:内核模块签名导致的启动失败
某嵌入式设备在启用Secure Boot后,内核启动到一半时崩溃,日志显示module verification failed: signature and/or required key missing。排查过程:
- 背景:内核开启了
CONFIG_MODULE_SIG_FORCE,但设备树中未包含用于验证模块的公钥。 - 关键参数:内核版本5.10,模块签名使用SHA256+RSA,公钥编译进内核或通过
module.sig_enforce参数控制。 - 分析判断:使用
modinfo检查模块签名,发现签名者证书未在内核密钥环中。通过keyctl show查看内核密钥环,确认缺少.builtin_trusted_keys。 - 解决步骤:将公钥编译进内核(
CONFIG_SYSTEM_TRUSTED_KEYS),或使用mokutil --import导入MOK(Machine Owner Key)。重新编译内核后模块加载正常。
实用建议:对于嵌入式Linux,建议将内核签名与模块签名分离。内核镜像使用UEFI db签名,模块使用独立的MOK密钥,这样在更新模块时无需修改固件db。
4.3.4 常见误区与总结
- 误区一:认为内核签名覆盖整个vmlinuz文件。实际上,签名数据本身不参与哈希计算,且
boot_params中的某些字段(如kernel_alignment)在验签前可能被修改。 - 误区二:忽略initramfs的验签。initramfs通常不包含在vmlinuz签名范围内,攻击者可以替换initramfs中的脚本。建议将initramfs也纳入签名或使用加密。
- 总结:Linux内核验签的关键在于明确签名数据的位置和覆盖范围,以及密钥的信任链管理。建议使用sbsigntool统一签名,并在内核配置中启用
CONFIG_SECURITY_LOCKDOWN_LSM增强安全性。
4.4 三种格式的对比与选型建议
| 维度 | U-Boot (fitImage) | UEFI (PE/COFF) | Linux内核 (vmlinuz) |
|---|---|---|---|
| 签名位置 | 镜像内部签名节点 | WIN_CERTIFICATE表 | 文件尾部附加数据 |
| 签名算法 | RSA/ECDSA | RSA (PKCS#7) | RSA (PKCS#7) |
| 覆盖范围 | 各组件数据段 | 整个文件(除证书表) | 文件头到签名数据前 |
| 密钥管理 | 设备树中的公钥 | db/dbx数据库 | 内核密钥环/MOK |
| 验签时机 | SPL/U-Boot加载时 | 固件加载EFI应用时 | GRUB加载内核时 |
| 灵活性 | 高(可独立签名组件) | 中(依赖固件实现) | 中(需引导器配合) |
在实际项目中,选择哪种格式取决于硬件平台和启动链设计。对于ARM嵌入式平台,U-Boot+fitImage是主流选择;对于x86服务器,UEFI Secure Boot是标准方案;而Linux内核签名则作为纵深防御的补充。无论采用哪种格式,都必须确保签名覆盖所有可执行代码和关键配置数据,并建立从根密钥到最终镜像的完整信任链。
最终提醒:镜像格式的解析是Secure Boot验签流程的基础。在实施验签前,务必使用二进制查看工具(如hexdump、objdump)确认签名数据的实际偏移和大小,避免因格式理解偏差导致验签失败或安全漏洞。