2. 固件签名验证流程:从BootROM到内核模块

签名验证不是一步到位的。它是一连串的信任传递,像接力赛一样。每一级都要检查下一级的签名,只要有一棒掉了,整个安全链就断了。我见过太多设备,就是因为某一级验证没做好,被攻击者钻了空子。

这一章,我们就把这个链条拆开来看。从芯片上电的第一行代码,到内核加载驱动模块,每个环节的签名校验机制,我都会结合实战经验来讲。

2.1 BootROM中的签名校验——信任的起点

芯片一上电,CPU首先执行的是BootROM里的代码。这段代码是固化在芯片内部的,你改不了它。它负责初始化最基本的硬件,然后去加载下一级引导程序。

BootROM的签名校验,是整个信任链的根。如果这里被攻破,后面所有验证都是摆设。

核心逻辑:BootROM会读取外部存储(比如SPI Flash、eMMC)中的第一级引导镜像,用芯片内部固化的公钥去验证镜像的数字签名。验证通过,才把控制权交给它。

我拆过一台某品牌的工业路由器。它的BootROM验证流程大致是这样的:

  1. 读取引导头——从Flash固定偏移读取镜像头部,获取签名算法标识、签名长度、镜像大小等信息。
  2. 计算哈希——对镜像主体(不含签名部分)计算SHA-256哈希值。
  3. RSA验签——用BootROM里硬编码的RSA公钥,解密签名数据,与哈希值比对。
  4. 跳转执行——比对一致,跳转到镜像入口;不一致,死循环或重启。

你想想看,这个流程看起来挺严密的,对吧?但实际项目中,我遇到过几个典型的漏洞:

  • 公钥可替换——有些芯片的BootROM会从外部OTP区域读取公钥,而不是用硬编码的。如果OTP没锁死,攻击者就能写入自己的公钥。
  • 签名算法可降级——BootROM支持多种签名算法,攻击者可以伪造一个头部,声明使用"无签名"或"弱哈希"算法。
  • 缓冲区溢出——镜像头部解析时,如果长度字段校验不严,可能触发溢出,直接跳过验签。

注意:BootROM的漏洞通常无法通过固件更新修复。因为BootROM本身是只读的。一旦发现BootROM级别的签名绕过漏洞,这颗芯片基本就废了——只能换硬件。

2.2 U-Boot/引导加载程序的校验——承上启下

BootROM把控制权交给U-Boot后,U-Boot就要负责加载内核了。U-Boot的签名校验,通常比BootROM更灵活,但也更复杂。

我个人习惯把U-Boot的签名校验分成两类:

类型 验证对象 常见实现
镜像级校验 整个内核镜像(zImage/Image) U-Boot的bootm命令自带验签功能
文件级校验 设备树(DTB)、ramdisk等独立文件 U-Boot的fitImage结构,每个子文件单独签名

这里重点说一下fitImage。它是U-Boot引入的一种镜像打包格式。一个fitImage文件里可以包含内核、设备树、ramdisk等多个组件,每个组件都可以有自己的签名。

fitImage的结构大概是这样:

/ {
  description = "Kernel Image with FDT";
  #address-cells = <1>;

  images {
    kernel@1 {
      data = /incbin/("zImage");
      hash@1 { algo = "sha256"; };
      signature@1 {
        algo = "rsa2048";
        key-name-hint = "dev-key";
        value = [ ... 签名数据 ... ];
      };
    };
    fdt@1 {
      data = /incbin/("board.dtb");
      hash@1 { algo = "sha256"; };
    };
  };

  configurations {
    default = "conf@1";
    conf@1 {
      kernel = "kernel@1";
      fdt = "fdt@1";
      signature@1 {
        algo = "rsa2048";
        key-name-hint = "dev-key";
        value = [ ... 配置签名 ... ];
      };
    };
  };
};

嗯,这里要注意。fitImage的签名可以放在两个地方:

  • 组件级别——每个镜像(kernel、fdt)单独签名
  • 配置级别——对整个配置(指定用哪个kernel+哪个fdt)签名

我曾经在分析一台安防摄像头时,发现它的U-Boot只校验了配置级别的签名,没校验组件级别的签名。这意味着什么?攻击者可以替换掉配置里引用的kernel组件,但保留原来的配置签名——因为配置签名只校验了"配置描述"本身,没校验实际数据。这就是一个典型的签名绕过点。

实战技巧:逆向分析U-Boot时,重点关注image_sign_info结构体和bootm_verify函数的实现。这两个地方决定了签名校验的逻辑。用IDA Pro加载U-Boot的二进制,搜索"rsa"、"sha"、"signature"等字符串,很快就能定位到验签代码。

2.3 内核模块校验——最后一道防线

内核启动后,还要加载各种内核模块(.ko文件)。这些模块通常有独立的签名机制。Linux内核从3.7版本开始引入了模块签名验证功能。

内核模块的签名验证流程,说白了就是:

  1. 提取签名——内核在加载模块时,从.ko文件的末尾提取签名信息(PKCS#7格式)。
  2. 验证证书链——用内核内置的证书公钥,验证签名的证书链是否可信。
  3. 校验哈希——对模块的代码段和数据段计算哈希,与签名中的哈希比对。
  4. 决定加载与否——验证通过才允许加载,否则拒绝。

内核模块签名的数据结构,我整理了一个表格:

字段 偏移 说明
签名魔数 文件末尾 - 16字节 固定为"~Module signature appended~\n"
签名长度 魔数前4字节 PKCS#7签名数据的长度
签名数据 长度字段前 DER编码的PKCS#7签名
模块数据 文件开头 实际的ELF模块内容

我在做车机固件逆向时,遇到过一种很有意思的绕过方式。那台车机的内核模块签名验证是开启的,但它的内核内置证书是自签名的。更致命的是,车机的文件系统里有一个脚本,允许用户通过U盘更新证书。

攻击路径就清晰了:

  • 生成自己的证书和私钥
  • 用私钥给恶意模块签名
  • 通过U盘更新脚本,把自己的证书写入内核证书库
  • 恶意模块就能顺利加载了

你看,这不是签名算法的问题,而是证书管理的问题。签名验证再强,如果证书可以被随意替换,那跟没有验证是一样的。

关键点:内核模块签名验证的安全性,取决于两个因素:

  • 签名算法强度——RSA 2048位以上,SHA-256以上
  • 证书管理策略——证书是否可被替换、是否锁死在只读存储中

很多设备在算法上没问题,但在证书管理上漏洞百出。

2.4 签名验证流程全景图

把上面三个环节串起来,就是完整的签名验证信任链。我画了一张图,帮你直观理解:

固件签名验证信任链流程图 固件签名验证信任链 第1级:BootROM 固化公钥 → 验证U-Boot镜像签名 第2级:U-Boot 内置公钥 → 验证内核/DTB/ramdisk签名 第3级:Linux内核 内置证书 → 验证内核模块(.ko)签名 第4级:内核模块 PKCS#7签名 → 验证模块完整性 每一级验证通过后,才将控制权移交给下一级。任何一级被绕过,后续验证全部失效。

这张图展示的就是典型的四层信任链。每一层都依赖上一层的公钥来验证下一层的签名。攻击者只要攻破任意一层,就能控制后续所有环节。

2.5 常见的签名绕过切入点

根据我这些年做逆向的经验,签名绕过攻击的切入点主要集中在以下几个地方:

攻击层级 常见手法 难度 典型案例
BootROM 公钥替换、算法降级、缓冲区溢出 极高 某路由器芯片BootROM未锁定OTP公钥区域
U-Boot 配置签名绕过、未校验所有组件、签名检查可跳过 中高 安防摄像头fitImage配置签名未校验实际数据
内核 证书替换、关闭模块验证、利用未签名模块加载接口 中 车机通过U盘脚本替换内核证书
内核模块 伪造签名、利用证书链漏洞、降级攻击 中低 IoT设备使用弱RSA密钥(512位)

避坑指南:我曾经在分析一台智能网关时,花了三天时间研究它的BootROM签名算法,结果发现漏洞根本不在BootROM——它的U-Boot有一个调试串口没关闭,直接通过串口中断了启动流程,跳过了所有签名验证。所以,别只盯着技术细节,攻击面分析要全面。