2. 固件镜像的完整性校验:哈希算法(MD5、SHA-256)的原理与应用
各位同学,咱们接着聊固件升级。上一章我提到了一个核心问题:你怎么知道你下载下来的固件,就是开发者发布的那一份?中间有没有被人动过手脚?传输过程中有没有丢包、损坏?
答案就是——哈希校验。说白了,就是给固件算一个“数字指纹”。
2.1 哈希算法到底是什么?
哈希算法,也叫散列函数。它能把任意长度的数据,映射成一个固定长度的字符串。这个字符串就是“哈希值”或“摘要”。
我打个比方。你有一份固件,好比是一头大象。哈希算法就是一台绞肉机。你把整头大象塞进去,出来一坨肉馅。这坨肉馅就是哈希值。你没法从肉馅变回大象,这就是“不可逆”。而且,只要大象少了一条腿,出来的肉馅就完全不一样,这就是“雪崩效应”。
哈希算法有几个关键特性,我列出来给你看:
| 特性 | 说明 | 为什么重要 |
|---|---|---|
| 单向性 | 从哈希值无法反推出原始数据 | 攻击者拿到哈希值也没用,他造不出固件 |
| 抗碰撞性 | 很难找到两个不同数据有相同哈希值 | 防止攻击者用假固件冒充真固件 |
| 雪崩效应 | 输入改一个比特,输出完全变样 | 哪怕固件只坏了一个字节,校验也能发现 |
| 固定输出长度 | 不管输入多大,输出长度固定 | 方便存储和比对,不占太多空间 |
核心要点:哈希校验不是加密。加密是为了保密,哈希是为了完整性。你不需要把哈希值藏起来,你只需要确保它没有被篡改。
2.2 MD5 vs SHA-256:老将和新秀
说到具体的算法,MD5 和 SHA-256 是固件领域最常见的两个。我当年刚入行时,MD5 还是主流。现在嘛……我建议你尽量别碰 MD5 了。
2.2.1 MD5——曾经的主力
MD5 输出 128 位,也就是 32 个十六进制字符。它计算速度快,资源占用少。在早期的嵌入式设备上,这很重要。那时候的 MCU 主频才几十兆赫,RAM 也就几 KB,跑 SHA-256 确实吃力。
但是,MD5 已经被攻破了。2004 年,中国密码学家王小云团队就找到了 MD5 的碰撞方法。什么意思?就是你能人为构造两个不同的文件,让它们有相同的 MD5 值。
警告:我在 2018 年审计过一个客户的产品,他们还在用 MD5 做固件校验。我当场演示了如何构造一个恶意固件,MD5 值跟原厂固件一模一样。客户脸都绿了。从那以后,我坚决要求所有新项目至少用 SHA-256。
2.2.2 SHA-256——当前的标准
SHA-256 属于 SHA-2 家族,输出 256 位,也就是 64 个十六进制字符。它的安全性远高于 MD5。到目前为止,还没有任何有效的碰撞攻击方法。
当然,代价是计算量更大。在资源受限的 MCU 上,算一次 SHA-256 可能需要几百毫秒甚至几秒。但我觉得,这个代价是值得的。安全不能打折。
| 对比项 | MD5 | SHA-256 |
|---|---|---|
| 输出长度 | 128 位(32 字节) | 256 位(64 字节) |
| 安全性 | 已攻破(存在碰撞) | 目前安全 |
| 计算速度 | 快 | 较慢(约慢 2-3 倍) |
| 资源占用 | 低 | 较高 |
| 推荐场景 | 不推荐用于安全场景 | 固件完整性校验首选 |
2.3 在固件升级中怎么用?
好,理论说完了。咱们看看实际怎么操作。我画了一张流程图,帮你理解整个校验过程。
流程其实不复杂。开发者编译好固件,算一个哈希值。然后把固件和哈希值一起发布。设备端下载固件后,自己也算一个哈希值。两个值比对,一样就说明固件是完整的。
2.4 代码实现:手写一个校验函数
光说不练假把式。我给你看一段实际代码。这是在 STM32 上计算 SHA-256 的示例。我习惯用 mbedTLS 库,它轻量、移植性好。
#include "mbedtls/sha256.h"
/**
* @brief 计算固件镜像的 SHA-256 哈希值
* @param firmware 固件数据指针
* @param length 固件长度
* @param output 输出缓冲区(至少 32 字节)
* @return 0 成功,非0 失败
*/
int calculate_firmware_hash(const uint8_t *firmware, uint32_t length, uint8_t *output)
{
mbedtls_sha256_context ctx;
int ret;
// 1. 初始化上下文
mbedtls_sha256_init(&ctx);
// 2. 启动 SHA-256 计算(0 表示 SHA-256,非0 表示 SHA-224)
ret = mbedtls_sha256_starts(&ctx, 0);
if (ret != 0) {
mbedtls_sha256_free(&ctx);
return ret;
}
// 3. 喂数据。可以分多次喂,适合大固件
ret = mbedtls_sha256_update(&ctx, firmware, length);
if (ret != 0) {
mbedtls_sha256_free(&ctx);
return ret;
}
// 4. 收尾,得到哈希值
ret = mbedtls_sha256_finish(&ctx, output);
if (ret != 0) {
mbedtls_sha256_free(&ctx);
return ret;
}
// 5. 清理
mbedtls_sha256_free(&ctx);
return 0;
}
小技巧:对于大固件(比如几兆字节),不要一次性把整个固件加载到内存。用 mbedtls_sha256_update 分块喂数据,每次读一个扇区。我在一个项目里处理 8MB 的固件,RAM 只用了 4KB,就是这么干的。
2.5 避坑指南:我踩过的几个坑
做固件校验这么多年,我栽过跟头,也见过别人栽跟头。分享几个典型的坑:
- 哈希值传输不安全。我曾经见过一个产品,哈希值跟固件放在同一个 HTTP 响应里,没有任何签名。攻击者完全可以同时替换固件和哈希值。哈希校验就形同虚设了。正确的做法是:哈希值要用私钥签名,或者通过 HTTPS 等安全通道传输。
- 忘了校验哈希值本身。有个同事把哈希值写在固件头里,然后计算整个固件(包括头)的哈希。这就成了“先有鸡还是先有蛋”的问题。正确的做法是:固件头里放的是“预期哈希值”,计算哈希时排除这个字段。
- 字节序搞错。MCU 有大小端之分。PC 上算出来的哈希值,跟 MCU 上算出来的,如果字节序处理不一致,结果就不一样。我建议统一用大端序(网络字节序),这是行业惯例。
- 忽略空填充。有些固件末尾有填充字节(比如为了对齐 Flash 页)。计算哈希时,一定要明确:是算原始固件,还是算填充后的固件?我习惯算原始固件,填充部分不参与哈希计算。
我的建议:如果你现在还在用 MD5,尽快迁移到 SHA-256。如果 MCU 资源实在紧张,可以考虑 SHA-256 的硬件加速模块,很多新款的 MCU 都集成了。实在不行,至少用 SHA-1 过渡一下——虽然 SHA-1 也不安全了,但比 MD5 强一点。
2.6 小结
哈希校验是固件安全的第一道防线。它不复杂,但容易出错。记住三点:选对算法(SHA-256)、保护哈希值、注意实现细节。做到这三点,你的固件升级就迈出了坚实的一步。