4、固件加密与签名:固件镜像的AES加密与RSA签名流程,防止固件逆向与篡改。
好,咱们进入第四章。这一章聊的是固件安全里最核心的两个动作——加密和签名。
你想想看,固件一旦被提取出来,就像把你的源代码摊在桌面上。攻击者拿着IDA Pro或者Ghidra,分分钟就能逆向出你的业务逻辑、通信协议、甚至密钥。更可怕的是,他改掉你的固件,再刷回去,你的设备就变成他的肉鸡了。
怎么防?两个手段:AES加密 防止别人看懂你的固件;RSA签名 防止别人篡改你的固件。这两者缺一不可。
4.1 为什么加密和签名要分开做?
我见过不少工程师,觉得「加密了不就安全了吗?」。其实不是。
加密只解决「保密性」问题。你固件里的算法、密钥、逻辑,别人看不懂。但加密不解决「完整性」问题。攻击者可以拿你的加密固件,直接替换掉一部分密文,然后刷回去。设备解密后,得到的是乱码,但设备不知道这是被篡改过的。
签名解决的就是「完整性」和「真实性」。只有持有私钥的人才能生成合法签名。设备用公钥验证签名,只要签名不对,就拒绝启动。
所以,加密是锁门,签名是贴封条。两个都得做。
核心原则: 先签名,后加密。或者先加密,再对密文签名。但绝对不能只做其中一项。
4.2 AES加密流程:把固件变成天书
AES是对称加密,加密和解密用同一个密钥。速度快,适合处理大块数据。固件镜像动辄几MB,用RSA硬扛会慢到怀疑人生。
我个人习惯用AES-256-CBC模式。为什么是CBC?因为同样的明文块,每次加密结果都不一样(因为有IV向量),能有效防止模式识别。
4.2.1 加密步骤
- 生成随机密钥和IV:AES-256需要32字节密钥,16字节IV。每次加密都用新随机数。
- 填充固件:AES是块加密,块大小16字节。固件长度不一定正好是16的倍数,需要PKCS#7填充。
- 逐块加密:用密钥和IV,对填充后的固件进行CBC模式加密。
- 输出密文:通常把IV放在密文前面,方便解密时提取。
// 伪代码示例:AES-256-CBC加密固件
uint8_t aes_key[32]; // 随机生成
uint8_t iv[16]; // 随机生成
uint8_t *plaintext; // 原始固件
size_t plaintext_len; // 原始长度
// 1. PKCS#7填充
size_t padded_len = plaintext_len + (16 - plaintext_len % 16);
uint8_t *padded = malloc(padded_len);
memcpy(padded, plaintext, plaintext_len);
// 填充字节值为填充长度
uint8_t pad_val = padded_len - plaintext_len;
memset(padded + plaintext_len, pad_val, pad_val);
// 2. AES-CBC加密
uint8_t *ciphertext = malloc(padded_len);
AES_CBC_encrypt(padded, padded_len, aes_key, iv, ciphertext);
// 3. 输出:IV + 密文
uint8_t *output = malloc(16 + padded_len);
memcpy(output, iv, 16);
memcpy(output + 16, ciphertext, padded_len);
我的经验: 密钥不要硬编码在固件里。我在一个项目里见过有人把AES密钥写在代码注释里,还写着「此密钥请勿外传」……嗯,结果固件被提取后,密钥直接暴露。正确的做法是:密钥从安全元件(SE)或OTP区域读取,或者由Bootloader派生。
4.3 RSA签名流程:给固件贴上防伪标签
RSA是非对称加密,用私钥签名,公钥验签。私钥放在你的安全编译服务器上,公钥烧死在设备的Bootloader里。
签名不是对整个固件做RSA运算——那样太慢了。而是先对固件做哈希,再对哈希值签名。
4.3.1 签名步骤(生产环境)
- 计算固件哈希:用SHA-256对固件(或加密后的固件)计算摘要,得到32字节哈希值。
- RSA签名:用私钥对哈希值进行RSA签名,输出256字节(2048位RSA)或512字节(4096位RSA)签名值。
- 打包:将签名值附加到固件后面,或者放在固件头部。
// 伪代码:RSA-SHA256签名
uint8_t *firmware; // 固件数据(可以是明文或密文)
size_t fw_len;
RSA *private_key; // 从安全存储加载
// 1. 计算SHA-256哈希
uint8_t hash[32];
SHA256(firmware, fw_len, hash);
// 2. RSA签名(PKCS#1 v1.5或PSS)
uint8_t signature[256]; // 2048位RSA
size_t sig_len;
RSA_sign(NID_sha256, hash, 32, signature, &sig_len, private_key);
// 3. 打包:固件 + 签名
uint8_t *package = malloc(fw_len + sig_len);
memcpy(package, firmware, fw_len);
memcpy(package + fw_len, signature, sig_len);
4.3.2 验签步骤(设备端Bootloader)
- 提取签名:从固件包末尾取出签名值。
- 计算哈希:对固件部分重新计算SHA-256。
- RSA验签:用公钥验证签名是否匹配。
- 通过则启动,失败则拒绝:签名不对,直接进入恢复模式或报错。
我曾经踩过的坑: 有个项目,Bootloader验签通过了,但固件启动后还是跑飞了。查了两天,发现是签名时用的哈希算法是SHA-1,而验签时误配成了SHA-256。哈希值长度不同,但RSA验签函数没报错,因为签名长度固定。结果就是:签名验证形同虚设。所以,哈希算法必须严格一致,最好在代码里写死,不要用配置项。
4.4 加密+签名的组合策略
实际项目中,加密和签名怎么组合?有三种常见方案:
| 方案 | 流程 | 优点 | 缺点 |
|---|---|---|---|
| 先签名后加密 | 明文固件 → 签名 → 加密 → 传输 | 解密后直接验签,流程简单 | 设备需先解密再验签,暴露明文窗口 |
| 先加密后签名 | 明文固件 → 加密 → 对密文签名 → 传输 | 验签时无需解密,可提前拒绝篡改固件 | 解密后仍需验签(可选),多一步 |
| 加密+签名并行 | 对明文签名,再加密整个包(含签名) | 最安全,篡改和泄露都防 | 设备需先解密,再验签,计算量大 |
我个人推荐先加密后签名。为什么?因为设备在验签时,不需要先解密。如果签名不对,直接丢弃,省了解密的计算开销。而且,攻击者拿到的密文包,即使破解了加密,也无法伪造签名——因为他没有私钥。
避坑指南: 签名时,一定要把固件的版本号、芯片ID、产品ID等信息也纳入哈希计算。否则,攻击者可以把旧版本固件(有漏洞的)签名替换到新设备上。这叫「版本回滚攻击」。我见过一个IoT设备因此被批量攻破,教训深刻。
4.5 密钥管理:最容易被忽视的一环
加密和签名算法本身是公开的,安全的核心在于密钥。密钥泄露,一切白搭。
- AES密钥:每个设备可以不同,由唯一ID派生。或者使用密钥协商协议(如ECDH)生成。
- RSA私钥:保存在离线服务器,永远不要联网。使用HSM(硬件安全模块)存储。
- RSA公钥:烧死在Bootloader的只读区域,或者由安全元件提供。防止被替换。
嗯,这里要注意:公钥虽然叫「公」,但你不能随便放。如果攻击者替换了Bootloader里的公钥,他就可以用自己的私钥签名恶意固件。所以,公钥的存储区域必须是一次性编程(OTP)或者安全启动链保护的。
4.6 总结
固件加密和签名,说白了就是两件事:让别人看不懂,让别人改不了。AES负责加密,RSA负责签名。两者结合,才能构建一个相对安全的固件防护体系。
我在做第一个量产项目时,只做了加密没做签名。结果固件被替换,设备批量变砖。从那以后,我再也不敢省签名这一步了。你想想看,攻击者拿到你的设备,最想干的就是替换固件。签名就是最后一道防线。
下一章,咱们聊聊安全启动链——怎么从Bootloader到APP,层层验证,确保每一级代码都是可信的。