3. 哈希算法入门:MD5、SHA-1、SHA-256的原理与对比
做固件校验,绕不开哈希算法。说白了,哈希就是给一段数据算出一个固定长度的“指纹”。指纹变了,数据肯定被人动过。我在项目里见过太多因为选错哈希算法导致的校验事故,所以这一章咱们把MD5、SHA-1、SHA-256这三个家伙彻底聊透。
3.1 哈希算法到底在干什么?
你想想看,一个固件镜像动辄几兆甚至几十兆。你要验证它有没有被篡改,总不能逐字节去比对吧?哈希算法就是来解决这个问题的。它把任意长度的输入,通过一个数学函数,压缩成一个固定长度的输出——这个输出就叫哈希值、摘要或者指纹。
好的哈希算法有三个核心特性:
- 单向性:从哈希值反推原始数据,理论上不可能。嗯,这里要注意,是“计算上不可行”,不是绝对不可能。
- 抗碰撞性:很难找到两个不同的输入,算出相同的哈希值。
- 雪崩效应:输入改一个比特,输出就面目全非。
核心概念:哈希不是加密。加密可以解密,哈希不行。哈希是“摘要”,不是“密文”。
3.2 MD5:曾经的老大哥,现在的过气网红
MD5(Message Digest 5)是1992年诞生的,输出128位,也就是32个十六进制字符。我在刚入行那会儿,MD5几乎是校验的标配。那时候大家觉得它又快又好用。
但好景不长。2004年,中国密码学家王小云团队宣布找到了MD5的碰撞——也就是说,他们能构造出两个不同的文件,但MD5值一模一样。我当时看到这个消息,心里咯噔一下。后来我在一个OTA升级项目里,亲眼看到有人用MD5做校验,结果被攻击者用碰撞攻击替换了固件。从那以后,我再也不敢在安全场景里用MD5了。
警告:MD5已经不安全。不要在任何涉及安全校验的场景中使用MD5。它只适合做非安全性的数据去重或校验和。
3.3 SHA-1:比MD5强一点,但也不行了
SHA-1(Secure Hash Algorithm 1)由美国国家安全局(NSA)设计,1995年发布为标准。输出160位,比MD5长32位。理论上,它的抗碰撞性比MD5强。
但2017年,Google和CWI Amsterdam联合宣布了SHAttered攻击——他们成功构造了SHA-1碰撞。我记得当时我们团队正在评估一个老产品的固件升级方案,用的就是SHA-1。看到这个消息后,我们连夜改了方案,全部迁移到SHA-256。
你可能会问:SHA-1现在还在用吗?说实话,在一些老旧设备或者非安全场景里,确实还有人用。但我个人建议,只要你的固件涉及安全校验,就别碰SHA-1了。
| 算法 | 输出长度 | 安全性状态 | 典型应用 |
|---|---|---|---|
| MD5 | 128位(16字节) | 已破解,不安全 | 非安全校验、数据去重 |
| SHA-1 | 160位(20字节) | 已破解,不推荐 | 老旧系统兼容 |
| SHA-256 | 256位(32字节) | 安全,主流选择 | 固件校验、数字签名、TLS |
3.4 SHA-256:为什么它是主流?
SHA-256属于SHA-2家族,也是NSA设计的,2001年发布。输出256位,比SHA-1多了96位。别小看这96位,它让暴力破解的难度呈指数级上升。
我个人的习惯是:只要做固件镜像校验,默认就用SHA-256。原因有三:
- 安全性足够:到目前为止,没有公开的SHA-256碰撞攻击。理论上它的安全强度是128位,这在可预见的未来是够用的。
- 硬件支持好:现在的MCU和SoC,很多都内置了SHA-256硬件加速器。我在STM32和ESP32上都用过,硬件算起来比软件快一个数量级。
- 生态成熟:OpenSSL、mbedTLS、Linux内核、各种Bootloader,全都原生支持SHA-256。你不用操心兼容性问题。
经验之谈:在嵌入式环境里,如果MCU没有硬件加速,SHA-256的软件实现可能会有点慢。我曾经在一个Cortex-M0的芯片上跑SHA-256,算一个1MB的固件花了将近3秒。这种情况下,可以考虑用硬件加速或者优化实现。
3.5 三种算法的原理对比(通俗版)
我不打算把数学公式贴出来,那玩意儿看着头疼。咱们用大白话聊聊它们的核心区别:
- MD5:把数据分成512位的块,每块做4轮运算,每轮16步。说白了就是一顿乱搅,最后输出128位。结构简单,所以快,但也容易被攻破。
- SHA-1:也是512位一块,但做80步运算,输出160位。比MD5多搅了几轮,安全性好一些,但终究没扛住。
- SHA-256:块大小变成1024位,做64步运算,输出256位。每一步的运算逻辑比SHA-1复杂得多,用了更多的位运算和逻辑函数。说白了,搅得更狠,更难找到规律。
为什么会这样?因为攻击者要找的就是数学规律。MD5和SHA-1的数学结构相对简单,被找到了弱点。SHA-256的设计更保守,更“混乱”,所以目前还扛得住。
3.6 固件校验中的哈希算法选择建议
基于我这些年的经验,给你几个实际建议:
- 新项目一律用SHA-256:别犹豫,这是最省心的选择。
- 老项目迁移要评估性能:如果MCU太老,跑SHA-256太慢,可以考虑SHA-224(SHA-256的截断版)或者硬件加速。
- 不要自己实现哈希算法:我曾经见过有人自己写了个“简化版SHA-256”,结果漏洞百出。用现成的库,比如mbedTLS、OpenSSL、或者芯片厂商提供的驱动。
- 哈希校验要配合签名:哈希只能检测数据是否被篡改,但不能验证数据来源。要防伪造,还得加上数字签名(比如RSA或ECDSA)。这个后面章节会讲。
一句话总结:MD5已死,SHA-1垂危,SHA-256当立。做固件校验,选SHA-256就对了。
3.7 哈希算法在固件校验中的位置
下面这张图展示了哈希算法在整个固件校验流程中的位置。你可以看到,哈希是校验的核心环节,但不是全部。
从图上你可以看到,哈希算法不是孤立存在的。它需要配合安全存储(存放参考哈希)和比较逻辑,才能构成完整的校验流程。我曾经在一个项目里踩过坑——参考哈希存在了Flash的明文区域,结果攻击者连固件带哈希一起改了,校验形同虚设。所以,哈希值本身也要保护好。
3.8 代码示例:用mbedTLS计算SHA-256
在嵌入式环境里,我常用mbedTLS。下面是一个计算SHA-256的示例代码:
#include "mbedtls/sha256.h"
#include <string.h>
#include <stdio.h>
int calculate_sha256(const uint8_t *data, size_t len, uint8_t output[32]) {
mbedtls_sha256_context ctx;
int ret;
mbedtls_sha256_init(&ctx);
ret = mbedtls_sha256_starts(&ctx, 0); // 0表示SHA-256,1表示SHA-224
if (ret != 0) {
printf("SHA-256 starts failed: %d\n", ret);
goto exit;
}
ret = mbedtls_sha256_update(&ctx, data, len);
if (ret != 0) {
printf("SHA-256 update failed: %d\n", ret);
goto exit;
}
ret = mbedtls_sha256_finish(&ctx, output);
if (ret != 0) {
printf("SHA-256 finish failed: %d\n", ret);
goto exit;
}
exit:
mbedtls_sha256_free(&ctx);
return ret;
}
// 使用示例
void verify_firmware(const uint8_t *firmware, size_t fw_size) {
uint8_t hash[32];
uint8_t expected_hash[32] = { /* 从安全存储读取的参考哈希 */ };
if (calculate_sha256(firmware, fw_size, hash) != 0) {
printf("Hash calculation failed!\n");
return;
}
if (memcmp(hash, expected_hash, 32) == 0) {
printf("Firmware verified successfully.\n");
} else {
printf("Firmware verification FAILED!\n");
}
}
小技巧:如果MCU支持硬件SHA-256,mbedTLS会自动检测并使用硬件加速。你不需要改代码,只需要在编译时启用对应的宏定义,比如MBEDTLS_SHA256_ALT。