3. Bootloader 安全:U-Boot 配置、签名验证与漏洞挖掘

Bootloader 是硬件上电后第一个运行的软件。它的安全直接决定了整个启动链的可信度。我见过太多团队把精力花在应用层加密上,却忽略了 Bootloader 这个“守门员”。门没锁,墙修再高也没用。

这一章,我们聚焦三个核心问题:U-Boot 怎么配才安全?签名验证到底怎么落地?攻击者通常从哪里下手?

3.1 U-Boot 安全配置:别让默认设置害了你

U-Boot 是嵌入式 Linux 世界的事实标准。但它的默认配置,坦白说,是为了开发便利,不是为了安全。我接手过一个项目,产品出厂后被人通过串口中断启动,直接进了 U-Boot shell,然后 dump 了整个文件系统。原因?开发者忘了关掉 bootdelay 和 stop_boot 功能。

3.1.1 必须关闭的“开发后门”

配置项 风险说明 建议操作
CONFIG_BOOTDELAY 启动时等待按键中断,进入命令行 设为 -1,彻底禁用中断
CONFIG_SYS_PROMPT 暴露 U-Boot 版本和编译信息 改为自定义字符串,隐藏版本号
CONFIG_CMD_USB 允许从 USB 设备加载镜像 量产固件中移除该命令
CONFIG_CMD_NET 允许通过网络加载镜像 量产固件中移除该命令
CONFIG_ENV_IS_IN_FLASH 环境变量可被物理篡改 配合签名校验,或使用只读存储
⚠️ 我曾经踩过的坑: 有次客户要求保留串口调试功能用于现场维护,但没做物理访问控制。结果被人用逻辑分析仪抓到了启动时的密钥派生过程。记住:串口一旦暴露,攻击面就打开了。如果必须保留,至少加上密码认证。

3.1.2 环境变量保护

U-Boot 的环境变量存储在 Flash 或 EEPROM 中。攻击者如果能修改 bootcmd 变量,就能加载任意代码。我建议的做法是:

  • 锁定关键变量:使用 setenv -l 锁定 bootcmd、bootargs 等变量,防止运行时修改。
  • 环境变量签名:对环境变量区域计算 HMAC,启动时验证完整性。
  • 冗余备份:在 Flash 中保留一份只读的出厂默认环境,用于恢复。

3.2 Bootloader 签名验证机制:信任链的第一环

签名验证的核心逻辑很简单:Bootloader 在加载下一阶段镜像(如 Linux 内核)之前,先验证镜像的数字签名。只有签名通过,才允许执行。但“简单”不等于“容易做对”。

3.2.1 典型的验证流程

我画了一张图,帮你理解整个流程:

Bootloader 签名验证流程 BootROM U-Boot SPL U-Boot 主镜像 Linux 内核 设备树 + 文件系统 验证 SPL 签名 验证 U-Boot 签名 验证内核签名 验证设备树签名 关键点: 1. 每一级 Bootloader 在加载下一级之前,必须验证其签名。 2. 公钥通常固化在 BootROM 或一次性可编程(OTP)区域。 3. 签名算法推荐 RSA-2048 或 ECDSA P-256。

3.2.2 签名验证的常见实现方式

U-Boot 支持两种主流的签名验证方式:

  • FIT Image(Flattened Image Tree):U-Boot 原生支持的镜像格式。镜像本身包含一个描述头,里面存放了哈希值和签名。U-Boot 在加载时会自动校验。
  • 外部签名工具:使用 mkimage 工具对镜像进行签名,然后将签名附加到镜像尾部或单独存储。

我个人习惯用 FIT Image,因为它把镜像、设备树、ramdisk 打包在一起,签名和配置都在一个文件里,管理起来方便。而且 U-Boot 对 FIT 的支持很成熟,几乎不需要额外写代码。

3.2.3 一个典型的 FIT Image 签名配置

// 在 U-Boot 配置中启用 FIT 签名
CONFIG_FIT=y
CONFIG_FIT_SIGNATURE=y
CONFIG_RSA=y
CONFIG_RSA_SOFTWARE_EXP=y
CONFIG_SPL_FIT=y
CONFIG_SPL_FIT_SIGNATURE=y

// 使用 mkimage 对 FIT 镜像签名
mkimage -f fit.its fit.itb
mkimage -F -k /path/to/keys -r fit.itb

这里要注意:-r 参数会在签名后标记镜像为“只读”,防止后续被篡改。我见过有人忘了加这个参数,结果签名后的镜像还能被重新签名,等于没保护。

3.3 Bootloader 漏洞挖掘方法:攻击者视角

要防守,先得知道攻击者怎么打。我这些年做安全审计,发现 Bootloader 的漏洞主要集中在几个地方。

3.3.1 常见攻击面

攻击面 典型漏洞 危害等级
串口/调试接口 未禁用中断、无密码保护 严重
镜像加载路径 未校验镜像来源(USB/网络) 严重
内存缓冲区 镜像解析时的缓冲区溢出 高危
环境变量 未锁定关键变量,可被篡改 高危
签名验证逻辑 验证失败后仍允许回退执行 严重
硬件调试接口 JTAG/SWD 未锁定 严重

3.3.2 漏洞挖掘方法

我总结了一套实用的挖掘流程,你可以照着做:

  1. 固件提取:从 Flash 或固件包中提取 U-Boot 镜像。用 binwalk 或 dd 命令。
  2. 静态分析:用 Ghidra 或 IDA Pro 反编译 U-Boot。重点关注字符串引用(如 bootcmd、load、verify)和命令处理函数。
  3. 动态调试:如果 JTAG 可用,直接硬件调试。否则通过串口交互,尝试输入异常命令或超长参数。
  4. 模糊测试:对镜像解析函数(如 FIT 解析、ext4 文件系统解析)进行 fuzz。我常用 afl-fuzz 配合 QEMU 模拟。
  5. 逻辑漏洞挖掘:检查签名验证失败后的处理逻辑。很多实现会在验证失败后打印错误,但仍然继续执行。这是致命的。

💡 一个小技巧: 在反编译 U-Boot 时,搜索 bootm 命令的处理函数。这个函数负责加载和启动内核,通常包含了签名验证的核心逻辑。找到它,你就找到了整个信任链的“咽喉”。

3.3.3 一个真实案例:验证失败后的回退

我曾经审计过一个 IoT 网关设备。它的 U-Boot 实现了签名验证,但代码逻辑是这样的:

if (verify_signature(image) == 0) {
    printf("Signature verification failed!\n");
    // 注意:这里没有 return 或 goto
}
// 继续执行加载
bootm_image(image);

看到了吗?验证失败后只打印了日志,然后继续执行 bootm_image。攻击者只需要提供一个未签名的镜像,设备就会乖乖加载。这就是典型的“日志型安全”——看起来有验证,实际上形同虚设。

⚠️ 教训: 签名验证失败后,必须立即停止执行,进入死循环或重启。不要给攻击者任何回退的机会。我现在的代码里,验证失败后直接调用 hang() 函数,让系统卡死。

3.4 总结一下

Bootloader 安全没有银弹。你需要做好三件事:

  • 配置上:关掉所有开发后门,锁定环境变量,隐藏版本信息。
  • 验证上:建立完整的信任链,每一级都要签名,验证失败必须死锁。
  • 测试上:从攻击者视角去挖掘漏洞,别只做功能测试。

嗯,这一章的内容就到这里。记住:Bootloader 是信任链的根,根烂了,上面的一切都是空中楼阁。