哈希算法基础:SHA-256原理、哈希碰撞与安全性、固件完整性校验
聊固件签名,绕不开哈希算法。说白了,哈希就是给数据算一个「指纹」。指纹是唯一的,数据变了,指纹就得变。我刚开始做固件时,总觉得哈希就是个校验和,后来踩了坑才明白——选错哈希,签名等于白做。
SHA-256 到底怎么工作的?
SHA-256 属于 SHA-2 家族,输出固定 256 位(32 字节)的摘要。你给它 1KB 的固件,它算 32 字节;给它 1MB 的固件,还是 32 字节。这个特性在签名里特别重要——我们不会去签名整个固件,而是签名它的哈希值。
它的内部流程大致分三步:
- 填充:把数据补成 512 位的整数倍,末尾还要加上原始长度信息。
- 分块:每 512 位(64 字节)为一个消息块,依次处理。
- 压缩:每个块经过 64 轮迭代运算,更新 8 个 32 位工作变量。
嗯,这里要注意——SHA-256 的每一轮都用到了位运算、模加和逻辑函数。它不是简单的异或,而是精心设计的非线性变换。我个人习惯用下面这张图来理解它的整体流程:
我在项目中遇到过有人用 MD5 做固件校验,结果被篡改了都没发现。SHA-256 的雪崩效应保证了——哪怕只改一个比特,输出的 256 位里大约一半的位都会翻转。这才是真正的「指纹」。
哈希碰撞与安全性
哈希碰撞,就是两个不同的输入算出了相同的摘要。理论上,任何哈希函数都有碰撞,因为输入空间无限,输出空间有限。但关键是——找到碰撞的难度有多大。
| 哈希算法 | 输出长度 | 碰撞攻击复杂度 | 当前状态 |
|---|---|---|---|
| MD5 | 128 bit | 2^18(秒级) | 已攻破,禁止使用 |
| SHA-1 | 160 bit | 2^63(理论可行) | 已攻破,禁止使用 |
| SHA-256 | 256 bit | 2^128(生日攻击) | 安全,推荐使用 |
| SHA-3-256 | 256 bit | 2^128 | 安全,备选方案 |
你想想看,2^128 是什么概念?就算你用全球所有的算力去跑,也要跑几十亿年。所以 SHA-256 的碰撞安全性,在可预见的未来是牢不可破的。
⚠️ 避坑指南
我曾经在一个遗留项目里看到固件校验还在用 SHA-1。当时客户要求升级,我查了一下——SHA-1 在 2017 年就被 Google 和 CWI 联合攻破了,他们演示了 SHAttered 攻击,生成了两个不同的 PDF 文件,SHA-1 摘要完全相同。如果你还在用 SHA-1,请立刻迁移到 SHA-256。
固件完整性校验实战
固件完整性校验,说白了就是回答一个问题:「我拿到的固件,是不是原始作者写的那一份?」
校验流程通常分两步:
- 生成参考值:在构建阶段,对固件二进制计算 SHA-256,得到参考摘要。
- 现场校验:设备启动时,对存储的固件重新计算 SHA-256,对比参考值。
代码实现其实不复杂。下面是我在嵌入式 Linux 环境下常用的校验片段:
#include <stdio.h>
#include <string.h>
#include <openssl/sha.h>
int verify_firmware(const char *fw_path, const unsigned char *expected_hash) {
FILE *fp = fopen(fw_path, "rb");
if (!fp) return -1;
SHA256_CTX ctx;
SHA256_Init(&ctx);
unsigned char buf[4096];
int bytes;
while ((bytes = fread(buf, 1, sizeof(buf), fp)) > 0) {
SHA256_Update(&ctx, buf, bytes);
}
fclose(fp);
unsigned char actual_hash[SHA256_DIGEST_LENGTH];
SHA256_Final(actual_hash, &ctx);
// 恒定时间比较,防止时序攻击
int result = 0;
for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
result |= actual_hash[i] ^ expected_hash[i];
}
return (result == 0) ? 0 : -1;
}
💡 个人经验
注意上面代码里的「恒定时间比较」。我见过有人用 memcmp 做哈希对比,结果被侧信道攻击利用——攻击者通过测量比较时间,逐字节猜出了哈希值。虽然嵌入式场景下这种攻击门槛高,但养成好习惯总没错。
哈希在签名流程中的位置
固件签名不是直接对固件签名,而是「先哈希,再签名」。为什么?
- 性能:非对称签名算法(如 RSA-2048)处理大块数据很慢。哈希把任意大小的固件压缩成 32 字节,签名效率高得多。
- 安全性:哈希的雪崩效应保证了签名无法被重用或伪造。
- 灵活性:你可以先校验哈希,再决定是否验证签名。在资源受限的 MCU 上,有时只做哈希校验就够了。
我习惯把整个流程画成一条链:
嗯,这里有个细节很多人忽略——哈希算法本身不提供防篡改能力。攻击者可以同时篡改固件和它附带的哈希值。所以哈希必须和签名配合使用,或者存储在安全区域(如 OTP、安全元件)。
核心要点总结
- SHA-256 输出固定 256 位,雪崩效应保证输入微小变化导致输出剧烈变化。
- MD5 和 SHA-1 已不安全,固件场景下严禁使用。
- 哈希碰撞的理论风险存在,但 SHA-256 的 2^128 复杂度在工程上不可行。
- 固件完整性校验必须使用恒定时间比较,防止时序侧信道攻击。
- 哈希是签名的前置步骤,不是替代方案——两者缺一不可。