2. 固件签名验证流程:从BootROM到内核模块
签名验证不是一步到位的。它是一连串的信任传递,像接力赛一样。每一级都要检查下一级的签名,只要有一棒掉了,整个安全链就断了。我见过太多设备,就是因为某一级验证没做好,被攻击者钻了空子。
这一章,我们就把这个链条拆开来看。从芯片上电的第一行代码,到内核加载驱动模块,每个环节的签名校验机制,我都会结合实战经验来讲。
2.1 BootROM中的签名校验——信任的起点
芯片一上电,CPU首先执行的是BootROM里的代码。这段代码是固化在芯片内部的,你改不了它。它负责初始化最基本的硬件,然后去加载下一级引导程序。
BootROM的签名校验,是整个信任链的根。如果这里被攻破,后面所有验证都是摆设。
核心逻辑:BootROM会读取外部存储(比如SPI Flash、eMMC)中的第一级引导镜像,用芯片内部固化的公钥去验证镜像的数字签名。验证通过,才把控制权交给它。
我拆过一台某品牌的工业路由器。它的BootROM验证流程大致是这样的:
- 读取引导头——从Flash固定偏移读取镜像头部,获取签名算法标识、签名长度、镜像大小等信息。
- 计算哈希——对镜像主体(不含签名部分)计算SHA-256哈希值。
- RSA验签——用BootROM里硬编码的RSA公钥,解密签名数据,与哈希值比对。
- 跳转执行——比对一致,跳转到镜像入口;不一致,死循环或重启。
你想想看,这个流程看起来挺严密的,对吧?但实际项目中,我遇到过几个典型的漏洞:
- 公钥可替换——有些芯片的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版本开始引入了模块签名验证功能。
内核模块的签名验证流程,说白了就是:
- 提取签名——内核在加载模块时,从.ko文件的末尾提取签名信息(PKCS#7格式)。
- 验证证书链——用内核内置的证书公钥,验证签名的证书链是否可信。
- 校验哈希——对模块的代码段和数据段计算哈希,与签名中的哈希比对。
- 决定加载与否——验证通过才允许加载,否则拒绝。
内核模块签名的数据结构,我整理了一个表格:
| 字段 | 偏移 | 说明 |
|---|---|---|
| 签名魔数 | 文件末尾 - 16字节 | 固定为"~Module signature appended~\n" |
| 签名长度 | 魔数前4字节 | PKCS#7签名数据的长度 |
| 签名数据 | 长度字段前 | DER编码的PKCS#7签名 |
| 模块数据 | 文件开头 | 实际的ELF模块内容 |
我在做车机固件逆向时,遇到过一种很有意思的绕过方式。那台车机的内核模块签名验证是开启的,但它的内核内置证书是自签名的。更致命的是,车机的文件系统里有一个脚本,允许用户通过U盘更新证书。
攻击路径就清晰了:
- 生成自己的证书和私钥
- 用私钥给恶意模块签名
- 通过U盘更新脚本,把自己的证书写入内核证书库
- 恶意模块就能顺利加载了
你看,这不是签名算法的问题,而是证书管理的问题。签名验证再强,如果证书可以被随意替换,那跟没有验证是一样的。
关键点:内核模块签名验证的安全性,取决于两个因素:
- 签名算法强度——RSA 2048位以上,SHA-256以上
- 证书管理策略——证书是否可被替换、是否锁死在只读存储中
很多设备在算法上没问题,但在证书管理上漏洞百出。
2.4 签名验证流程全景图
把上面三个环节串起来,就是完整的签名验证信任链。我画了一张图,帮你直观理解:
这张图展示的就是典型的四层信任链。每一层都依赖上一层的公钥来验证下一层的签名。攻击者只要攻破任意一层,就能控制后续所有环节。
2.5 常见的签名绕过切入点
根据我这些年做逆向的经验,签名绕过攻击的切入点主要集中在以下几个地方:
| 攻击层级 | 常见手法 | 难度 | 典型案例 |
|---|---|---|---|
| BootROM | 公钥替换、算法降级、缓冲区溢出 | 极高 | 某路由器芯片BootROM未锁定OTP公钥区域 |
| U-Boot | 配置签名绕过、未校验所有组件、签名检查可跳过 | 中高 | 安防摄像头fitImage配置签名未校验实际数据 |
| 内核 | 证书替换、关闭模块验证、利用未签名模块加载接口 | 中 | 车机通过U盘脚本替换内核证书 |
| 内核模块 | 伪造签名、利用证书链漏洞、降级攻击 | 中低 | IoT设备使用弱RSA密钥(512位) |
避坑指南:我曾经在分析一台智能网关时,花了三天时间研究它的BootROM签名算法,结果发现漏洞根本不在BootROM——它的U-Boot有一个调试串口没关闭,直接通过串口中断了启动流程,跳过了所有签名验证。所以,别只盯着技术细节,攻击面分析要全面。